UNPKG

3.35 kBPlain TextView Raw
1import { StringMap } from '@naturalcycles/js-lib'
2import * as fs from 'fs'
3import { base64ToString, Debug } from '..'
4import { decryptRandomIVBuffer } from './crypto.util'
5
6let loaded = false
7
8// it's wrapped to be able to pipe console.* to Stackdriver
9const getLog = () => Debug('nc:nodejs-lib:secret')
10
11const secretMap: StringMap = {}
12
13/**
14 * Loads plaintext secrets from process.env, removes them, stores locally.
15 * Make sure to call this function early on server startup, so secrets are removed from process.env
16 *
17 * Does NOT delete previous secrets from secretMap.
18 */
19export function loadSecretsFromEnv(): void {
20 require('dotenv').config() // ensure .env is loaded
21
22 const secrets: StringMap = {}
23 Object.keys(process.env)
24 .filter(k => k.toUpperCase().startsWith('SECRET_'))
25 .forEach(k => {
26 secrets[k.toUpperCase()] = process.env[k]!
27 secretMap[k.toUpperCase()] = process.env[k]!
28 delete process.env[k]
29 })
30
31 loaded = true
32 getLog()(
33 `${Object.keys(secrets).length} secret(s) loaded from process.env: ${Object.keys(secrets).join(
34 ', ',
35 )}`,
36 )
37}
38
39/**
40 * Removes process.env.SECRET_*
41 */
42export function removeSecretsFromEnv(): void {
43 Object.keys(process.env)
44 .filter(k => k.toUpperCase().startsWith('SECRET_'))
45 .forEach(k => delete process.env[k])
46}
47
48/**
49 * Does NOT delete previous secrets from secretMap.
50 *
51 * If SECRET_ENCRYPTION_KEY argument is passed - will decrypt the contents of the file first, before parsing it as JSON.
52 */
53export function loadSecretsFromJsonFile(filePath: string, SECRET_ENCRYPTION_KEY?: string): void {
54 if (!fs.existsSync(filePath)) {
55 throw new Error(`loadSecretsFromPlainJsonFile() cannot load from path: ${filePath}`)
56 }
57
58 let secrets: StringMap
59
60 if (SECRET_ENCRYPTION_KEY) {
61 const buf = fs.readFileSync(filePath)
62 const plain = decryptRandomIVBuffer(buf, SECRET_ENCRYPTION_KEY).toString('utf8')
63 secrets = JSON.parse(plain)
64 } else {
65 secrets = JSON.parse(fs.readFileSync(filePath, 'utf8'))
66 }
67
68 Object.entries(secrets).forEach(([k, v]) => (secretMap[k.toUpperCase()] = v))
69
70 loaded = true
71 getLog()(
72 `${Object.keys(secrets).length} secret(s) loaded from ${filePath}: ${Object.keys(secrets)
73 .map(s => s.toUpperCase())
74 .join(', ')}`,
75 )
76}
77
78/**
79 * json secrets are always base64'd
80 */
81export function secret<T = string>(k: string, json = false): T {
82 const v = secretOptional(k, json)
83 if (!v) {
84 throw new Error(`secret(${k.toUpperCase()}) not found!`)
85 }
86
87 return v as any
88}
89
90export function secretOptional<T = string>(k: string, json = false): T | undefined {
91 requireLoaded()
92 const v = secretMap[k.toUpperCase()]
93 return v && json ? JSON.parse(base64ToString(v)) : v
94}
95
96export function getSecretMap(): StringMap {
97 requireLoaded()
98 return secretMap
99}
100
101/**
102 * REPLACES secretMap with new map.
103 */
104export function setSecretMap(map: StringMap): void {
105 Object.keys(secretMap).forEach(k => delete secretMap[k])
106 Object.entries(map).forEach(([k, v]) => (secretMap[k.toUpperCase()] = v))
107 getLog()(
108 `setSecretMap set ${Object.keys(secretMap).length} secret(s): ${Object.keys(map)
109 .map(s => s.toUpperCase())
110 .join(', ')}`,
111 )
112}
113
114function requireLoaded(): void {
115 if (!loaded) {
116 throw new Error(`Secrets were not loaded! Call loadSecrets() before accessing secrets.`)
117 }
118}