@maroonedsoftware/appconfig 1.5.1 → 1.7.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.
package/README.md CHANGED
@@ -6,9 +6,10 @@ A flexible, type-safe configuration management library with support for multiple
6
6
 
7
7
  - **Type-safe access** - Full TypeScript support with generics
8
8
  - **Multiple sources** - Load configuration from JSON files, YAML files, `.env` files, and more
9
- - **Value transformation** - Resolve environment variables and GCP secrets in configuration values
9
+ - **Value transformation** - Resolve environment variables, GCP secrets, and AWS secrets in configuration values
10
10
  - **Deep merging** - Combine configurations from multiple sources with predictable override behavior
11
11
  - **Flat key grouping** - Collapse `KEY__sub=val` dotenv entries into nested objects automatically
12
+ - **Live reload** - Rebuild config on demand (e.g. when a secret rotates) and push the latest values into singletons via an `IOptions`-style accessor trio
12
13
  - **Extensible** - Create custom sources and providers for your specific needs
13
14
 
14
15
  ## Installation
@@ -127,6 +128,39 @@ const config = await new AppConfigBuilder()
127
128
 
128
129
  > **Note:** The GCP secrets provider requires valid GCP credentials. It uses Application Default Credentials (ADC), so ensure you have authenticated via `gcloud auth application-default login` or have set up a service account.
129
130
 
131
+ ### AWS Secrets Manager Integration
132
+
133
+ Use `${aws:SECRET_ID}` syntax to resolve secrets from AWS Secrets Manager. The secret id may be a secret name or a full ARN:
134
+
135
+ **config.json:**
136
+
137
+ ```json
138
+ {
139
+ "database": {
140
+ "password": "${aws:DB_PASSWORD}",
141
+ "connectionString": "${aws:DATABASE_CONNECTION_STRING}"
142
+ },
143
+ "api": {
144
+ "key": "${aws:API_SECRET_KEY}"
145
+ }
146
+ }
147
+ ```
148
+
149
+ **app.ts:**
150
+
151
+ ```typescript
152
+ import { AppConfigBuilder, AppConfigSourceJson, AppConfigProviderAwsSecrets } from '@maroonedsoftware/appconfig';
153
+
154
+ const config = await new AppConfigBuilder()
155
+ .addSource(new AppConfigSourceJson('./config.json'))
156
+ .addProvider(new AppConfigProviderAwsSecrets('us-east-1'))
157
+ .build();
158
+
159
+ // Secrets are fetched from AWS Secrets Manager (latest version)
160
+ ```
161
+
162
+ > **Note:** The AWS secrets provider requires valid AWS credentials. Credentials and the region are resolved from the standard AWS provider chain (environment variables such as `AWS_ACCESS_KEY_ID`/`AWS_REGION`, shared config/credentials files, or instance/task IAM roles). The `region` argument is optional and overrides the chain when provided.
163
+
130
164
  ## API
131
165
 
132
166
  ### AppConfig
@@ -322,6 +356,120 @@ The provider:
322
356
  - Requires valid GCP credentials (uses Application Default Credentials)
323
357
  - Is decorated with `@Injectable()` for dependency injection support
324
358
 
