@63klabs/cache-data 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,492 @@
1
+
2
+ const http = require('http'); // For AWS Parameters and Secrets Lambda Extension - accesses localhost via http
3
+
4
+ const DebugAndLog = require('./DebugAndLog.class');
5
+ const Timer = require('./Timer.class');
6
+
7
+ /* ****************************************************************************
8
+ * Systems Manager Parameter Store and Secrets Manager Lambda Extension
9
+ * ----------------------------------------------------------------------------
10
+ *
11
+ * AWS Parameters and Secrets Lambda Extension
12
+ * To use, the Systems Manager Parameter Store and Secrets Manager Lambda
13
+ * Extension layer must be installed for your Lambda function.
14
+ *
15
+ * Added in Cache-Data v1.0.38
16
+ *
17
+ * https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html
18
+ * https://aws.amazon.com/blogs/compute/using-the-aws-parameter-and-secrets-lambda-extension-to-cache-parameters-and-secrets/
19
+ *************************************************************************** */
20
+
21
+ /* ****************************************************************************
22
+
23
+
24
+ */
25
+
26
+ class CachedParameterSecrets {
27
+ /**
28
+ * @typedef {Array<CachedParameterSecret>}
29
+ */
30
+ static #cachedParameterSecrets = [];
31
+
32
+ /**
33
+ * @param {CachedParameterSecret} The CachedParameterSecret object to add
34
+ */
35
+ static add (cachedParameterSecretObject) {
36
+ CachedParameterSecrets.#cachedParameterSecrets.push(cachedParameterSecretObject);
37
+ }
38
+
39
+ /**
40
+ * @param {string} The Parameter name or Secret Id to locate
41
+ * @returns {CachedParameterSecret}
42
+ */
43
+ static get (name) {
44
+ return CachedParameterSecrets.#cachedParameterSecrets.find(cachedParameterSecretObject => cachedParameterSecretObject.getName() === name);
45
+ }
46
+
47
+ /**
48
+ *
49
+ * @returns {Array<object>} An array of objects representing the CachedParameterSecret.toObject()
50
+ * (see CachedParameterSecret.toObject() for details
51
+ */
52
+ static toArray() {
53
+ // return an array of cachedParameterSecret.toObject()
54
+ const objects = [];
55
+ CachedParameterSecrets.#cachedParameterSecrets.forEach(cachedParameterSecretObject => {
56
+ objects.push(cachedParameterSecretObject.toObject());
57
+ });
58
+ return objects;
59
+ };
60
+
61
+ static toObject() {
62
+ // return an object of cachedParameterSecret.toObject()
63
+ return {objects: CachedParameterSecrets.toArray()};
64
+ }
65
+
66
+ /**
67
+ *
68
+ * @returns {string} JSON string of CachedParameterSecrets.toObject()
69
+ */
70
+ static toJSON() {
71
+ return JSON.stringify(CachedParameterSecrets.toObject());
72
+ };
73
+
74
+ /**
75
+ *
76
+ * @returns {Array<string>}
77
+ */
78
+ static getNameTags() {
79
+ const nameTags = [];
80
+ CachedParameterSecrets.#cachedParameterSecrets.forEach(cachedParameterSecretObject => {
81
+ nameTags.push(cachedParameterSecretObject.getNameTag());
82
+ });
83
+ return nameTags;
84
+ };
85
+
86
+ /**
87
+ *
88
+ * @returns {Array<string>}
89
+ */
90
+ static getNames() {
91
+ const names = [];
92
+ CachedParameterSecrets.#cachedParameterSecrets.forEach(cachedParameterSecretObject => {
93
+ names.push(cachedParameterSecretObject.getName());
94
+ });
95
+ return names;
96
+ };
97
+
98
+ /**
99
+ * Call .prime() of all CachedParameterSecrets and return all the promises
100
+ * @returns {Promise<Array>}
101
+ */
102
+ static async prime() {
103
+
104
+ return new Promise(async (resolve, reject) => {
105
+
106
+ try {
107
+ const promises = [];
108
+ CachedParameterSecrets.#cachedParameterSecrets.forEach(cachedParameterSecretObject => {
109
+ promises.push(cachedParameterSecretObject.prime());
110
+ });
111
+
112
+ await Promise.all(promises);
113
+
114
+ resolve(true);
115
+
116
+ } catch (error) {
117
+ DebugAndLog.error(`CachedParameterSecrets.prime(): ${error.message}`, error.stack);
118
+ reject(false);
119
+ }
120
+
121
+ });
122
+
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Parent class to extend CachedSSMParameter and CachedSecret classes.
128
+ * Accesses data through Systems Manager Parameter Store and Secrets Manager Lambda Extension
129
+ * Since the Lambda Extension runs a localhost via http, it handles it's own http request. Also,
130
+ * since the lambda extension needs time to boot during a cold start, it is not available during
131
+ * the regular init phase outside of the handler. Therefore, we can pass the Object to be used as
132
+ * the secret and then perform an async .get() or .getValue() at runtime. If we need to use a
133
+ * synchronous function, then we must perform a .prime() and make sure it is complete before calling
134
+ * the sync function.
135
+ *
136
+ * @example
137
+ * const write(data) {
138
+ * const edata = encrypt(data, myParam.sync_getValue()); // some encrypt function
139
+ * return edata;
140
+ * }
141
+ *
142
+ * async main () => {
143
+ * const myParam = new CachedSSMParameter('myParam');
144
+ * myParam.prime(); // gets things started in the background
145
+ *
146
+ * // ... some code that may take a few ms to run ...
147
+ *
148
+ * // We are going to call a sync function that MUST
149
+ * // have the myParam value resolved so we
150
+ * // make sure we are good to go before proceeding
151
+ * await myParam.prime();
152
+ * console.log(write(data));
153
+ * }
154
+ */
155
+ class CachedParameterSecret {
156
+ static hostname = "localhost";
157
+ static port = "2773";
158
+
159
+ name = "";
160
+ value = null;
161
+ cache = {
162
+ lastRefresh: 0,
163
+ status: -1,
164
+ refreshAfter: (5 * 60),
165
+ promise: null
166
+ }
167
+
168
+ /**
169
+ *
170
+ * @param {string} name Path and Parameter Name from Parameter Store '/my/path/parametername' or id of secret from Secret Manager
171
+ * @param {{refreshAfter: number}} options Increase the number of seconds the value should be kept before refreshing. Note that this is in addition to the Lambda Layer cache of 5 minutes. Can shave off a few ms of time if you increase. However, if value or parameter values change frequently you should leave as default.
172
+ */
173
+ constructor(name, options = {}) {
174
+ this.name = name;
175
+ this.cache.refreshAfter = parseInt((options?.refreshAfter ?? this.cache.refreshAfter), 10);
176
+ CachedParameterSecrets.add(this);
177
+ DebugAndLog.debug(`CachedParameterSecret: ${this.getNameTag()}`);
178
+ };
179
+
180
+ /**
181
+ *
182
+ * @returns {string} The Parameter path and name or Id of Secret
183
+ */
184
+ getName() {
185
+ return this.name;
186
+ };
187
+
188
+ /**
189
+ * Returns a string with the name and instance of the class object
190
+ * @returns {string} 'name [instanceof]'
191
+ */
192
+ getNameTag() {
193
+ return `${this.name} [${this.instanceof()}]`
194
+ }
195
+
196
+ /**
197
+ * Returns an object representation of the data (except the value)
198
+ * @returns {{name: string, instanceof: string, cache: {lastRefresh: number, status: number, refreshAfter: number, promise: Promise} isRefreshing: boolean, needsRefresh: boolean, isValid: boolean}}
199
+ */
200
+ toObject() {
201
+ return {
202
+ name: this.name,
203
+ instanceof: this.instanceof(),
204
+ cache: this.cache,
205
+ isRefreshing: this.isRefreshing(),
206
+ needsRefresh: this.needsRefresh(),
207
+ isValid: this.isValid()
208
+ };
209
+ };
210
+
211
+ /**
212
+ * JSON.stringify() looks for .toJSON methods and uses it when stringify is called.
213
+ * This allows us to set an object property such as key with the Class object and
214
+ * then, when the object is put to use through stringify, the object will be
215
+ * converted to a string.
216
+ * @returns {string} value of secret or parameter
217
+ */
218
+ toJSON() {
219
+ return this.sync_getValue();
220
+ };
221
+
222
+ /**
223
+ * This allows us to set an object property such as key with the Class object and
224
+ * then, when the object is put to use through stringify, the object will be
225
+ * converted to a string.
226
+ * @returns {string} value of secret or parameter
227
+ */
228
+ toString() {
229
+ return this.sync_getValue();
230
+ };
231
+
232
+ /**
233
+ *
234
+ * @returns {string} The constructor name
235
+ */
236
+ instanceof() {
237
+ return this.constructor.name; //((this instanceof CachedSSMParameter) ? 'CachedSSMParameter' : 'CachedSecret');
238
+ };
239
+
240
+ /**
241
+ *
242
+ * @returns {boolean} true if the value is currently being refreshed
243
+ */
244
+ isRefreshing() {
245
+ return ( this.cache.status === 0 );
246
+ };
247
+
248
+ /**
249
+ *
250
+ * @returns {boolean} true if the value has expired and needs a refresh
251
+ */
252
+ needsRefresh() {
253
+ return ( !this.isRefreshing() && ( (Date.now() - (this.cache.refreshAfter * 1000)) > this.cache.lastRefresh || this.cache.status < 0 ));
254
+ };
255
+
256
+ /**
257
+ *
258
+ * @returns {boolean} true if the value is valid (has been set and is not null)
259
+ */
260
+ isValid() {
261
+ return (
262
+ this.value !== null
263
+ && typeof this.value === "object"
264
+ );
265
+ }
266
+
267
+ /**
268
+ * Pre-emptively run a request for the secret or parameter. Call this function without
269
+ * await to start the request in the background.
270
+ *
271
+ * Call any of the async functions (.get(), .getValue()) with await just prior to needing the value.
272
+ * You must await prior to going into a syncronous function and using sync_getValue()
273
+ *
274
+ * @example
275
+ * myParam.prime();
276
+ * //... some code that may take a few ms to run ...
277
+ * await myParam.get();
278
+ *
279
+ * @returns {Promise<number>} -1 if error, 1 if success
280
+ */
281
+ async prime() {
282
+ DebugAndLog.debug(`CachedParameterSecret.prime() called for ${this.getNameTag()}`);
283
+ const p = (this.needsRefresh()) ? this.refresh() : this.cache.promise;
284
+ DebugAndLog.debug(`CachedParameterSecret.prime() status of ${this.getNameTag()}`, this.toObject());
285
+ return p;
286
+ };
287
+
288
+ /**
289
+ * Forces a refresh of the value from AWS Parameter Store or Secrets Manager whether or not it has expired
290
+ * @returns {Promise<number>} -1 if error, 1 if success
291
+ */
292
+ async refresh() {
293
+
294
+ // check to see if this.cache.status is an unresolved promise
295
+ DebugAndLog.debug(`CachedParameterSecret.refresh() Checking refresh status of ${this.name}`);
296
+ if ( !this.isRefreshing() ) {
297
+ this.cache.status = 0;
298
+ this.cache.promise = new Promise(async (resolve, reject) => {
299
+ try {
300
+ const timer = new Timer('CachedParameterSecret_refresh', true);
301
+ let resp = null;
302
+ let tryCount = 0;
303
+ while (resp === null && tryCount < 3) {
304
+ tryCount++;
305
+ if (tryCount > 1) { DebugAndLog.warn(`CachedParameterSecret.refresh() failed. Retry #${tryCount} for ${this.name}`)}
306
+ resp = await this._requestSecretsFromLambdaExtension();
307
+ if (resp !== null) {
308
+ this.value = resp;
309
+ this.cache.lastRefresh = Date.now();
310
+ this.cache.status = 1;
311
+ } else {
312
+ this.cache.status = -1;
313
+ }
314
+ }
315
+ timer.stop();
316
+ resolve(this.cache.status);
317
+ } catch (error) {
318
+ DebugAndLog.error(`Error Calling Secrets Manager and SSM Parameter Store Lambda Extension during refresh: ${error.message}`, error.stack);
319
+ reject(-1);
320
+ }
321
+ });
322
+ }
323
+ return this.cache.promise;
324
+ }
325
+
326
+ /**
327
+ * Gets the current value object from AWS Parameter Store or Secrets Manager.
328
+ * It contains the meta-data and properties of the value as well as the value.
329
+ * The value comes back decrypted.
330
+ * If the value has expired, it will be refreshed and the refreshed value will be returned.
331
+ * @returns {Promise<object>} Secret or Parameter Object
332
+ */
333
+ async get() {
334
+ await this.prime();
335
+ return this.value;
336
+ }
337
+
338
+ /**
339
+ * Returns just the current value string from AWS Parameter Store or Secrets Manager.
340
+ * The value comes back decrypted.
341
+ * If the value has expired, it will be refreshed and the refreshed value will be returned.
342
+ * @returns {Promise<string>} Secret or Parameter String
343
+ */
344
+ async getValue() {
345
+ await this.get();
346
+ if (this.value === null) {
347
+ return null;
348
+ } else {
349
+ return this.sync_getValue();
350
+ }
351
+ }
352
+
353
+ /**
354
+ * This can be used in sync functions after .get(), .getValue(), or .refresh() completes
355
+ * The value comes back decrypted.
356
+ * It will return the current, cached copy which may have expired.
357
+ * @returns {string} The value of the Secret or Parameter
358
+ */
359
+ sync_getValue() {
360
+ if (this.isValid()) {
361
+ DebugAndLog.debug(`CachedParameterSecret.sync_getValue() returning value for ${this.name}`, this.toObject());
362
+ return ("Parameter" in this.value) ? this.value?.Parameter?.Value : this.value?.SecretString ;
363
+ } else {
364
+ // Throw error
365
+ throw new Error("CachedParameterSecret Error: Secret is null. Must call and await async function .get(), .getValue(), or .refresh() first");
366
+ }
367
+ }
368
+
369
+ /**
370
+ *
371
+ * @returns {string} The URL path passed to localhost for the AWS Parameters and Secrets Lambda Extension
372
+ */
373
+ getPath() {
374
+ return "";
375
+ }
376
+
377
+ async _requestSecretsFromLambdaExtension() {
378
+
379
+ return new Promise(async (resolve, reject) => {
380
+
381
+ let body = "";
382
+
383
+ const options = {
384
+ hostname: CachedParameterSecret.hostname,
385
+ port: CachedParameterSecret.port,
386
+ path: this.getPath(),
387
+ headers: {
388
+ 'X-Aws-Parameters-Secrets-Token': process.env.AWS_SESSION_TOKEN
389
+ },
390
+ method: 'GET'
391
+ };
392
+
393
+ const responseBodyToObject = function (valueBody) {
394
+ try {
395
+ let value = null;
396
+
397
+ if (typeof valueBody === "string") {
398
+ value = JSON.parse(valueBody);
399
+ }
400
+
401
+ resolve(value);
402
+
403
+ } catch (error) {
404
+ DebugAndLog.error(`CachedParameterSecret http: Error Calling Secrets Manager and SSM Parameter Store Lambda Extension: Error parsing response for ${options.path} ${error.message}`, error.stack);
405
+ reject(null);
406
+ }
407
+
408
+ };
409
+
410
+ let req = http.request(options, (res) => {
411
+
412
+ DebugAndLog.debug('CachedParameterSecret http: Calling Secrets Manager and SSM Parameter Store Lambda Extension');
413
+
414
+ try {
415
+ /*
416
+ The 3 classic https.get() functions
417
+ What to do on "data", "end" and "error"
418
+ */
419
+
420
+ res.on('data', function (chunk) { body += chunk; });
421
+
422
+ res.on('end', function () {
423
+ DebugAndLog.debug(`CachedParameterSecret http: Received response for ${options.path}`);
424
+ responseBodyToObject(body);
425
+ });
426
+
427
+ res.on('error', error => {
428
+ DebugAndLog.error(`CachedParameterSecret http Error: E0 Error obtaining response for ${options.path} ${error.message}`, error.stack);
429
+ reject(null);
430
+ });
431
+
432
+ } catch (error) {
433
+ DebugAndLog.error(`CachedParameterSecret http Error: E1 Error obtaining response for ${options.path} ${error.message}`, error.stack);
434
+ reject(null);
435
+ }
436
+
437
+ });
438
+
439
+ req.on('timeout', () => {
440
+ DebugAndLog.error(`CachedParameterSecret http Error: Endpoint request timeout reached for ${options.path}`);
441
+ req.end();
442
+ reject(null);
443
+ });
444
+
445
+ req.on('error', error => {
446
+ DebugAndLog.error(`CachedParameterSecret http Error: Error during request for ${options.path} ${error.message}`, error.stack);
447
+ reject(null);
448
+ });
449
+
450
+ req.end();
451
+
452
+ });
453
+
454
+ };
455
+
456
+ }
457
+
458
+ class CachedSSMParameter extends CachedParameterSecret {
459
+ getPath() {
460
+ const uriEncodedSecret = encodeURIComponent(this.name);
461
+ return `/systemsmanager/parameters/get/?name=${uriEncodedSecret}&withDecryption=true`;
462
+ }
463
+
464
+ isValid() {
465
+ return (
466
+ super.isValid()
467
+ && "Parameter" in this.value
468
+ );
469
+ }
470
+ }
471
+
472
+ class CachedSecret extends CachedParameterSecret {
473
+
474
+ getPath() {
475
+ const uriEncodedSecret = encodeURIComponent(this.name);
476
+ return `/secretsmanager/get?secretId=${uriEncodedSecret}&withDecryption=true`;
477
+ }
478
+
479
+ isValid() {
480
+ return (
481
+ super.isValid()
482
+ && "SecretString" in this.value
483
+ );
484
+ }
485
+ };
486
+
487
+ module.exports = {
488
+ CachedParameterSecrets,
489
+ CachedParameterSecret,
490
+ CachedSSMParameter,
491
+ CachedSecret
492
+ }