UNPKG

4.37 kBPlain TextView Raw
1import * as fs from 'fs'
2import { _assert, _stringMapEntries, StringMap } from '@naturalcycles/js-lib'
3import { base64ToString } from '..'
4import { decryptRandomIVBuffer, decryptString } from './crypto.util'
5
6let loaded = false
7
8const secretMap: StringMap = {}
9
10/**
11 * Loads plaintext secrets from process.env, removes them, stores locally.
12 * Make sure to call this function early on server startup, so secrets are removed from process.env
13 *
14 * Does NOT delete previous secrets from secretMap.
15 */
16export function loadSecretsFromEnv(): void {
17 require('dotenv').config() // ensure .env is loaded
18
19 const secrets: StringMap = {}
20 Object.keys(process.env)
21 .filter(k => k.toUpperCase().startsWith('SECRET_'))
22 .forEach(k => {
23 secrets[k.toUpperCase()] = process.env[k]!
24 secretMap[k.toUpperCase()] = process.env[k]!
25 delete process.env[k]
26 })
27
28 loaded = true
29 console.log(
30 `${Object.keys(secrets).length} secret(s) loaded from process.env: ${Object.keys(secrets).join(
31 ', ',
32 )}`,
33 )
34}
35
36/**
37 * Removes process.env.SECRET_*
38 */
39export function removeSecretsFromEnv(): void {
40 Object.keys(process.env)
41 .filter(k => k.toUpperCase().startsWith('SECRET_'))
42 .forEach(k => delete process.env[k])
43}
44
45/**
46 * Does NOT delete previous secrets from secretMap.
47 *
48 * If SECRET_ENCRYPTION_KEY argument is passed - will decrypt the contents of the file first, before parsing it as JSON.
49 *
50 * Whole file is encrypted.
51 * For "json-values encrypted" style - use `loadSecretsFromEncryptedJsonFileValues`
52 */
53// eslint-disable-next-line @typescript-eslint/naming-convention
54export function loadSecretsFromEncryptedJsonFile(
55 filePath: string,
56 secretEncryptionKey?: string,
57): void {
58 _assert(
59 fs.existsSync(filePath),
60 `loadSecretsFromEncryptedJsonFile() cannot load from path: ${filePath}`,
61 )
62
63 let secrets: StringMap
64
65 if (secretEncryptionKey) {
66 const buf = fs.readFileSync(filePath)
67 const plain = decryptRandomIVBuffer(buf, secretEncryptionKey).toString('utf8')
68 secrets = JSON.parse(plain)
69 } else {
70 secrets = JSON.parse(fs.readFileSync(filePath, 'utf8'))
71 }
72
73 Object.entries(secrets).forEach(([k, v]) => (secretMap[k.toUpperCase()] = v))
74
75 loaded = true
76 console.log(
77 `${Object.keys(secrets).length} secret(s) loaded from ${filePath}: ${Object.keys(secrets)
78 .map(s => s.toUpperCase())
79 .join(', ')}`,
80 )
81}
82
83/**
84 * Whole file is NOT encrypted, but instead individual json values ARE encrypted..
85 * For whole-file encryption - use `loadSecretsFromEncryptedJsonFile`
86 */
87export function loadSecretsFromEncryptedJsonFileValues(
88 filePath: string,
89 secretEncryptionKey?: string,
90): void {
91 _assert(
92 fs.existsSync(filePath),
93 `loadSecretsFromEncryptedJsonFileValues() cannot load from path: ${filePath}`,
94 )
95
96 const secrets: StringMap = JSON.parse(fs.readFileSync(filePath, 'utf8'))
97
98 if (secretEncryptionKey) {
99 _stringMapEntries(secrets).forEach(([k, enc]) => {
100 secrets[k] = decryptString(enc, secretEncryptionKey)
101 })
102 }
103
104 Object.entries(secrets).forEach(([k, v]) => (secretMap[k.toUpperCase()] = v))
105
106 loaded = true
107 console.log(
108 `${Object.keys(secrets).length} secret(s) loaded from ${filePath}: ${Object.keys(secrets)
109 .map(s => s.toUpperCase())
110 .join(', ')}`,
111 )
112}
113
114/**
115 * json secrets are always base64'd
116 */
117export function secret<T = string>(k: string, json = false): T {
118 const v = secretOptional(k, json)
119 if (!v) {
120 throw new Error(`secret(${k.toUpperCase()}) not found!`)
121 }
122
123 return v as any
124}
125
126export function secretOptional<T = string>(k: string, json = false): T | undefined {
127 requireLoaded()
128 const v = secretMap[k.toUpperCase()]
129 return v && json ? JSON.parse(base64ToString(v)) : v
130}
131
132export function getSecretMap(): StringMap {
133 requireLoaded()
134 return secretMap
135}
136
137/**
138 * REPLACES secretMap with new map.
139 */
140export function setSecretMap(map: StringMap): void {
141 Object.keys(secretMap).forEach(k => delete secretMap[k])
142 Object.entries(map).forEach(([k, v]) => (secretMap[k.toUpperCase()] = v))
143 console.log(
144 `setSecretMap set ${Object.keys(secretMap).length} secret(s): ${Object.keys(map)
145 .map(s => s.toUpperCase())
146 .join(', ')}`,
147 )
148}
149
150function requireLoaded(): void {
151 if (!loaded) {
152 throw new Error(`Secrets were not loaded! Call loadSecrets() before accessing secrets.`)
153 }
154}