359
+ #### AppConfigProviderAwsSecrets
360
+
361
+ Resolves AWS Secrets Manager references in configuration values.
362
+
363
+ ```typescript
364
+ // Default pattern: ${aws:SECRET_ID}, region from the AWS provider chain
365
+ const provider = new AppConfigProviderAwsSecrets();
366
+
367
+ // Explicit region
368
+ const provider = new AppConfigProviderAwsSecrets('us-east-1');
369
+
370
+ // Custom regex pattern
371
+ const provider = new AppConfigProviderAwsSecrets('us-east-1', /\$\{secret:([^}]+)\}/g);
372
+ ```
373
+
374
+ | Parameter | Type | Description |
375
+ | --------- | ------------------ | ------------------------------------------------------------------------------------------------ |
376
+ | `region` | `string` | Optional AWS region. Resolved from the AWS provider chain when omitted |
377
+ | `prefix` | `string \| RegExp` | Optional pattern to match secret references. Default: `/\$\{aws:(.+)\}/g` (matches `${aws:ID}`) |
378
+
379
+ The provider:
380
+
381
+ - Fetches secrets from AWS Secrets Manager using the latest version (`AWSCURRENT`)
382
+ - Supports both `SecretString` and `SecretBinary` (binary is decoded as UTF-8)
383
+ - Automatically attempts to parse secret values as JSON
384
+ - Requires valid AWS credentials (uses the standard AWS provider chain)
385
+ - Is decorated with `@Injectable()` for dependency injection support
386
+
387
+ ## Live configuration
388
+
389
+ `AppConfigBuilder.build()` resolves everything once and returns an immutable `AppConfig`. When a
390
+ value can change at runtime — most often a secret rotated in GCP/AWS Secret Manager — you can rebuild
391
+ the config and have running singletons observe the new value, mirroring C#'s `IOptions` /
392
+ `IOptionsSnapshot` / `IOptionsMonitor`.
393
+
394
+ ### AppConfigStore
395
+
396
+ Holds the current `AppConfig` and rebuilds it on demand. `reload()` re-runs the full builder pipeline
397
+ (re-reading sources and re-resolving providers); it swaps in the new config **only on success**, so a
398
+ failed rebuild leaves the process on its last-good values and rethrows for the caller to log.
399
+
400
+ ```typescript
401
+ const builder = new AppConfigBuilder().addSource(jsonSource).addProvider(awsSecrets);
402
+ const store = new AppConfigStore(builder, await builder.build<RootConfig>());
403
+
404
+ // Driven by your own trigger — a timer, a GCP Pub-Sub message, an AWS EventBridge event, etc.
405
+ await store.reload().catch(err => logger.error('config reload failed', err));
406
+ ```
407
+
408
+ The store delivers the `reload()` primitive only; deciding _when_ to reload is left to the application.
409
+
410
+ ### The options trio
411
+
412
+ | Accessor | Lifetime | Value | Use it for |
413
+ | ----------------------------- | --------- | -------------------------- | --------------------------------------------------- |
414
+ | `AppConfigOptions<T>` | singleton | `.value` (boot snapshot) | values that never change at runtime |
415
+ | `AppConfigOptionsSnapshot<T>` | scoped | `.value` (stable per request) | request-stable values that may differ between requests |
416
+ | `AppConfigOptionsMonitor<T>` | singleton | `.current` + `onChange` | live values; react to changes (rebuild a pool, etc.) |
417
+
418
+ Each is an abstract `@Injectable()` class, so a configuration section mints its own DI token by
419
+ subclassing — the same one-class-per-section shape used by `SlackConfig` and `Logger`:
420
+
421
+ ```typescript
422
+ @Injectable() export abstract class SlackOptionsMonitor extends AppConfigOptionsMonitor<SlackConfig> {}
423
+ @Injectable() export abstract class SlackOptionsSnapshot extends AppConfigOptionsSnapshot<SlackConfig> {}
424
+ ```
425
+
426
+ ### Wiring with AppConfigOptionsManager
427
+
428
+ `AppConfigOptionsManager` owns the live monitors and keeps them in sync with the store. At bootstrap,
429
+ register the tiers a section needs with `registerAppConfigOptions`:
430
+
431
+ ```typescript
432
+ const store = new AppConfigStore(builder, await builder.build<RootConfig>());
433
+ const manager = new AppConfigOptionsManager(store, logger); // logger: @maroonedsoftware/logger
434
+
435
+ registerAppConfigOptions(registry, store, manager, 'slack', {
436
+ monitor: SlackOptionsMonitor,
437
+ snapshot: SlackOptionsSnapshot,
438
+ });
439
+ ```
440
+
441
+ Consumers inject the token like any other service:
442
+
443
+ ```typescript
444
+ @Injectable()
445
+ class SlackClient {
446
+ constructor(private readonly options: SlackOptionsMonitor) {}
447
+
448
+ send() {
449
+ // Read at use-time — never cache `current` in a field, or you lose live updates.
450
+ postWebhook(this.options.current.incomingWebhookUrl);
451
+ }
452
+ }
453
+
454
+ @Injectable()
455
+ class DbPool {
456
+ private pool = createPool(this.options.current);
457
+
458
+ constructor(private readonly options: DbOptionsMonitor) {
459
+ // Rebuild the pool when a credential rotates.
460
+ this.options.onChange(async cfg => {
461
+ const previous = this.pool;
462
+ this.pool = createPool(cfg);
463
+ await previous.end();
464
+ });
465
+ }
466
+ }
467
+ ```
468
+
469
+ `onChange` fires only when a reload produces a structurally different value (a re-fetched but identical
470
+ secret is ignored), the listener may be async, and a throwing/rejecting listener is reported via the
471
+ manager's logger without affecting the swap or other listeners. The returned function unsubscribes.
472
+
325
473
  ## Utilities
