updating-secrets 1.0.4 → 1.1.1
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.
- package/dist/adapters/base.adapter.d.ts +2 -0
- package/dist/adapters/base.adapter.js +5 -0
- package/dist/adapters/secrets-json-file.adapter.d.ts +8 -4
- package/dist/adapters/secrets-json-file.adapter.js +33 -3
- package/dist/adapters/static-secrets.adapter.d.ts +2 -0
- package/dist/adapters/static-secrets.adapter.js +4 -0
- package/dist/updating-secrets.d.ts +16 -2
- package/dist/updating-secrets.js +43 -3
- package/package.json +9 -9
|
@@ -28,4 +28,6 @@ export declare class BaseSecretsAdapter {
|
|
|
28
28
|
* any resources passed to it in its constructor.
|
|
29
29
|
*/
|
|
30
30
|
destroy(): void;
|
|
31
|
+
/** Load an individual secret from the adapter. No shape checking is performed here. */
|
|
32
|
+
loadSingleSecret(secretId: string): MaybePromise<unknown>;
|
|
31
33
|
}
|
|
@@ -26,4 +26,9 @@ export class BaseSecretsAdapter {
|
|
|
26
26
|
* any resources passed to it in its constructor.
|
|
27
27
|
*/
|
|
28
28
|
destroy() { }
|
|
29
|
+
/** Load an individual secret from the adapter. No shape checking is performed here. */
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
31
|
+
loadSingleSecret(secretId) {
|
|
32
|
+
throw new Error('Do not try to load a secret from the base secrets adapter.');
|
|
33
|
+
}
|
|
29
34
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type MaybePromise, type PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
-
import { type SecretDefinitions, type SecretValues } from '../secrets-definition/define-secrets.js';
|
|
2
|
+
import { type ProcessedSecretDefinitions, type SecretDefinitions, type SecretValues } from '../secrets-definition/define-secrets.js';
|
|
3
3
|
import { BaseSecretsAdapter } from './base.adapter.js';
|
|
4
4
|
/**
|
|
5
5
|
* Options for {@link SecretsJsonFileAdapter}.
|
|
@@ -28,8 +28,10 @@ export type SecretsJsonFileAdapterOptions<Secrets extends SecretDefinitions = an
|
|
|
28
28
|
existsSync: (filePath: string) => boolean;
|
|
29
29
|
};
|
|
30
30
|
/**
|
|
31
|
-
* Optional function that will automatically generate and save
|
|
32
|
-
* missing.
|
|
31
|
+
* Optional function that will automatically generate and save secrets if the JSON file is
|
|
32
|
+
* missing or if any secrets are missing. Only missing secrets will be added from the generated
|
|
33
|
+
* values; existing secrets are preserved. This is particularly useful for dev or testing
|
|
34
|
+
* environments where new secrets may be added over time.
|
|
33
35
|
*/
|
|
34
36
|
generateValues: (() => MaybePromise<SecretValues<Secrets>>) | undefined;
|
|
35
37
|
};
|
|
@@ -51,5 +53,7 @@ export declare class SecretsJsonFileAdapter<const Secrets extends SecretDefiniti
|
|
|
51
53
|
/** Path to the JSON */
|
|
52
54
|
jsonFilePath: string, options?: PartialWithUndefined<SecretsJsonFileAdapterOptions<Secrets>>);
|
|
53
55
|
/** Loads secrets from the given JSON file path. */
|
|
54
|
-
loadSecrets(): Promise<any>;
|
|
56
|
+
loadSecrets(secrets: Readonly<ProcessedSecretDefinitions>): Promise<any>;
|
|
57
|
+
/** Load an individual secret from the JSON file. */
|
|
58
|
+
loadSingleSecret(secretKey: string): Promise<any>;
|
|
55
59
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { check } from '@augment-vir/assert';
|
|
1
2
|
import { mergeDefinedProperties, parseWithJson5, } from '@augment-vir/common';
|
|
2
3
|
import { existsSync as existsSyncImport } from 'node:fs';
|
|
3
4
|
import { mkdir as mkDirImport, readFile as readFileImport, writeFile as writeFileImport, } from 'node:fs/promises';
|
|
4
5
|
import { dirname } from 'node:path';
|
|
6
|
+
import { checkValidShape } from 'object-shape-tester';
|
|
5
7
|
import { BaseSecretsAdapter } from './base.adapter.js';
|
|
6
8
|
const defaultSecretsJsonFileAdapterOptions = {
|
|
7
9
|
fsOverride: {
|
|
@@ -35,20 +37,48 @@ export class SecretsJsonFileAdapter extends BaseSecretsAdapter {
|
|
|
35
37
|
this.options = mergeDefinedProperties(defaultSecretsJsonFileAdapterOptions, options);
|
|
36
38
|
}
|
|
37
39
|
/** Loads secrets from the given JSON file path. */
|
|
38
|
-
async loadSecrets() {
|
|
39
|
-
|
|
40
|
+
async loadSecrets(secrets) {
|
|
41
|
+
const fileExists = this.options.fsOverride.existsSync(this.jsonFilePath);
|
|
42
|
+
if (!fileExists) {
|
|
40
43
|
if (this.options.generateValues) {
|
|
41
44
|
const newSecrets = await this.options.generateValues();
|
|
42
45
|
await this.options.fsOverride.promises.mkdir(dirname(this.jsonFilePath), {
|
|
43
46
|
recursive: true,
|
|
44
47
|
});
|
|
45
48
|
await this.options.fsOverride.promises.writeFile(this.jsonFilePath, JSON.stringify(newSecrets));
|
|
49
|
+
return newSecrets;
|
|
46
50
|
}
|
|
47
51
|
else {
|
|
48
52
|
throw new Error(`Missing secrets JSON file at '${this.jsonFilePath}'`);
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
const fileContents = String(await this.options.fsOverride.promises.readFile(this.jsonFilePath));
|
|
52
|
-
|
|
56
|
+
const existingSecrets = parseWithJson5(fileContents);
|
|
57
|
+
if (this.options.generateValues) {
|
|
58
|
+
const invalidSecretKeys = Object.keys(secrets).filter((key) => {
|
|
59
|
+
if (!(key in existingSecrets)) {
|
|
60
|
+
/** Secret is missing. */
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
const secretValue = existingSecrets[key];
|
|
64
|
+
const shapeDefinition = secrets[key]?.shapeDefinition;
|
|
65
|
+
return shapeDefinition
|
|
66
|
+
? !checkValidShape(secretValue, shapeDefinition)
|
|
67
|
+
: !check.isString(secretValue);
|
|
68
|
+
});
|
|
69
|
+
if (invalidSecretKeys.length > 0) {
|
|
70
|
+
const generatedSecrets = await this.options.generateValues();
|
|
71
|
+
invalidSecretKeys.forEach((invalidSecretKey) => {
|
|
72
|
+
existingSecrets[invalidSecretKey] = generatedSecrets[invalidSecretKey];
|
|
73
|
+
});
|
|
74
|
+
await this.options.fsOverride.promises.writeFile(this.jsonFilePath, JSON.stringify(existingSecrets));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return existingSecrets;
|
|
78
|
+
}
|
|
79
|
+
/** Load an individual secret from the JSON file. */
|
|
80
|
+
async loadSingleSecret(secretKey) {
|
|
81
|
+
const fileContents = parseWithJson5(String(await this.options.fsOverride.promises.readFile(this.jsonFilePath)));
|
|
82
|
+
return fileContents[secretKey];
|
|
53
83
|
}
|
|
54
84
|
}
|
|
@@ -20,4 +20,6 @@ export declare class StaticSecretsAdapter extends BaseSecretsAdapter {
|
|
|
20
20
|
staticSecrets: Record<string, JsonCompatibleValue>);
|
|
21
21
|
/** Directly returns the static secrets given. */
|
|
22
22
|
loadSecrets(): Record<string, JsonCompatibleValue>;
|
|
23
|
+
/** Load an individual secret from the static secrets given. */
|
|
24
|
+
loadSingleSecret(secretKey: string): JsonCompatibleValue;
|
|
23
25
|
}
|
|
@@ -23,4 +23,8 @@ export class StaticSecretsAdapter extends BaseSecretsAdapter {
|
|
|
23
23
|
loadSecrets() {
|
|
24
24
|
return this.staticSecrets;
|
|
25
25
|
}
|
|
26
|
+
/** Load an individual secret from the static secrets given. */
|
|
27
|
+
loadSingleSecret(secretKey) {
|
|
28
|
+
return this.staticSecrets[secretKey];
|
|
29
|
+
}
|
|
26
30
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type PartialWithUndefined, type RequiredAndNotNull, type Values } from '@augment-vir/common';
|
|
2
|
-
import { type AnyDuration } from 'date-vir';
|
|
2
|
+
import { type AnyDuration, type FullDate } from 'date-vir';
|
|
3
|
+
import { type Shape } from 'object-shape-tester';
|
|
3
4
|
import { type BaseSecretsAdapter } from './adapters/base.adapter.js';
|
|
4
5
|
import { type ProcessedSecretDefinitions, type RotatableSecretValue, type SecretDefinitions, type SecretValues } from './secrets-definition/define-secrets.js';
|
|
5
6
|
/**
|
|
@@ -95,6 +96,12 @@ export declare class UpdatingSecrets<const Secrets extends Readonly<SecretDefini
|
|
|
95
96
|
*/
|
|
96
97
|
protected loadingSecretsPromise: Promise<SecretValues<Secrets>> | undefined;
|
|
97
98
|
protected consecutiveFailureCount: number;
|
|
99
|
+
protected dynamicCache: {
|
|
100
|
+
[SecretName in string]: {
|
|
101
|
+
value: any;
|
|
102
|
+
cachedAt: FullDate;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
98
105
|
constructor(secrets: Readonly<Secrets>,
|
|
99
106
|
/**
|
|
100
107
|
* A list of adapters to load secrets from. Order here matters: all values loaded from the
|
|
@@ -173,6 +180,13 @@ export declare class UpdatingSecrets<const Secrets extends Readonly<SecretDefini
|
|
|
173
180
|
actualRotatableSecretValue: RotatableSecretValue): boolean;
|
|
174
181
|
/** Get the latest secret values. */
|
|
175
182
|
get get(): SecretValues<Secrets>;
|
|
183
|
+
/**
|
|
184
|
+
* Load a single secret dynamically, without a secret definition. The given `secretKey` varies
|
|
185
|
+
* based on the adapters in use.
|
|
186
|
+
*/
|
|
187
|
+
loadDynamicSecret<const S extends Shape>(secretKey: string, shapeRequirement: S): Promise<S['runtimeType']>;
|
|
188
|
+
/** Try to load a single secret from any of the provided adapters. */
|
|
189
|
+
protected loadSecretFromAdapters<const S extends Shape>(secretKey: string, shapeRequirement: S): Promise<S['runtimeType']>;
|
|
176
190
|
}
|
|
177
191
|
/**
|
|
178
192
|
* Processes the given {@link SecretDefinitions} so they can be easily consumed by adapters. This is
|
|
@@ -187,6 +201,6 @@ export declare function processSecrets<const Secrets extends SecretDefinitions>(
|
|
|
187
201
|
description: string;
|
|
188
202
|
whereToFind: string;
|
|
189
203
|
};
|
|
190
|
-
shapeDefinition:
|
|
204
|
+
shapeDefinition: Shape | undefined;
|
|
191
205
|
adapterConfig: NonNullable<Values<SecretDefinitions>["adapterConfig"]>;
|
|
192
206
|
}>;
|
package/dist/updating-secrets.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { assert } from '@augment-vir/assert';
|
|
2
|
-
import { combineErrors, DeferredPromise, ensureError, extractErrorMessage, getObjectTypedEntries, log, makeWritable, mapObject, mapObjectValues, mergeDefinedProperties, } from '@augment-vir/common';
|
|
3
|
-
import { convertDuration } from 'date-vir';
|
|
4
|
-
import { assertValidShape, defineShape } from 'object-shape-tester';
|
|
2
|
+
import { combineErrors, DeferredPromise, ensureError, ensureErrorAndPrependMessage, extractErrorMessage, getObjectTypedEntries, log, makeWritable, mapObject, mapObjectValues, mergeDefinedProperties, } from '@augment-vir/common';
|
|
3
|
+
import { calculateRelativeDate, convertDuration, getNowInUtcTimezone, isDateAfter, } from 'date-vir';
|
|
4
|
+
import { assertValidShape, checkValidShape, defineShape } from 'object-shape-tester';
|
|
5
5
|
import { SecretLoadError } from './secret-load.error.js';
|
|
6
6
|
const defaultOptions = {
|
|
7
7
|
lazyFailure: false,
|
|
@@ -59,6 +59,7 @@ export class UpdatingSecrets {
|
|
|
59
59
|
*/
|
|
60
60
|
loadingSecretsPromise;
|
|
61
61
|
consecutiveFailureCount = 0;
|
|
62
|
+
dynamicCache = {};
|
|
62
63
|
constructor(secrets,
|
|
63
64
|
/**
|
|
64
65
|
* A list of adapters to load secrets from. Order here matters: all values loaded from the
|
|
@@ -292,6 +293,45 @@ export class UpdatingSecrets {
|
|
|
292
293
|
}
|
|
293
294
|
return this.currentSecrets;
|
|
294
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Load a single secret dynamically, without a secret definition. The given `secretKey` varies
|
|
298
|
+
* based on the adapters in use.
|
|
299
|
+
*/
|
|
300
|
+
async loadDynamicSecret(secretKey, shapeRequirement) {
|
|
301
|
+
const cached = this.dynamicCache[secretKey];
|
|
302
|
+
if (cached &&
|
|
303
|
+
checkValidShape(cached.value, shapeRequirement) &&
|
|
304
|
+
!isDateAfter({
|
|
305
|
+
fullDate: getNowInUtcTimezone(),
|
|
306
|
+
relativeTo: calculateRelativeDate(cached.cachedAt, this.options.updateInterval),
|
|
307
|
+
})) {
|
|
308
|
+
return cached.value;
|
|
309
|
+
}
|
|
310
|
+
const newValue = await this.loadSecretFromAdapters(secretKey, shapeRequirement);
|
|
311
|
+
this.dynamicCache[secretKey] = {
|
|
312
|
+
value: newValue,
|
|
313
|
+
cachedAt: getNowInUtcTimezone(),
|
|
314
|
+
};
|
|
315
|
+
return newValue;
|
|
316
|
+
}
|
|
317
|
+
/** Try to load a single secret from any of the provided adapters. */
|
|
318
|
+
async loadSecretFromAdapters(secretKey, shapeRequirement) {
|
|
319
|
+
const errors = [new Error(`Secret '${secretKey}' not found in any adapters.`)];
|
|
320
|
+
for (const adapter of this.adapters) {
|
|
321
|
+
try {
|
|
322
|
+
const value = await adapter.loadSingleSecret(secretKey);
|
|
323
|
+
if (!value) {
|
|
324
|
+
throw new Error('Secret is empty');
|
|
325
|
+
}
|
|
326
|
+
assertValidShape(value, shapeRequirement);
|
|
327
|
+
return value;
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
errors.push(ensureErrorAndPrependMessage(error, `Failed to load secret '${secretKey}' from adapter '${adapter.adapterName}'`));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
throw combineErrors(errors);
|
|
334
|
+
}
|
|
295
335
|
}
|
|
296
336
|
/**
|
|
297
337
|
* Processes the given {@link SecretDefinitions} so they can be easily consumed by adapters. This is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "updating-secrets",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Automatically update secrets on an interval with support for seamless secret rotation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"secrets",
|
|
@@ -44,19 +44,19 @@
|
|
|
44
44
|
"test:update": "npm run test update"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@augment-vir/assert": "^31.
|
|
48
|
-
"@augment-vir/common": "^31.
|
|
49
|
-
"date-vir": "^8.
|
|
50
|
-
"object-shape-tester": "^6.
|
|
51
|
-
"type-fest": "^5.1
|
|
47
|
+
"@augment-vir/assert": "^31.57.5",
|
|
48
|
+
"@augment-vir/common": "^31.57.5",
|
|
49
|
+
"date-vir": "^8.1.0",
|
|
50
|
+
"object-shape-tester": "^6.11.0",
|
|
51
|
+
"type-fest": "^5.3.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@augment-vir/test": "^31.
|
|
55
|
-
"@types/node": "^
|
|
54
|
+
"@augment-vir/test": "^31.57.5",
|
|
55
|
+
"@types/node": "^25.0.3",
|
|
56
56
|
"c8": "^10.1.3",
|
|
57
57
|
"istanbul-smart-text-reporter": "^1.1.5",
|
|
58
58
|
"markdown-code-example-inserter": "^3.0.3",
|
|
59
|
-
"typedoc": "^0.28.
|
|
59
|
+
"typedoc": "^0.28.15",
|
|
60
60
|
"typescript": "^5.9.3"
|
|
61
61
|
},
|
|
62
62
|
"engines": {
|