├── .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 |
--------------------------------------------------------------------------------