updating-secrets 1.0.3 → 1.1.0

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.
@@ -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
  }
@@ -52,4 +52,6 @@ export declare class SecretsJsonFileAdapter<const Secrets extends SecretDefiniti
52
52
  jsonFilePath: string, options?: PartialWithUndefined<SecretsJsonFileAdapterOptions<Secrets>>);
53
53
  /** Loads secrets from the given JSON file path. */
54
54
  loadSecrets(): Promise<any>;
55
+ /** Load an individual secret from the JSON file. */
56
+ loadSingleSecret(secretKey: string): Promise<any>;
55
57
  }
@@ -51,4 +51,9 @@ export class SecretsJsonFileAdapter extends BaseSecretsAdapter {
51
51
  const fileContents = String(await this.options.fsOverride.promises.readFile(this.jsonFilePath));
52
52
  return parseWithJson5(fileContents);
53
53
  }
54
+ /** Load an individual secret from the JSON file. */
55
+ async loadSingleSecret(secretKey) {
56
+ const fileContents = parseWithJson5(String(await this.options.fsOverride.promises.readFile(this.jsonFilePath)));
57
+ return fileContents[secretKey];
58
+ }
54
59
  }
@@ -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
  }
@@ -43,7 +43,7 @@ export declare const rotatableSecretShape: Shape<{
43
43
  /** The latest up-to-date version of the secret's value. */
44
44
  current: string;
45
45
  /** The optional legacy value for the secret. Use for graceful secret rotation. */
46
- legacy: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
46
+ legacy: Shape<import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>>;
47
47
  }>;
48
48
  /**
49
49
  * Type expansion for {@link rotatableSecretShape}.
@@ -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: import("object-shape-tester").Shape | undefined;
204
+ shapeDefinition: Shape | undefined;
191
205
  adapterConfig: NonNullable<Values<SecretDefinitions>["adapterConfig"]>;
192
206
  }>;
@@ -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.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Automatically update secrets on an interval with support for seamless secret rotation.",
5
5
  "keywords": [
6
6
  "secrets",
@@ -44,20 +44,20 @@
44
44
  "test:update": "npm run test update"
45
45
  },
46
46
  "dependencies": {
47
- "@augment-vir/assert": "^31.34.0",
48
- "@augment-vir/common": "^31.34.0",
49
- "date-vir": "^7.4.2",
50
- "object-shape-tester": "^6.2.1",
51
- "type-fest": "^4.41.0"
47
+ "@augment-vir/assert": "^31.48.0",
48
+ "@augment-vir/common": "^31.48.0",
49
+ "date-vir": "^8.0.0",
50
+ "object-shape-tester": "^6.9.3",
51
+ "type-fest": "^5.2.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@augment-vir/test": "^31.34.0",
55
- "@types/node": "^24.3.1",
54
+ "@augment-vir/test": "^31.48.0",
55
+ "@types/node": "^24.10.0",
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.12",
60
- "typescript": "^5.9.2"
59
+ "typedoc": "^0.28.14",
60
+ "typescript": "^5.9.3"
61
61
  },
62
62
  "engines": {
63
63
  "node": ">=22"