'use strict' const EventEmitter = require('events') // default expiry 3 mins const DEFAULT_EXPIRY = 3 * 60 * 1000 const create = ({ keys, ssm, keyPrefix = '', expiryMs = DEFAULT_EXPIRY }) => { let isRefreshing = false if (!keys || !Array.isArray(keys) || keys.length === 0) { throw new Error('Provide a non-empty array of config keys') } if (expiryMs <= 0) { throw new Error('Specify an expiry (ms) greater than 0') } let cache = { expiration: new Date(0), items: {} } let eventEmitter = new EventEmitter() let validate = (keys, params) => { let missing = keys.filter(k => params[k] === undefined) if (missing.length > 0) { throw new Error(`Missing SSM Parameter Store keys: ${missing}`) } } let fetchParameters = async () => { if (process.env.NODE_ENV === 'production') { let req = { Names: keys.map(k => `${keyPrefix}${k}`), WithDecryption: true } let { Parameters } = await ssm.getParameters(req).promise() let params = {} for (let p of Parameters) { let name = p.Name.replace(keyPrefix, '') params[name] = p.Value } return params } else { let params = {} for (let key of keys) { params[key] = process.env[key] } return params } } let refresh = async () => { if (isRefreshing) { return } isRefreshing = true console.log(`Refreshing SSM Parameter Store keys: ${keys}`) try { let params = await fetchParameters() validate(keys, params) console.log(`Successfully refreshed SSM Parameter Store keys: ${keys}`) let now = new Date() cache.expiration = new Date(now.getTime() + expiryMs) cache.items = params eventEmitter.emit('refresh') } finally { isRefreshing = false } } let getValue = (key) => { let now = new Date() if (now >= cache.expiration) { refresh() } return cache.items[key] } let config = { refresh: refresh, onRefresh: listener => eventEmitter.addListener('refresh', listener), onRefreshError: listener => eventEmitter.addListener('refreshError', listener) } let defaultKeys = Object.keys(config) for (let key of keys) { key = key.replace(keyPrefix, '') Object.defineProperty(config, key, { get() { return getValue(key) }, enumerable: true, configurable: false }) } // catch usage of undefined keys if (process.env.NODE_ENV !== 'production') { return new Proxy(config, { get(target, key) { if (keys.includes(key) === false && defaultKeys.includes(key) === false) { throw new Error(`Key '${key}' is not defined in config keys`) } return target[key] } }) } return config } module.exports = { create }