├── .github └── dependabot.yml ├── .gitignore ├── README.md ├── jest.config.js ├── mocha.html ├── package.json ├── renovate.json ├── src ├── base-encryptor.spec.ts ├── base-encryptor.ts ├── browser-encryptor.spec.ts ├── browser-encryptor.ts ├── encryptor.spec.ts └── encryptor.ts ├── tsconfig.json └── yarn.lock /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /test-dist 4 | /coverage 5 | /.cache 6 | /.nyc_output -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # secure-e2ee 2 | 3 | Secure end-to-end-encryption for Node.js and Browser. 4 | 5 | ## Usage 6 | 7 | ```ts 8 | import Encryptor from "secure-e2ee"; 9 | 10 | const encryptor = new Encryptor( 11 | // encryption secret 12 | // needs to be 32 characters long 13 | "e761daf732c272ee0db9bd71f49c66a0", 14 | 15 | // old encryption secrets 16 | // that have been rotated out. 17 | // (optional) 18 | [ 19 | "e761daf732c272ee0db9bd71f49c66a0", 20 | "c272732c66aee0db9bd71f49e761daf0", 21 | ] 22 | ); 23 | 24 | const cipher = encryptor.encrypt("I ❤️ Blitz.js"); 25 | 26 | // send over the wire 27 | 28 | const original = encryptor.decrypt(cipher); 29 | 30 | // original === "I ❤️ Blitz.js"; 31 | ``` 32 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: [ 5 | "src" 6 | ] 7 | }; -------------------------------------------------------------------------------- /mocha.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Tests 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 18 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secure-e2ee", 3 | "version": "0.4.0", 4 | "description": "Secure end-to-end-encryption", 5 | "main": "dist/encryptor.js", 6 | "browser": "dist/browser-encryptor.js", 7 | "author": "Simon Knott", 8 | "license": "MIT", 9 | "files": [ 10 | "dist/" 11 | ], 12 | "scripts": { 13 | "build": "tsc", 14 | "test:node": "nyc mocha -r ts-node/register src/encryptor.spec.ts", 15 | "test:browser": "parcel mocha.html -d test-dist --open", 16 | "prepack": "yarn build" 17 | }, 18 | "devDependencies": { 19 | "@types/chai": "4.2.21", 20 | "@types/md5": "^2.3.0", 21 | "@types/mocha": "^8.2.2", 22 | "@types/node": "^16.7.12", 23 | "chai": "^4.3.4", 24 | "mocha": "^9.1.1", 25 | "nyc": "^15.1.0", 26 | "parcel-bundler": "^1.12.5", 27 | "ts-node": "^9.1.1", 28 | "typescript": "^4.2.3" 29 | }, 30 | "dependencies": { 31 | "base64-js": "^1.5.1", 32 | "md5": "^2.3.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/base-encryptor.spec.ts: -------------------------------------------------------------------------------- 1 | import type { Encryptor as EncryptorClass } from "./encryptor"; 2 | import { expect } from "chai"; 3 | import { exec } from "child_process"; 4 | 5 | export function testEncryptor(_Encryptor: any) { 6 | const Encryptor = _Encryptor as typeof EncryptorClass; 7 | 8 | describe(Encryptor.name, () => { 9 | describe("getSecretDescriptor", () => { 10 | const encryptor = new Encryptor("e761daf732c272ee0db9bd71f49c66a0"); 11 | 12 | [ 13 | ["abcde", "ab56"], 14 | ["dasda", "8f40"], 15 | ["e761daf732c272ee0db9bd71f49c66a0", "122e"], 16 | ].forEach(([input, output]) => { 17 | it(`getSecretDescriptor("${input}") == "${output}"`, () => { 18 | expect(encryptor.getSecretDescriptor(input)).to.equal(output); 19 | }); 20 | }); 21 | }); 22 | 23 | it("generateInitialisationVector", () => { 24 | expect( 25 | new Encryptor( 26 | "e761daf732c272ee0db9bd71f49c66a0" 27 | ).generateInitialisationVector() 28 | ).to.have.length(16); 29 | }); 30 | 31 | describe("when given an invalid secret", () => { 32 | it("throws", () => { 33 | expect(() => new Encryptor("too-short")).to.throw( 34 | "`encryptionSecret` needs to be 32 characters, but was 9 characters." 35 | ); 36 | 37 | expect( 38 | () => new Encryptor("e761daf732c272ee0db9bd71f49c66a0", ["too-short"]) 39 | ).to.throw( 40 | "decryptionSecrets needs to be 32 characters, but was 9 characters." 41 | ); 42 | }); 43 | }); 44 | 45 | describe("encryption flow", () => { 46 | it("when encrypting on one and decrypting on the other", async () => { 47 | const secret = "e761daf732c272ee0db9bd71f49c66a0"; 48 | 49 | const encryptor = new Encryptor(secret); 50 | 51 | let callsToIv = 0; 52 | encryptor.generateInitialisationVector = () => { 53 | callsToIv++; 54 | return Buffer.from( 55 | Uint8Array.from([ 56 | 74, 57 | 239, 58 | 191, 59 | 189, 60 | 239, 61 | 191, 62 | 189, 63 | 220, 64 | 153, 65 | 65, 66 | 83, 67 | 101, 68 | 45, 69 | 44, 70 | 33, 71 | 110, 72 | 239, 73 | 191, 74 | 189, 75 | 239, 76 | 191, 77 | 189, 78 | 239, 79 | 191, 80 | 189, 81 | 239, 82 | 191, 83 | 189, 84 | ]) 85 | ); 86 | }; 87 | 88 | const input = "abcde"; 89 | 90 | const ciphered_message = await encryptor.encrypt(input); 91 | 92 | expect(ciphered_message).to.eq( 93 | "122e:Su+/ve+/vdyZQVNlLSwhbu+/ve+/ve+/ve+/vQ==:0MjARaU=:4BkD28lgqYaxBHQL4SWvZw==" 94 | ); 95 | 96 | const deciphered_text = await encryptor.decrypt(ciphered_message); 97 | expect(deciphered_text).to.equal(input); 98 | 99 | expect(callsToIv).to.eq(1); 100 | }); 101 | 102 | describe("with one secret", () => { 103 | it("works", async () => { 104 | const secret = "e761daf732c272ee0db9bd71f49c66a0"; 105 | const input = "abcde"; 106 | 107 | const encryptor = new Encryptor(secret); 108 | 109 | const ciphered_message = await encryptor.encrypt(input); 110 | 111 | const deciphered_text = await encryptor.decrypt(ciphered_message); 112 | 113 | expect(deciphered_text).to.equal(input); 114 | }); 115 | }); 116 | 117 | describe("with very small input", () => { 118 | it("works", async () => { 119 | const secret = "e761daf732c272ee0db9bd71f49c66a0"; 120 | const input = JSON.stringify(null); 121 | 122 | const encryptor = new Encryptor(secret); 123 | 124 | const ciphered_message = await encryptor.encrypt(input); 125 | 126 | const deciphered_text = await encryptor.decrypt(ciphered_message); 127 | 128 | expect(JSON.parse(deciphered_text)).to.be.null; 129 | }); 130 | }); 131 | 132 | describe("with missing auth tag (old version)", () => { 133 | it("works", async () => { 134 | const secret = "e761daf732c272ee0db9bd71f49c66a0"; 135 | const input = "hello world"; 136 | 137 | const encryptor = new Encryptor(secret); 138 | 139 | const ciphered_message = await encryptor.encrypt(input); 140 | 141 | const withoutAuthTag = ciphered_message.slice( 142 | 0, 143 | ciphered_message.lastIndexOf(":") 144 | ); 145 | 146 | if (Encryptor.name === "BrowserEncryptor") { 147 | try { 148 | await encryptor.decrypt(withoutAuthTag); 149 | expect("this").to.equal("should not be"); 150 | } catch (error) { 151 | expect(error.message).to.be.equal("Could not decrypt: Auth tag missing."); 152 | } 153 | } else { 154 | const deciphered_text = await encryptor.decrypt(withoutAuthTag); 155 | expect(deciphered_text).to.equal("hello world"); 156 | } 157 | }); 158 | }); 159 | 160 | describe("with secret rotation", () => { 161 | it("works", async () => { 162 | const oldSecret = "e761daf732c272ee0db9bd71f49c66a0"; 163 | const input = "abcde"; 164 | 165 | const oldEncryptor = new Encryptor(oldSecret); 166 | 167 | const cipher_text = await oldEncryptor.encrypt(input); 168 | 169 | const newSecret = "ee0db9bd71f49c66a0e761daf732c272"; 170 | 171 | const newEncryptor = new Encryptor(newSecret, [oldSecret]); 172 | 173 | const deciphered_text = await newEncryptor.decrypt(cipher_text); 174 | 175 | expect(deciphered_text).to.equal(input); 176 | }); 177 | 178 | describe("and used secret not existing", () => { 179 | it("throws an error", async () => { 180 | const oldSecret = "e761daf732c272ee0db9bd71f49c66a0"; 181 | const input = "abcde"; 182 | 183 | const oldEncryptor = new Encryptor(oldSecret); 184 | 185 | const cipher_text = await oldEncryptor.encrypt(input); 186 | 187 | const newSecret = "ee0db9bd71f49c66a0e761daf732c272"; 188 | 189 | const newEncryptor = new Encryptor(newSecret); 190 | 191 | try { 192 | await newEncryptor.decrypt(cipher_text); 193 | } catch (error) { 194 | expect(error).to.be.instanceOf(Error); 195 | expect(error.message).to.equal( 196 | "Could not decrypt: No matching secret." 197 | ); 198 | } 199 | }); 200 | }); 201 | }); 202 | }); 203 | }); 204 | } 205 | -------------------------------------------------------------------------------- /src/base-encryptor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | The `BaseEncrypter` deals with the secure-e2ee-specific stuff. 3 | The concrete encryption is offloaded to concrete versions, 4 | which need to use aes-256-gcm, a symmetric encryption algorithm. 5 | 6 | It uses a so-called "initialisation vector": 7 | a random, non-predictable value 8 | that makes encryption unpredictable 9 | (kind of like "salts" in hash functions). 10 | 11 | initialisation_vector = generate_random_string() 12 | 13 | encrypted = encrypt( 14 | input, 15 | secret, 16 | initialisation_vector 17 | ) 18 | 19 | send_over_the_wire(encrypted, initialisation_vector) 20 | 21 | decrypted = decrypt( 22 | cipher_text, 23 | secret, 24 | initialisation_vector 25 | ) 26 | 27 | decrypted === input // 🎉 28 | 29 | That would already be enough to simply encrypt our messages, 30 | but there's still one problem: 31 | What happens when the secret needs to be changed, e.g. because it has been leaked? 32 | If there was only one secret, all previously encrypted messages 33 | would become unreadable. 34 | 35 | To prevent this, the Encrypter has so-called decryption-only-secrets. 36 | If a secret needs to be cycled out, you can add it to the decryption-only-secrets, 37 | to allow decryption of previously-encrypted messages. 38 | 39 | To make this work, we also send a small secret 40 | descriptor over the networks, that indicates 41 | which secret should be used for decryption. 42 | */ 43 | 44 | import b64 from "base64-js"; 45 | 46 | interface EncryptedMessage { 47 | secretDescriptor: string; 48 | cipher: Uint8Array; 49 | initialisationVector: Uint8Array; 50 | authTag?: Uint8Array; 51 | } 52 | 53 | function isValidSecret(string: String): boolean { 54 | return string.length === 32; 55 | } 56 | 57 | function packMessage(message: EncryptedMessage) { 58 | const arr = [ 59 | message.secretDescriptor, 60 | b64.fromByteArray(message.initialisationVector), 61 | b64.fromByteArray(message.cipher), 62 | ]; 63 | 64 | if (message.authTag) { 65 | arr.push(b64.fromByteArray(message.authTag)); 66 | } 67 | 68 | return arr.join(":"); 69 | } 70 | 71 | function unpackMessage(message: string): EncryptedMessage { 72 | const [ 73 | secretDescriptor, 74 | initialisationVector, 75 | cipher, 76 | authTag, 77 | ] = message.split(":"); 78 | return { 79 | secretDescriptor, 80 | initialisationVector: b64.toByteArray(initialisationVector), 81 | cipher: b64.toByteArray(cipher), 82 | authTag: authTag ? b64.toByteArray(authTag) : undefined, 83 | }; 84 | } 85 | 86 | export abstract class BaseEncryptor { 87 | private readonly decryptionSecretsByDescriptor: Record = {}; 88 | 89 | constructor( 90 | private readonly encryptionSecret: string, 91 | decryptionSecrets: string[] = [encryptionSecret] 92 | ) { 93 | if (!isValidSecret(encryptionSecret)) { 94 | throw new Error( 95 | `\`encryptionSecret\` needs to be 32 characters, but was ${encryptionSecret.length} characters.` 96 | ); 97 | } 98 | 99 | for (const s of decryptionSecrets) { 100 | if (!isValidSecret(s)) { 101 | throw new Error( 102 | `decryptionSecrets needs to be 32 characters, but was ${s.length} characters.` 103 | ); 104 | } 105 | 106 | const id = this.getSecretDescriptor(s); 107 | this.decryptionSecretsByDescriptor[id] = s; 108 | } 109 | } 110 | 111 | protected abstract md5(input: string): string; 112 | 113 | public getSecretDescriptor(secret: string): string { 114 | return this.md5(secret).slice(0, 4); 115 | } 116 | 117 | abstract generateInitialisationVector(): Uint8Array; 118 | 119 | public async encrypt(input: string): Promise { 120 | const secretDescriptor = this.getSecretDescriptor(this.encryptionSecret); 121 | const initialisationVector = this.generateInitialisationVector(); 122 | 123 | const [cipher, authTag] = await this._encrypt( 124 | input, 125 | initialisationVector, 126 | this.encryptionSecret 127 | ); 128 | 129 | return packMessage({ 130 | cipher, 131 | authTag, 132 | initialisationVector, 133 | secretDescriptor, 134 | }); 135 | } 136 | 137 | protected abstract _encrypt( 138 | input: string, 139 | iv: Uint8Array, 140 | key: string 141 | ): Promise<[cipher: Uint8Array, authTag: Uint8Array]>; 142 | 143 | public async decrypt(string: string): Promise { 144 | const { 145 | cipher, 146 | initialisationVector, 147 | secretDescriptor, 148 | authTag, 149 | } = unpackMessage(string); 150 | 151 | const key = this.decryptionSecretsByDescriptor[secretDescriptor]; 152 | if (!key) { 153 | throw new Error("Could not decrypt: No matching secret."); 154 | } 155 | 156 | return await this._decrypt(cipher, authTag, initialisationVector, key); 157 | } 158 | 159 | protected abstract _decrypt( 160 | cipher: Uint8Array, 161 | authTag: Uint8Array | undefined, 162 | iv: Uint8Array, 163 | key: string 164 | ): Promise; 165 | } 166 | -------------------------------------------------------------------------------- /src/browser-encryptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { testEncryptor } from "./base-encryptor.spec"; 2 | import { BrowserEncryptor } from "./browser-encryptor"; 3 | 4 | testEncryptor(BrowserEncryptor); 5 | -------------------------------------------------------------------------------- /src/browser-encryptor.ts: -------------------------------------------------------------------------------- 1 | import { BaseEncryptor } from "./base-encryptor"; 2 | import md5 from "md5"; 3 | 4 | const textEncoder = new TextEncoder(); 5 | const textDecoder = new TextDecoder(); 6 | 7 | function concatUint8Array(a: Uint8Array, b: Uint8Array) { 8 | const result = new Uint8Array(a.byteLength + b.byteLength); 9 | result.set(new Uint8Array(a), 0); 10 | result.set(new Uint8Array(b), a.byteLength); 11 | return result; 12 | } 13 | 14 | async function getKey(key: string) { 15 | return await window.crypto.subtle.importKey( 16 | "raw", 17 | textEncoder.encode(key), 18 | { 19 | name: "AES-GCM", 20 | }, 21 | false, 22 | ["encrypt", "decrypt"] 23 | ); 24 | } 25 | 26 | export class BrowserEncryptor extends BaseEncryptor { 27 | protected md5(input: string): string { 28 | return md5(input); 29 | } 30 | 31 | generateInitialisationVector(): Uint8Array { 32 | return window.crypto.getRandomValues(new Uint8Array(16)); 33 | } 34 | 35 | protected async _encrypt( 36 | input: string, 37 | iv: Uint8Array, 38 | key: string 39 | ): Promise<[Uint8Array, Uint8Array]> { 40 | const result = await window.crypto.subtle.encrypt( 41 | { 42 | name: "AES-GCM", 43 | iv, 44 | tagLength: 128, 45 | }, 46 | await getKey(key), 47 | textEncoder.encode(input) 48 | ); 49 | 50 | const cipher = result.slice(0, result.byteLength - 16); 51 | const authTag = result.slice(result.byteLength - 16); 52 | 53 | return [new Uint8Array(cipher), new Uint8Array(authTag)]; 54 | } 55 | protected async _decrypt( 56 | cipher: Uint8Array, 57 | authTag: Uint8Array | undefined, 58 | iv: Uint8Array, 59 | key: string 60 | ): Promise { 61 | if (!authTag) { 62 | throw new Error("Could not decrypt: Auth tag missing."); 63 | } 64 | 65 | const result = await window.crypto.subtle.decrypt( 66 | { 67 | name: "AES-GCM", 68 | iv, 69 | tagLength: 128, 70 | }, 71 | await getKey(key), 72 | concatUint8Array(cipher, authTag) 73 | ); 74 | 75 | return textDecoder.decode(result); 76 | } 77 | } 78 | 79 | export default BrowserEncryptor; 80 | -------------------------------------------------------------------------------- /src/encryptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { testEncryptor } from "./base-encryptor.spec"; 2 | import { Encryptor } from "./encryptor"; 3 | 4 | testEncryptor(Encryptor); 5 | -------------------------------------------------------------------------------- /src/encryptor.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import { BaseEncryptor } from "./base-encryptor"; 3 | 4 | const algo = "aes-256-gcm"; 5 | 6 | export class Encryptor extends BaseEncryptor { 7 | protected md5(input: string): string { 8 | const hash = crypto.createHash("md5"); 9 | hash.update(input); 10 | return hash.digest("hex"); 11 | } 12 | 13 | generateInitialisationVector(): Buffer { 14 | return crypto.randomBytes(16); 15 | } 16 | 17 | protected async _encrypt( 18 | input: string, 19 | iv: Uint8Array, 20 | key: string 21 | ): Promise<[cipher: Uint8Array, authTag: Uint8Array]> { 22 | const cipher = crypto.createCipheriv(algo, key, iv, { 23 | authTagLength: 16, 24 | }); 25 | 26 | const encryptedInput = Buffer.concat([ 27 | cipher.update(input, "utf8"), 28 | cipher.final(), 29 | ]); 30 | 31 | return [encryptedInput, cipher.getAuthTag()]; 32 | } 33 | 34 | protected async _decrypt( 35 | cipher: Uint8Array, 36 | authTag: Uint8Array | undefined, 37 | iv: Uint8Array, 38 | key: string 39 | ): Promise { 40 | const decipher = crypto.createDecipheriv(algo, key, iv, { 41 | authTagLength: 16, 42 | }); 43 | 44 | if (authTag) { 45 | decipher.setAuthTag(authTag); 46 | } 47 | 48 | return decipher.update( 49 | cipher, 50 | "hex", 51 | "utf8" 52 | ); 53 | } 54 | } 55 | 56 | export default Encryptor; 57 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": ["ES2015", "DOM"], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | } 69 | } 70 | --------------------------------------------------------------------------------