├── .editorconfig ├── .gitignore ├── .prettierignore ├── .travis.yml ├── changelog.md ├── license ├── package-lock.json ├── package.json ├── readme.md ├── src ├── encryption.spec.ts ├── encryption.ts ├── hash.spec.ts ├── hash.ts ├── index.spec.ts ├── index.ts ├── random.spec.ts ├── random.ts ├── utils.spec.ts └── utils.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 4 6 | indent_style = tab 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [*.yml] 14 | indent_size = 2 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | dist 5 | coverage 6 | .nyc_output 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 13 5 | - 12 6 | - 11 7 | - 10 8 | - 9 9 | - 8 10 | - 7 11 | - 6 12 | after_success: 13 | - npm run coveralls 14 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Crypto Extra Changelog 2 | 3 | ## v1.0 4 | 5 | - Removed manual type checking in favor of Typescript 6 | - Renamed `generateKey` to `randomKey` 7 | 8 | ## v0.4 9 | 10 | - BREAKING: Removed both Bcrypt methods (https://github.com/jsonmaur/node-crypto-extra/issues/1) 11 | - Bug fixes 12 | 13 | ## v0.3 14 | 15 | - Removed [npmjs.org/bcryptjs]() package in favor of [npmjs.org/bcrypt](), which relies on `node-gyp` for faster results. 16 | - Removed `.checksum` and `.checksumSync`. Use [this package](https://github.com/dshaw/checksum) instead. 17 | - Renamed `.bcrypt` to `.bcryptHash` to be more consistent. 18 | - Removed `.bcryptSync` and `.bcryptCompareSync` in favor of promised versions. 19 | - Added `length` option to `.generateKey`. 20 | 21 | #### Removed Deprecations 22 | 23 | - `.decrypt` that was used before encryption IV was implemented. 24 | - Old version of `.random`. 25 | - Specifying `options.length` on `.randomString`. 26 | - Old consolidated version of `.bcrypt` that was hash and compare in one. 27 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Jason Maurer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crypto-extra", 3 | "version": "1.0.1", 4 | "description": "Convenience methods for the crypto module", 5 | "author": "Jason Maurer", 6 | "license": "MIT", 7 | "homepage": "https://github.com/jsonmaur/node-crypto-extra#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/jsonmaur/node-crypto-extra.git" 11 | }, 12 | "engines": { 13 | "node": ">=6" 14 | }, 15 | "keywords": [ 16 | "crypto", 17 | "cryptography", 18 | "hash", 19 | "encrypt", 20 | "decrypt", 21 | "encryption", 22 | "decryption", 23 | "sha1", 24 | "md5", 25 | "aes256", 26 | "random", 27 | "hex", 28 | "cipher", 29 | "extra" 30 | ], 31 | "files": [ 32 | "dist/", 33 | "license", 34 | "readme.md" 35 | ], 36 | "main": "dist/index.js", 37 | "scripts": { 38 | "clean": "rm -rf dist coverage", 39 | "format": "prettier --write './**/*.{ts,json,yml,md}'", 40 | "format:check": "prettier --check './**/*.{ts,json,yml,md}'", 41 | "test": "npm run format:check && jest --coverage", 42 | "coveralls": "cat coverage/lcov.info | coveralls", 43 | "prebuild": "npm run clean", 44 | "build": "tsc", 45 | "prepare": "npm run build" 46 | }, 47 | "devDependencies": { 48 | "@types/jest": "24.0.19", 49 | "@types/node": "12.11.5", 50 | "coveralls": "3.0.7", 51 | "jest": "24.9.0", 52 | "prettier": "1.18.2", 53 | "ts-jest": "24.1.0", 54 | "typescript": "3.6.4" 55 | }, 56 | "prettier": { 57 | "semi": false, 58 | "useTabs": true, 59 | "trailingComma": "all", 60 | "arrowParens": "always", 61 | "printWidth": 100 62 | }, 63 | "jest": { 64 | "testEnvironment": "node", 65 | "transform": { 66 | "\\.ts$": "ts-jest" 67 | }, 68 | "collectCoverageFrom": [ 69 | "src/**/*.ts" 70 | ], 71 | "coverageReporters": [ 72 | "lcov", 73 | "text" 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Crypto-Extra for Node.js 2 | 3 | [![Build Status](https://travis-ci.org/jsonmaur/node-crypto-extra.svg?branch=master)](https://travis-ci.org/jsonmaur/node-crypto-extra) 4 | [![Coverage Status](https://coveralls.io/repos/github/jsonmaur/node-crypto-extra/badge.svg?branch=master)](https://coveralls.io/github/jsonmaur/node-crypto-extra?branch=master) 5 | 6 | Adds convenience methods to the native Node.js [crypto module](https://nodejs.org/api/crypto.html). It is a drop in replacement, and extends the original module functionality. 7 | 8 | - [Getting Started](#getting-started) 9 | - [API](#api) 10 | - [encrypt](#api-encrypt) 11 | - [decrypt](#api-decrypt) 12 | - [hash](#api-hash) 13 | - [randomKey](#api-random-key) 14 | - [randomString](#api-random-string) 15 | - [randomNumber](#api-random-number) 16 | - [native crypto methods](https://nodejs.org/api/crypto.html) 17 | 18 | ## Why? 19 | 20 | The native `crypto` module can be a pain to work with, and requires a lot of boilerplate to do things such as randomizing and encryption. This abstracts all of that. 21 | 22 | 23 | 24 | ## Getting Started 25 | 26 | ```bash 27 | $ npm install crypto-extra --save 28 | ``` 29 | 30 | To use in your project, simply require into your project as you would the `crypto` module. 31 | 32 | ```javascript 33 | const crypto = require("crypto-extra") 34 | 35 | crypto.randomString() 36 | //= L0e84MUt0n 37 | 38 | crypto.hash("hello") 39 | //= 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 40 | ``` 41 | 42 | 43 | 44 | ## API 45 | 46 | 47 | 48 | ### .encrypt (value, secretKey) 49 | 50 | Encrypts a value with a secret key using AES-256-CTR. 51 | 52 | - **value** - The value you want to encrypt. Everything (except objects) is converted to a string before encryption for consistency. Objects are stringified using `JSON.stringify`. 53 | 54 | > Type: `any` 55 | 56 | - **secretKey** - The key used in the encryption. If not supplied, the lib will fallback to the environment variable `ENCRYPTION_KEY`. 57 | 58 | > Type: `string` 59 | > Default: `process.env.ENCRYPTION_KEY` 60 | 61 | 62 | 63 | ### .decrypt (value, secretKey) 64 | 65 | Decrypts a value using AES-256-CTR. 66 | 67 | - **value** - The encrypted value you want to decrypt. Will automatically parse objects that were encrypted. 68 | 69 | > Type: `string` 70 | 71 | - **secretKey** - The key used in the encryption. If not supplied, the lib will fallback to the environment variable `ENCRYPTION_KEY`. 72 | 73 | > Type: `string` 74 | > Default: `process.env.ENCRYPTION_KEY` 75 | 76 | 77 | 78 | ### .hash (value, options) 79 | 80 | Hashes a string with the provided algorithm. 81 | 82 | - **value** - The value you want to hash. Any non-string value is converted to a string before hashing for consistency. 83 | 84 | > Type: `string` 85 | 86 | - **options** 87 | 88 | - **rounds** - The number of rounds to use when hashing. 89 | 90 | > Type: `integer` 91 | > Default: `1` 92 | 93 | - **salt** - A string to be appended to the value before it is hashed. 94 | 95 | > Type: `string` 96 | 97 | - **algorithm** - The hashing algorithm to use. 98 | 99 | > Type: `string` 100 | > Default: `SHA256` 101 | 102 | 103 | 104 | ### .randomKey (length) 105 | 106 | Generates a random 256-bit key that can be used as an encryption key. 107 | 108 | - **length** - The length of the key you want to generate. **Must be an even number.** 109 | 110 | > Type: `number` 111 | > Default: `32` 112 | 113 | 114 | 115 | ### .randomString (length, charset) 116 | 117 | Returns a random string of a defined length. 118 | 119 | - **length** - Length of the random string. Must be above 0. 120 | 121 | > Type: `integer` 122 | > Default: `10` 123 | 124 | - **charset** - The character set to take from. 125 | 126 | > Type: `string` 127 | > Default: `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789` 128 | 129 | 130 | 131 | ### .randomNumber (options) 132 | 133 | Returns a random string within a defined range. 134 | 135 | - **options** 136 | 137 | - **min** - Minimum number of range. Must be a positive integer. 138 | 139 | > Type: `integer` 140 | > Default: `0` 141 | 142 | - **max** - Maximum number of range. This cannot be higher than `9007199254740991` due to Javascript integer limits (http://mzl.la/1A1nVyU). If you need a number higher than this, consider using [randomString](#api-random-string) with the charset `0123456789` instead. 143 | 144 | > Type: `integer` 145 | > Default: `9007199254740991` 146 | 147 | ## License 148 | 149 | [MIT](license) © [Jason Maurer](https://maur.co) 150 | -------------------------------------------------------------------------------- /src/encryption.spec.ts: -------------------------------------------------------------------------------- 1 | import * as encryption from "./encryption" 2 | 3 | const SECRET_KEY = "asdfasdfasdfasdfasdfasdfasdfasdf" 4 | 5 | test("encrypt()", async () => { 6 | expect(encryption.decrypt(encryption.encrypt("hey", SECRET_KEY), SECRET_KEY)).toBe("hey") 7 | expect(encryption.encrypt("hey", SECRET_KEY)).toBeTruthy() 8 | expect(encryption.encrypt(100, SECRET_KEY)).toBeTruthy() 9 | expect(encryption.encrypt({ hello: "hey" }, SECRET_KEY)).toBeTruthy() 10 | expect(encryption.encrypt(true, SECRET_KEY)).toBeTruthy() 11 | expect(encryption.encrypt("hey", "hello")).toBeTruthy() 12 | expect(() => encryption.encrypt("hey", "")).toThrow(Error) 13 | expect(() => encryption.encrypt("hey")).toThrow(Error) 14 | }) 15 | 16 | test("decrypt()", async () => { 17 | const encryptedStr = 18 | "ae8f31$9aef670513191e77b51d3948bc0ea539$33a8b27803725fd7e8c5641548b43b545a876767219e9badc280be8e3aff8bba" 19 | const encryptedObj = 20 | "55bc46634486927655e1c9e7a514ae$f5ba32e5cc42e78f5a07bcfe0645f668$d9a20b941a6ffdb64526055d388a1411e8227afd978dcec7f7aa110085a218b8" 21 | const encryptedObjTampered = 22 | "55bc46634486927655e1c9e7a514ae$f5ba32e5cc42e78f5a07bcfe0645f668$d9a20b941a6ffdb64526055d388a1411e8227afd978dcec7f7aa110085a218b9" 23 | 24 | expect(encryption.decrypt(encryptedStr, SECRET_KEY)).toBe("hey") 25 | expect(encryption.decrypt(encryptedObj, SECRET_KEY)).toEqual({ hello: "hey" }) 26 | expect(() => encryption.decrypt("hi$")).toThrow(Error) 27 | expect(() => encryption.decrypt("hey$", "short-secret-key")).toThrow(Error) 28 | expect(() => encryption.decrypt(encryptedObjTampered, SECRET_KEY)).toThrow(Error) 29 | }) 30 | -------------------------------------------------------------------------------- /src/encryption.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto" 2 | import { stringify } from "./utils" 3 | 4 | const ALGORITHM = "aes-256-ctr" 5 | const HMAC_ALGORITHM = "sha256" 6 | 7 | /** 8 | * Gets the encryption key from the environment, 9 | * and hash with SHA256 (ensures length). Falls back 10 | * to the environment variable if no key is specified. 11 | */ 12 | function getEncryptionKey(key?: string): Buffer { 13 | const encryptionKey = key || process.env.ENCRYPTION_KEY 14 | 15 | if (!encryptionKey) { 16 | throw new Error("No encryption key was found") 17 | } 18 | 19 | const cryptoKey = crypto 20 | .createHash("sha256") 21 | .update(encryptionKey) 22 | .digest() 23 | 24 | return cryptoKey 25 | } 26 | 27 | /** 28 | * Ensures the encrypted payload has not been tampered with. 29 | */ 30 | function constantTimeCompare(val1: string, val2: string): boolean { 31 | if (val1.length !== val2.length) { 32 | return false 33 | } 34 | 35 | let sentinel = 0 36 | for (let i = 0, len = val1.length; i < len; i++) { 37 | sentinel |= val1.charCodeAt(i) ^ val2.charCodeAt(i) 38 | } 39 | 40 | return sentinel === 0 41 | } 42 | 43 | /** 44 | * Encrypts a value using ciphers. 45 | */ 46 | export function encrypt(value: any, key?: string): string { 47 | const iv = Buffer.from(crypto.randomBytes(16)) 48 | const encryptionKey = Buffer.from(getEncryptionKey(key)) 49 | const cipher = crypto.createCipheriv(ALGORITHM, encryptionKey, iv) 50 | 51 | cipher.setEncoding("hex") 52 | cipher.write(stringify(value)) 53 | cipher.end() 54 | 55 | const cipherText = cipher.read() 56 | const hmac = crypto.createHmac(HMAC_ALGORITHM, encryptionKey) 57 | 58 | hmac.update(cipherText) 59 | hmac.update(iv.toString("hex")) 60 | 61 | return `${cipherText}$${iv.toString("hex")}$${hmac.digest("hex")}` 62 | } 63 | 64 | /** 65 | * Decrypts a value using ciphers. 66 | */ 67 | export function decrypt(value: string, key?: string): any { 68 | const cipher = value.split("$") 69 | const iv = Buffer.from(cipher[1], "hex") 70 | const encryptionKey = Buffer.from(getEncryptionKey(key)) 71 | const hmac = crypto.createHmac(HMAC_ALGORITHM, encryptionKey) 72 | 73 | hmac.update(cipher[0]) 74 | hmac.update(iv.toString("hex")) 75 | 76 | if (!constantTimeCompare(hmac.digest("hex"), cipher[2])) { 77 | throw new Error("Encrypted payload has been tampered with") 78 | } 79 | 80 | const decipher = crypto.createDecipheriv(ALGORITHM, encryptionKey, iv) 81 | const decryptedText = decipher.update(cipher[0], "hex") 82 | const final = `${decryptedText}${decipher.final()}` 83 | 84 | try { 85 | return JSON.parse(final) 86 | } catch (err) { 87 | return final 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/hash.spec.ts: -------------------------------------------------------------------------------- 1 | import * as hash from "./hash" 2 | 3 | test("hash()", async () => { 4 | expect(hash.hash("testing")).toBe( 5 | "cf80cd8aed482d5d1527d7dc72fceff84e6326592848447d2dc0b0e87dfc9a90", 6 | ) 7 | expect(hash.hash("testing", { algorithm: "md5" })).toBe("ae2b1fca515949e5d54fb22b8ed95575") 8 | expect(hash.hash("testing", { salt: "yo-this-is-a-salt" })).toBe( 9 | "bd3df90288d99583d1c93f00ec00d92c97c3aff241b1beffb819dbd15f68d9f6", 10 | ) 11 | expect(hash.hash({ test: "hi" })).toBe( 12 | "aa6d68a0aab2f834d2bc353d734907e0e0d562e1beaf99432bd665c96f5b4d7b", 13 | ) 14 | expect(hash.hash("testing", { rounds: 100 })).toBe( 15 | "2c66de00e03581e03866d7b62a31a7d5776419f498f479a877270294c2600321", 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /src/hash.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto" 2 | import { stringify } from "./utils" 3 | 4 | type HashOptions = { 5 | algorithm?: string 6 | rounds?: number 7 | salt?: string 8 | } 9 | 10 | /** 11 | * Gets the hash a value. 12 | */ 13 | export function hash(value: any, options: HashOptions = {}): string { 14 | const parsedValue = stringify(value) 15 | const algorithm = options.algorithm || "sha256" 16 | const rounds = options.rounds || 1 17 | 18 | let hash = `${parsedValue}${options.salt || ""}` 19 | for (let i = 0; i < rounds; i++) { 20 | hash = crypto 21 | .createHash(algorithm) 22 | .update(hash) 23 | .digest("hex") 24 | } 25 | 26 | return hash 27 | } 28 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import crypto from "./" 2 | 3 | test("exports", async () => { 4 | expect(crypto.createHmac).toBeTruthy() 5 | expect(crypto.randomString).toBeTruthy() 6 | }) 7 | 8 | test("deprecated", async () => { 9 | expect(crypto.generateKey()).toBeTruthy() 10 | }) 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto" 2 | import * as hash from "./hash" 3 | import * as encryption from "./encryption" 4 | import * as random from "./random" 5 | 6 | function deprecationNotice(msg: string) { 7 | console.log(`crypto-extra: ${msg}`) 8 | } 9 | 10 | export = Object.assign(crypto, { 11 | hash: hash.hash, 12 | encrypt: encryption.encrypt, 13 | decrypt: encryption.decrypt, 14 | randomString: random.randomString, 15 | randomNumber: random.randomNumber, 16 | randomKey: random.randomKey, 17 | 18 | /* deprecated methods */ 19 | generateKey: (...args: any) => { 20 | deprecationNotice("`generateKey` has been renamed to `randomKey`") 21 | return random.randomKey(...args) 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /src/random.spec.ts: -------------------------------------------------------------------------------- 1 | import * as random from "./random" 2 | 3 | test("randomString()", async () => { 4 | expect(random.randomString()).toBeTruthy() 5 | expect(random.randomString()).toHaveLength(10) 6 | expect(random.randomString(20)).toHaveLength(20) 7 | expect(() => random.randomString(0)).toThrow(Error) 8 | expect(() => random.randomString(-5)).toThrow(Error) 9 | }) 10 | 11 | test("randomNumber()", async () => { 12 | expect(random.randomNumber()).toBeGreaterThan(0) 13 | expect(() => random.randomNumber({ max: Number.MAX_SAFE_INTEGER + 1 })).toThrow(Error) 14 | expect(() => random.randomNumber({ min: -1 })).toThrow(Error) 15 | expect(() => random.randomNumber({ max: -1 })).toThrow(Error) 16 | 17 | const min = 100 18 | const max = 150 19 | for (let i = 0; i < 10000; i++) { 20 | const num = random.randomNumber({ min, max }) 21 | expect(num >= min).toBe(true) 22 | expect(num <= max).toBe(true) 23 | } 24 | }) 25 | 26 | test("randomKey()", async () => { 27 | expect(random.randomKey()).toHaveLength(64) 28 | expect(random.randomKey(10)).toHaveLength(10) 29 | expect(random.randomKey(152)).toHaveLength(152) 30 | expect(() => random.randomKey(65)).toThrow(TypeError) 31 | expect(() => random.randomKey(0)).toThrow(TypeError) 32 | }) 33 | -------------------------------------------------------------------------------- /src/random.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from "crypto" 2 | 3 | type RandomNumberOptions = { 4 | min?: number 5 | max?: number 6 | } 7 | 8 | /** 9 | * Creates a random string. 10 | */ 11 | export function randomString(size?: number, charset?: string): string { 12 | if (size !== undefined && size <= 0) { 13 | throw new Error("Random size must be a number above 0!") 14 | } 15 | 16 | const bytes = randomBytes(size || 10) 17 | const chars = charset || "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" 18 | 19 | let value = "" 20 | for (let i = 0, len = bytes.length; i < len; i++) { 21 | value += chars[bytes.readUInt8(i) % chars.length] 22 | } 23 | 24 | return value 25 | } 26 | 27 | /** 28 | * Generates a random number. 29 | */ 30 | export function randomNumber(options: RandomNumberOptions = {}): number { 31 | const integerLimit = Number.MAX_SAFE_INTEGER 32 | 33 | options.min = options.min || 0 34 | options.max = options.max || integerLimit 35 | 36 | if ( 37 | options.min < 0 || 38 | options.min > integerLimit - 1 || 39 | options.max < 1 || 40 | options.max > integerLimit 41 | ) { 42 | throw new Error(`Limits must be between 0 and ${integerLimit}`) 43 | } 44 | 45 | const hex = randomBytes(16).toString("hex") 46 | const integer = parseInt(hex, 16) 47 | const random = integer / 0xffffffffffffffffffffffffffffffff 48 | 49 | return Math.floor(random * (options.max - options.min + 1) + options.min) 50 | } 51 | 52 | /** 53 | * Generates a secure 256-bit key. 54 | */ 55 | export function randomKey(length: number = 64): string { 56 | if (length < 2 || length % 2 !== 0) { 57 | throw new TypeError("Length must be an even number above 0") 58 | } 59 | 60 | return randomBytes(length / 2).toString("hex") 61 | } 62 | -------------------------------------------------------------------------------- /src/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import * as utils from "./utils" 2 | 3 | test("stringify()", async () => { 4 | expect(utils.stringify(1)).toBe("1") 5 | expect(utils.stringify({ hey: "hi" })).toBe('{"hey":"hi"}') 6 | }) 7 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Turns a value into a string. Uses JSON.stringify 3 | * if the value is an object. 4 | */ 5 | export function stringify(value: any): string { 6 | return typeof value === "object" ? JSON.stringify(value) : String(value) 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts"], 3 | "exclude": ["node_modules", "**/*.spec.ts"], 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "module": "commonjs", 7 | "declaration": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "noEmitOnError": true, 11 | "listEmittedFiles": true, 12 | "removeComments": true, 13 | "noUnusedLocals": true, 14 | "forceConsistentCasingInFileNames": true 15 | } 16 | } 17 | --------------------------------------------------------------------------------