326
474
 
327
475
  ### nestKeys
package/dist/helpers.d.ts CHANGED
@@ -5,6 +5,23 @@
5
5
  * @returns The parsed JSON value, or the original text if parsing fails.
6
6
  */
7
7
  export declare function tryParseJson(text: string): unknown;
8
+ /**
9
+ * Recursively compares two values for structural equality.
10
+ *
11
+ * Used to suppress no-op config-reload notifications: a secret re-fetched from a
12
+ * secret manager often produces a value that is structurally identical to the
13
+ * one already held, and reloading it should not bounce live consumers (e.g. a DB
14
+ * pool listening via `onChange`).
15
+ *
16
+ * Handles primitives, arrays, and plain objects by value. Two `NaN`s compare as
17
+ * unequal (matching `===` semantics); functions and other exotic values compare
18
+ * by reference. Key order is ignored for objects.
19
+ *
20
+ * @param a - The first value to compare.
21
+ * @param b - The second value to compare.
22
+ * @returns `true` if the values are structurally equal, `false` otherwise.
23
+ */
24
+ export declare function structurallyEqual(a: unknown, b: unknown): boolean;
8
25
  /**
9
26
  * Transforms a flat key/value record into a nested object by splitting keys on a
10
27
  * separator string.
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAMlD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsBpG"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAMlD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAuBjE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsBpG"}
package/dist/index.d.ts CHANGED
@@ -8,5 +8,12 @@ export { AppConfigSourceJson } from './sources/app.config.source.json.js';
8
8
  export { AppConfigSourceYaml } from './sources/app.config.source.yaml.js';
9
9
  export { AppConfigProviderDotenv } from './providers/app.config.provider.dotenv.js';
10
10
  export { AppConfigProviderGcpSecrets } from './providers/app.config.provider.gcp.secrets.js';
11
+ export { AppConfigProviderAwsSecrets } from './providers/app.config.provider.aws.secrets.js';
11
12
  export { nestKeys } from './helpers.js';
13
+ export { AppConfigOptions, AppConfigOptionsSnapshot, AppConfigOptionsMonitor } from './options/app.config.options.js';
14
+ export { AppConfigStore } from './options/app.config.store.js';
15
+ export type { AppConfigStoreListener } from './options/app.config.store.js';
16
+ export { AppConfigOptionsManager } from './options/app.config.options.manager.js';
17
+ export { registerAppConfigOptions } from './options/app.config.options.registration.js';
18
+ export type { AppConfigOptionsTokens } from './options/app.config.options.registration.js';
12
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,YAAY,EAAE,4BAA4B,EAAE,MAAM,uCAAuC,CAAC;AAC1F,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,uBAAuB,EAAE,MAAM,2CAA2C,CAAC;AACpF,OAAO,EAAE,2BAA2B,EAAE,MAAM,gDAAgD,CAAC;AAC7F,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,YAAY,EAAE,4BAA4B,EAAE,MAAM,uCAAuC,CAAC;AAC1F,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,uBAAuB,EAAE,MAAM,2CAA2C,CAAC;AACpF,OAAO,EAAE,2BAA2B,EAAE,MAAM,gDAAgD,CAAC;AAC7F,OAAO,EAAE,2BAA2B,EAAE,MAAM,gDAAgD,CAAC;AAC7F,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AACtH,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,YAAY,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAAE,wBAAwB,EAAE,MAAM,8CAA8C,CAAC;AACxF,YAAY,EAAE,sBAAsB,EAAE,MAAM,8CAA8C,CAAC"}
package/dist/index.js CHANGED
@@ -270,6 +270,29 @@ function tryParseJson(text) {
270
270
  }
271
271
  }
272
272
  __name(tryParseJson, "tryParseJson");
273
+ function structurallyEqual(a, b) {
274
+ if (a === b) {
275
+ return true;
276
+ }
277
+ if (typeof a !== typeof b || a === null || b === null || typeof a !== "object") {
278
+ return false;
279
+ }
280
+ if (Array.isArray(a) || Array.isArray(b)) {
281
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
282
+ return false;
283
+ }
284
+ return a.every((item, index) => structurallyEqual(item, b[index]));
285
+ }
286
+ const aRecord = a;
287
+ const bRecord = b;
288
+ const aKeys = Object.keys(aRecord);
289
+ const bKeys = Object.keys(bRecord);
290
+ if (aKeys.length !== bKeys.length) {
291
+ return false;
292
+ }
293
+ return aKeys.every((key) => Object.prototype.hasOwnProperty.call(bRecord, key) && structurallyEqual(aRecord[key], bRecord[key]));
294
+ }
295
+ __name(structurallyEqual, "structurallyEqual");
273
296
  function nestKeys(record, separator) {
274
297
  const result = {};
275
298
  for (const [key, value] of Object.entries(record)) {
@@ -632,14 +655,378 @@ AppConfigProviderGcpSecrets = _ts_decorate([
632
655
  Object
633
656
  ])
634
657
  ], AppConfigProviderGcpSecrets);
658
+
659
+ // src/providers/app.config.provider.aws.secrets.ts
660
+ import { Injectable as Injectable2 } from "injectkit";
661
+ import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
662
+ import { ServerkitError as ServerkitError2 } from "@maroonedsoftware/errors";
663
+ function _ts_decorate2(decorators, target, key, desc) {
664
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
665
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
666
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
667
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
668
+ }
669
+ __name(_ts_decorate2, "_ts_decorate");
670
+ function _ts_metadata2(k, v) {
671
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
672
+ }
673
+ __name(_ts_metadata2, "_ts_metadata");
674
+ var AppConfigProviderAwsSecrets = class {
675
+ static {
676
+ __name(this, "AppConfigProviderAwsSecrets");
677
+ }
678
+ region;
679
+ secretsManagerClient;
680
+ prefix;
681
+ /**
682
+ * Creates a new AppConfigProviderAwsSecrets instance.
683
+ *
684
+ * @param region - The AWS region where secrets are stored. If omitted, the region is
685
+ * resolved from the standard AWS provider chain (e.g. `AWS_REGION`).
686
+ * @param prefix - A regex pattern or string to match secret references.
687
+ * If a string is provided, it will be converted to a RegExp. The regex must have
688
+ * at least one capture group that extracts the secret id.
689
+ * Defaults to `/\$\{aws:(.+)\}/g` which matches `${aws:SECRET_ID}` patterns.
690
+ *
691
+ * @example
692
+ * ```typescript
693
+ * // Default pattern, region from the AWS provider chain
694
+ * const provider1 = new AppConfigProviderAwsSecrets();
695
+ *
696
+ * // Explicit region
697
+ * const provider2 = new AppConfigProviderAwsSecrets('us-east-1');
698
+ *
699
+ * // Custom regex pattern
700
+ * const provider3 = new AppConfigProviderAwsSecrets('us-east-1', /\$\{secret:([^}]+)\}/g);
701
+ * ```
702
+ */
703
+ constructor(region, prefix = /\$\{aws:(.+)\}/g) {
704
+ this.region = region;
705
+ this.secretsManagerClient = new SecretsManagerClient(region ? {
706
+ region
707
+ } : {});
708
+ this.prefix = typeof prefix === "string" ? new RegExp(prefix) : prefix;
709
+ }
710
+ /**
711
+ * Checks if this provider can parse the given value.
712
+ *
713
+ * @param value - The string value to check.
714
+ * @returns `true` if the value matches the provider's regex pattern, `false` otherwise.
715
+ */
716
+ canParse(value) {
717
+ this.prefix.lastIndex = 0;
718
+ return this.prefix.test(value);
719
+ }
720
+ /**
721
+ * Fetches a secret from AWS Secrets Manager.
722
+ *
723
+ * @param secretId - The id (name or ARN) of the secret to fetch.
724
+ * @returns A promise that resolves to the secret value.
725
+ * @throws {ServerkitError} When Secrets Manager rejects the access request (e.g. missing
726
+ * secret, IAM denial, network failure). The original error is attached via `withCause`
727
+ * and the failing `secretId` / `region` are recorded in `internalDetails`. Surfacing
728
+ * the failure prevents callers booting with an empty password / API key.
729
+ * @internal
730
+ */
731
+ async getSecret(secretId) {
732
+ try {
733
+ const response = await this.secretsManagerClient.send(new GetSecretValueCommand({
734
+ SecretId: secretId
735
+ }));
736
+ if (response.SecretString !== void 0) {
737
+ return response.SecretString;
738
+ }
739
+ return response.SecretBinary ? Buffer.from(response.SecretBinary).toString("utf-8") : "";
740
+ } catch (error) {
741
+ throw new ServerkitError2(`AppConfigProviderAwsSecrets: failed to resolve secret "${secretId}" in region "${this.region ?? "default"}"`).withCause(error).withInternalDetails({
742
+ secretId,
743
+ region: this.region
744
+ });
745
+ }
746
+ }
747
+ /**
748
+ * Parses the value by replacing AWS secret references with actual secret values.
749
+ *
750
+ * The method:
751
+ * 1. Finds all matches of the regex pattern in the value
752
+ * 2. Fetches each secret from AWS Secrets Manager in parallel
753
+ * 3. Attempts to parse each result as JSON
754
+ * 4. Updates the configuration object with the final value
755
+ *
756
+ * @param value - The string value containing AWS secret references.
757
+ * @param meta - Metadata about the value's location in the configuration object.
758
+ * @returns A promise that resolves when all secrets have been fetched and the
759
+ * transformation is complete.
760
+ * @throws {ServerkitError} Propagated from {@link getSecret} when any referenced secret
761
+ * cannot be resolved. The build call site is expected to fail loud and stop boot.
762
+ *
763
+ * @example
764
+ * ```typescript
765
+ * // If AWS secret "API_KEY" contains "sk-abc123"
766
+ * // Value: "${aws:API_KEY}"
767
+ * // Result: "sk-abc123"
768
+ *
769
+ * // If AWS secret "CONFIG" contains '{"retries": 3}'
770
+ * // Value: "${aws:CONFIG}"
771
+ * // Result: { retries: 3 } (parsed as JSON object)
772
+ * ```
773
+ */
774
+ async parse(value, meta) {
775
+ const tasks = [];
776
+ const matches = value.matchAll(this.prefix);
777
+ for (const [, key] of matches) {
778
+ const task = this.getSecret(key).then((value2) => {
779
+ if (meta.arrayIndex !== void 0 && Array.isArray(meta.owner)) {
780
+ meta.owner[meta.arrayIndex] = tryParseJson(value2);
781
+ } else {
782
+ meta.owner[meta.propertyPath] = tryParseJson(value2);
783
+ }
784
+ });
785
+ tasks.push(task);
786
+ }
787
+ await Promise.all(tasks);
788
+ }
789
+ };
790
+ AppConfigProviderAwsSecrets = _ts_decorate2([
791
+ Injectable2(),
792
+ _ts_metadata2("design:type", Function),
793
+ _ts_metadata2("design:paramtypes", [
794
+ String,
795
+ Object
796
+ ])
797
+ ], AppConfigProviderAwsSecrets);
798
+
799
+ // src/options/app.config.options.ts
800
+ import { Injectable as Injectable3 } from "injectkit";
801
+ function _ts_decorate3(decorators, target, key, desc) {
802
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
803
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
804
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
805
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
806
+ }
807
+ __name(_ts_decorate3, "_ts_decorate");
808
+ var AppConfigOptions = class {
809
+ static {
810
+ __name(this, "AppConfigOptions");
811
+ }
812
+ };
813
+ AppConfigOptions = _ts_decorate3([
814
+ Injectable3()
815
+ ], AppConfigOptions);
816
+ var AppConfigOptionsSnapshot = class {
817
+ static {
818
+ __name(this, "AppConfigOptionsSnapshot");
819
+ }
820
+ };
821
+ AppConfigOptionsSnapshot = _ts_decorate3([
822
+ Injectable3()
823
+ ], AppConfigOptionsSnapshot);
824
+ var AppConfigOptionsMonitor = class {
825
+ static {
826
+ __name(this, "AppConfigOptionsMonitor");
827
+ }
828
+ };
829
+ AppConfigOptionsMonitor = _ts_decorate3([
830
+ Injectable3()
831
+ ], AppConfigOptionsMonitor);
832
+
833
+ // src/options/app.config.store.ts
834
+ var AppConfigStore = class {
835
+ static {
836
+ __name(this, "AppConfigStore");
837
+ }
838
+ builder;
839
+ config;
840
+ listeners = /* @__PURE__ */ new Set();
841
+ /**
842
+ * Creates a store seeded with an already-built config.
843
+ *
844
+ * @param builder - The builder used to rebuild the config on each `reload()`.
845
+ * Pass the same builder instance that produced `initial`.
846
+ * @param initial - The config to serve until the first successful reload.
847
+ */
848
+ constructor(builder, initial) {
849
+ this.builder = builder;
850
+ this.config = initial;
851
+ }
852
+ /**
853
+ * The config currently in effect.
854
+ */
855
+ get current() {
856
+ return this.config;
857
+ }
858
+ /**
859
+ * Rebuilds the config and, on success, swaps it in and notifies subscribers.
860
+ *
861
+ * The new config is built fully before anything is swapped, so a failed
862
+ * rebuild (e.g. a secret that is momentarily unresolvable) leaves the current
863
+ * config untouched and the error is rethrown for the caller to log. This keeps
864
+ * a running process on its last-good values rather than crashing it — unlike
865
+ * boot, where a build failure is meant to stop startup.
866
+ *
867
+ * @returns A promise that resolves once the swap and notifications complete.
868
+ * @throws Propagates any error thrown while building the new config; the
869
+ * current config is left in place.
870
+ */
871
+ async reload() {
872
+ const next = await this.builder.build();
873
+ this.config = next;
874
+ for (const listener of this.listeners) {
875
+ listener(next);
876
+ }
877
+ }
878
+ /**
879
+ * Subscribes to config swaps.
880
+ *
881
+ * @param listener - Called with the new config after each successful reload.
882
+ * @returns A function that removes the listener when called.
883
+ */
884
+ subscribe(listener) {
885
+ this.listeners.add(listener);
886
+ return () => {
887
+ this.listeners.delete(listener);
888
+ };
889
+ }
890
+ };
891
+
892
+ // src/options/app.config.options.monitor.ts
893
+ var AppConfigOptionsMonitorImpl = class extends AppConfigOptionsMonitor {
894
+ static {
895
+ __name(this, "AppConfigOptionsMonitorImpl");
896
+ }
897
+ logger;
898
+ value;
899
+ listeners = /* @__PURE__ */ new Set();
900
+ /**
901
+ * @param initial - The value to serve until the first {@link AppConfigOptionsMonitorImpl.update}.
902
+ * @param logger - Used to report listener failures.
903
+ */
904
+ constructor(initial, logger) {
905
+ super(), this.logger = logger;
906
+ this.value = initial;
907
+ }
908
+ /**
909
+ * The latest value for this section.
910
+ */
911
+ get current() {
912
+ return this.value;
913
+ }
914
+ /**
915
+ * Subscribes to value changes. See {@link AppConfigOptionsMonitor.onChange}.
916
+ *
917
+ * @param listener - Called with the new value after each change.
918
+ * @returns A function that removes the listener when called.
919
+ */
920
+ onChange(listener) {
921
+ this.listeners.add(listener);
922
+ return () => {
923
+ this.listeners.delete(listener);
924
+ };
925
+ }
926
+ /**
927
+ * Swaps in a new value and notifies listeners.
928
+ *
929
+ * A structurally-equal value is ignored so a secret re-fetched unchanged does
930
+ * not bounce live consumers. The value is swapped before listeners run, so a
931
+ * listener reading `current` sees the new value. Each listener is invoked in
932
+ * isolation: a throw or rejection is reported via the logger and does not stop
933
+ * the swap or the other listeners.
934
+ *
935
+ * @param next - The newly resolved value for this section.
936
+ */
937
+ update(next) {
938
+ if (structurallyEqual(this.value, next)) {
939
+ return;
940
+ }
941
+ this.value = next;
942
+ for (const listener of this.listeners) {
943
+ Promise.resolve().then(() => listener(next)).catch((err) => this.logger.error("AppConfigOptionsMonitor: onChange listener failed", err));
944
+ }
945
+ }
946
+ };
947
+
948
+ // src/options/app.config.options.manager.ts
949
+ var AppConfigOptionsManager = class {
950
+ static {
951
+ __name(this, "AppConfigOptionsManager");
952
+ }
953
+ store;
954
+ logger;
955
+ monitors = /* @__PURE__ */ new Map();
956
+ /**
957
+ * @param store - The reloadable config store to track.
958
+ * @param logger - Passed to each monitor for listener-error reporting.
959
+ */
960
+ constructor(store, logger) {
961
+ this.store = store;
962
+ this.logger = logger;
963
+ this.store.subscribe((config) => {
964
+ for (const [key, monitor] of this.monitors) {
965
+ monitor.update(config.getAs(key));
966
+ }
967
+ });
968
+ }
969
+ /**
970
+ * Returns the live monitor for a section, creating it on first use.
971
+ *
972
+ * @param key - The configuration section key.
973
+ * @returns A monitor whose `current` tracks the latest value for `key`.
974
+ */
975
+ monitor(key) {
976
+ let monitor = this.monitors.get(key);
977
+ if (!monitor) {
978
+ monitor = new AppConfigOptionsMonitorImpl(this.store.current.getAs(key), this.logger);
979
+ this.monitors.set(key, monitor);
980
+ }
981
+ return monitor;
982
+ }
983
+ /**
984
+ * Returns a boot-snapshot accessor for a section.
985
+ *
986
+ * The value is read from the config current at call time and never updated —
987
+ * the static (`IOptions`) tier. Call this during registration so the captured
988
+ * value is the one in effect at container-build time.
989
+ *
990
+ * @param key - The configuration section key.
991
+ * @returns An {@link AppConfigOptions} holding the section's snapshot value.
992
+ */
993
+ options(key) {
994
+ return {
995
+ value: this.store.current.getAs(key)
996
+ };
997
+ }
998
+ };
999
+
1000
+ // src/options/app.config.options.registration.ts
1001
+ function registerAppConfigOptions(registry, store, manager, key, tokens) {
1002
+ if (tokens.options) {
1003
+ registry.register(tokens.options).useInstance(manager.options(key));
1004
+ }
1005
+ if (tokens.monitor) {
1006
+ registry.register(tokens.monitor).useInstance(manager.monitor(key));
1007
+ }
1008
+ if (tokens.snapshot) {
1009
+ registry.register(tokens.snapshot).useFactory(() => ({
1010
+ value: store.current.getAs(key)
1011
+ })).asScoped();
1012
+ }
1013
+ }
1014
+ __name(registerAppConfigOptions, "registerAppConfigOptions");
635
1015
  export {
636
1016
  AppConfig,
637
1017
  AppConfigBuilder,
1018
+ AppConfigOptions,
1019
+ AppConfigOptionsManager,
1020
+ AppConfigOptionsMonitor,
1021
+ AppConfigOptionsSnapshot,
1022
+ AppConfigProviderAwsSecrets,
638
1023
  AppConfigProviderDotenv,
639
1024
  AppConfigProviderGcpSecrets,
640
1025
  AppConfigSourceDotenv,
641
1026
  AppConfigSourceJson,
642
1027
  AppConfigSourceYaml,
643
- nestKeys
1028
+ AppConfigStore,
1029
+ nestKeys,
1030
+ registerAppConfigOptions
644
1031
  };
645
1032
  //# sourceMappingURL=index.js.map