@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 +149 -1
- package/dist/helpers.d.ts +17 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +388 -1
- package/dist/index.js.map +1 -1
- package/dist/options/app.config.options.d.ts +81 -0
- package/dist/options/app.config.options.d.ts.map +1 -0
- package/dist/options/app.config.options.manager.d.ts +52 -0
- package/dist/options/app.config.options.manager.d.ts.map +1 -0
- package/dist/options/app.config.options.monitor.d.ts +47 -0
- package/dist/options/app.config.options.monitor.d.ts.map +1 -0
- package/dist/options/app.config.options.registration.d.ts +51 -0
- package/dist/options/app.config.options.registration.d.ts.map +1 -0
- package/dist/options/app.config.store.d.ts +72 -0
- package/dist/options/app.config.store.d.ts.map +1 -0
- package/dist/providers/app.config.provider.aws.secrets.d.ts +106 -0
- package/dist/providers/app.config.provider.aws.secrets.d.ts.map +1 -0
- package/package.json +6 -3
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/app.config.ts","../src/app.config.builder.ts","../src/object.visitor.ts","../src/helpers.ts","../src/sources/app.config.source.dotenv.ts","../src/sources/app.config.source.json.ts","../src/sources/app.config.source.yaml.ts","../src/providers/app.config.provider.dotenv.ts","../src/providers/app.config.provider.gcp.secrets.ts"],"sourcesContent":["/**\n * Configuration container that provides type-safe access to configuration values.\n *\n * @template T - The type of the configuration object. Defaults to `Record<string, unknown>`.\n *\n * @example\n * ```typescript\n * const config = new AppConfig({\n * database: { host: 'localhost', port: 5432 },\n * api: { timeout: 5000 }\n * });\n *\n * const host = config.get('database').host; // Type-safe access\n * ```\n */\nexport class AppConfig<T = Record<string, unknown>> {\n /**\n * Creates a new AppConfig instance with the provided configuration.\n *\n * @param config - The configuration object to wrap.\n */\n constructor(private readonly config: T) {}\n\n /**\n * Retrieves a configuration value by key.\n *\n * @template K - The key type, must be a key of T.\n * @param key - The configuration key to retrieve.\n * @returns The configuration value for the given key.\n *\n * @example\n * ```typescript\n * const config = new AppConfig({ port: 3000, host: 'localhost' });\n * const port = config.get('port'); // Returns 3000, typed as number\n * ```\n */\n get(key: keyof T): T[keyof T] {\n return this.config[key];\n }\n\n /**\n * Retrieves a configuration value cast to a specific type.\n *\n * Unlike `get()`, which returns `T[keyof T]`, this method lets you cast the\n * value to an arbitrary type `U`. Use this when the TypeScript type of the\n * stored value differs from what you need at the call site — for example,\n * when reading a nested object as a typed interface.\n *\n * @template U - The type to cast the value to.\n * @param key - The configuration key to retrieve.\n * @returns The configuration value cast to `U`.\n *\n * @example\n * ```typescript\n * interface DbConfig { host: string; port: number }\n *\n * const config = new AppConfig({ database: { host: 'localhost', port: 5432 } });\n * const db = config.getAs<DbConfig>('database');\n * console.log(db.host); // 'localhost'\n * ```\n */\n getAs<U>(key: keyof T): U {\n return this.config[key] as U;\n }\n\n /**\n * Retrieves a configuration value as a string.\n *\n * The value is converted to a string using `String()`. This is useful when\n * you need to ensure a value is a string regardless of its original type.\n *\n * @template K - The key type, must be a key of T.\n * @param key - The configuration key to retrieve.\n * @returns The configuration value converted to a string.\n *\n * @example\n * ```typescript\n * const config = new AppConfig({ port: 3000, enabled: true });\n * const portStr = config.getString('port'); // Returns \"3000\"\n * const enabledStr = config.getString('enabled'); // Returns \"true\"\n * ```\n */\n getString(key: keyof T): string {\n return String(this.get(key));\n }\n\n /**\n * Retrieves a configuration value as a number.\n *\n * The value is converted to a number using `Number()`. This is useful when\n * you need to ensure a value is a number regardless of its original type.\n * Note: Invalid conversions will result in `NaN`.\n *\n * @template K - The key type, must be a key of T.\n * @param key - The configuration key to retrieve.\n * @returns The configuration value converted to a number.\n *\n * @example\n * ```typescript\n * const config = new AppConfig({ port: '3000', timeout: '5000' });\n * const port = config.getNumber('port'); // Returns 3000\n * const timeout = config.getNumber('timeout'); // Returns 5000\n * ```\n */\n getNumber(key: keyof T): number {\n return Number(this.config[key]);\n }\n\n /**\n * Retrieves a configuration value as a boolean.\n *\n * The value is converted to a boolean using `Boolean()`. This is useful when\n * you need to ensure a value is a boolean regardless of its original type.\n * Note: Only falsy values (false, 0, '', null, undefined, NaN) become false.\n *\n * @template K - The key type, must be a key of T.\n * @param key - The configuration key to retrieve.\n * @returns The configuration value converted to a boolean.\n *\n * @example\n * ```typescript\n * const config = new AppConfig({ enabled: 'true', debug: 1 });\n * const enabled = config.getBoolean('enabled'); // Returns true\n * const debug = config.getBoolean('debug'); // Returns true\n * ```\n */\n getBoolean(key: keyof T): boolean {\n return Boolean(this.config[key]);\n }\n\n /**\n * Retrieves a configuration value as an object.\n *\n * The value is cast to an object type. This is useful when you know a value\n * is an object and want to access it with object methods.\n *\n * @template K - The key type, must be a key of T.\n * @param key - The configuration key to retrieve.\n * @returns The configuration value cast as an object.\n *\n * @example\n * ```typescript\n * const config = new AppConfig({\n * database: { host: 'localhost', port: 5432 }\n * });\n * const db = config.getObject('database'); // Returns { host: 'localhost', port: 5432 }\n * ```\n */\n getObject(key: keyof T): object {\n return this.config[key] as object;\n }\n}\n","import { deepmerge } from 'deepmerge-ts';\nimport { AppConfig } from './app.config.js';\nimport { AppConfigProvider } from './app.config.provider.js';\nimport { AppConfigSource } from './app.config.source.js';\nimport { objectVisitor, ObjectVisitorMeta } from './object.visitor.js';\n\n/**\n * Builder for constructing AppConfig instances from multiple sources with value transformation.\n *\n * The builder allows you to:\n * - Load configuration from multiple sources (files, environment variables, etc.)\n * - Merge configurations with later sources overriding earlier ones\n * - Transform string values using providers (e.g., resolving environment variable references)\n *\n * @example\n * ```typescript\n * const config = await new AppConfigBuilder()\n * .addSource(new AppConfigSourceJson('./config.json'))\n * .addSource(new AppConfigSourceDotenv())\n * .addProvider(new AppConfigProviderDotenv())\n * .build();\n * ```\n */\nexport class AppConfigBuilder {\n private readonly sources: AppConfigSource[] = [];\n private readonly providers: AppConfigProvider[] = [];\n\n /**\n * Adds a configuration source to the builder.\n *\n * Sources are loaded in the order they are added, and later sources override earlier ones\n * when merging configurations.\n *\n * @param source - The configuration source to add.\n * @returns The builder instance for method chaining.\n *\n * @example\n * ```typescript\n * builder\n * .addSource(new AppConfigSourceJson('./default.json'))\n * .addSource(new AppConfigSourceJson('./local.json'));\n * ```\n */\n addSource(source: AppConfigSource) {\n this.sources.push(source);\n return this;\n }\n\n /**\n * Adds a provider to transform string values during configuration building.\n *\n * Providers are applied to all string values found in the merged configuration.\n * The first provider that can parse a value will be used to transform it.\n *\n * @param provider - The provider to add.\n * @returns The builder instance for method chaining.\n *\n * @example\n * ```typescript\n * builder.addProvider(new AppConfigProviderDotenv());\n * ```\n */\n addProvider(provider: AppConfigProvider) {\n this.providers.push(provider);\n return this;\n }\n\n /**\n * Builds the AppConfig instance by loading all sources, merging them, and applying providers.\n *\n * The build process:\n * 1. Loads all sources in parallel\n * 2. Merges configurations (later sources override earlier ones)\n * 3. Traverses the merged configuration and applies providers to string values\n * 4. Returns the final AppConfig instance\n *\n * @template T - The type of the configuration object. Defaults to `Record<string, unknown>`.\n * @returns A promise that resolves to the built AppConfig instance.\n *\n * @example\n * ```typescript\n * const config = await builder.build<MyConfigType>();\n * const value = config.get('someKey');\n * ```\n */\n async build<T = Record<string, unknown>>(): Promise<AppConfig<T>> {\n const sourceTasks = await Promise.all(this.sources.map(x => x.load()));\n // `deepmerge` with zero arguments returns `undefined`, which would crash\n // every downstream consumer with an opaque \"cannot read property of\n // undefined\". A misconfigured builder should still yield a usable empty\n // config object so the error surfaces at the missing-key call site.\n const mergedConfig = (sourceTasks.length === 0 ? {} : deepmerge(...sourceTasks)) as T;\n\n const tasks: Promise<void>[] = [];\n const parse = (value: unknown, meta: ObjectVisitorMeta) => {\n if (typeof value === 'string') {\n const provider = this.providers.find(x => x.canParse(value));\n if (provider) {\n tasks.push(provider.parse(value, meta));\n }\n }\n };\n\n objectVisitor(mergedConfig, parse);\n await Promise.all(tasks);\n\n return new AppConfig<T>(mergedConfig);\n }\n}\n","/**\n * Metadata about a value's location within an object structure.\n */\nexport type ObjectVisitorMeta = {\n /** The full path to the value (e.g., \"database.host\" or \"items[0]\"). */\n path: string;\n /** The object that owns this property. */\n owner: object;\n /** The property name or array index path (e.g., \"host\" or \"items[0]\"). */\n propertyPath: string;\n /** The type of the property value. */\n propertyType: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function';\n /** The array index if the value is in an array, undefined otherwise. */\n arrayIndex?: number;\n};\n\n/**\n * Callback function invoked for each primitive value found during object traversal.\n *\n * @param value - The primitive value found.\n * @param meta - Metadata about the value's location in the object structure.\n */\nexport type ObjectVisitorCallback = (value: unknown, meta: ObjectVisitorMeta) => void;\n\n/**\n * Traverses an object structure and invokes a callback for each primitive value found.\n *\n * The visitor recursively traverses objects and arrays, calling the callback for each\n * primitive value (string, number, boolean, bigint) encountered. It skips functions,\n * symbols, null, and undefined values.\n *\n * @param obj - The object to traverse. Can be any value.\n * @param callback - The callback function to invoke for each primitive value.\n *\n * @example\n * ```typescript\n * const config = {\n * database: { host: 'localhost', port: 5432 },\n * items: ['a', 'b', 'c']\n * };\n *\n * objectVisitor(config, (value, meta) => {\n * console.log(`${meta.path} = ${value}`);\n * });\n * // Output:\n * // database.host = localhost\n * // database.port = 5432\n * // items[0] = a\n * // items[1] = b\n * // items[2] = c\n * ```\n */\nexport const objectVisitor = (obj: unknown, callback: ObjectVisitorCallback): void => {\n const visit = (\n obj: unknown,\n callback: ObjectVisitorCallback,\n path: string = '',\n owner: object = {},\n propertyPath: string = '',\n arrayIndex?: number,\n ): void => {\n if (!obj) {\n return;\n }\n\n switch (typeof obj) {\n case 'object':\n if (Array.isArray(obj)) {\n obj.forEach((item, index) => {\n visit(item, callback, path + `[${index}]`, obj, propertyPath + `[${index}]`, index);\n });\n } else {\n const entries = Object.entries(obj);\n for (const entry of entries) {\n visit(entry[1], callback, path + (path.length > 0 ? '.' : '') + entry[0], obj, entry[0]);\n }\n }\n break;\n case 'function':\n case 'symbol':\n case 'undefined':\n break;\n default:\n callback(obj, {\n owner,\n propertyPath,\n path,\n propertyType: typeof obj,\n arrayIndex,\n });\n break;\n }\n };\n\n visit(obj, callback);\n};\n","/**\n * Attempts to parse a string as JSON, returning the original string if parsing fails.\n *\n * @param text - The text to parse.\n * @returns The parsed JSON value, or the original text if parsing fails.\n */\nexport function tryParseJson(text: string): unknown {\n try {\n return JSON.parse(text, (_, value) => value);\n } catch {\n return text;\n }\n}\n\n/**\n * Transforms a flat key/value record into a nested object by splitting keys on a\n * separator string.\n *\n * Each key is split into path segments. Intermediate objects are created as needed.\n * If a path segment collides with an existing non-object value it is replaced by the\n * new object. Keys that do not contain the separator are passed through unchanged.\n *\n * Supports arbitrary nesting depth — a key with N separators produces N+1 levels.\n *\n * @param record - The flat key/value record to transform.\n * @param separator - The string used to delimit path segments (e.g. `'__'`).\n * @returns A new nested object.\n *\n * @example\n * ```typescript\n * nestKeys(\n * {\n * WEBHOOK__secret: 'abc',\n * WEBHOOK__header: 'X-Sig',\n * DATABASE_URL: 'postgres://localhost/db',\n * },\n * '__',\n * );\n * // → { WEBHOOK: { secret: 'abc', header: 'X-Sig' }, DATABASE_URL: 'postgres://localhost/db' }\n * ```\n */\nexport function nestKeys(record: Record<string, unknown>, separator: string): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(record)) {\n const parts = key.split(separator);\n\n if (parts.length === 1) {\n result[key] = value;\n } else {\n let current = result;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]!;\n if (typeof current[part] !== 'object' || current[part] === null) {\n current[part] = {};\n }\n current = current[part] as Record<string, unknown>;\n }\n current[parts[parts.length - 1]!] = value;\n }\n }\n\n return result;\n}\n","import { AppConfigSource } from '../app.config.source.js';\nimport { nestKeys } from '../helpers.js';\nimport dotenv from 'dotenv';\n\n/**\n * Options for {@link AppConfigSourceDotenv}.\n */\nexport interface AppConfigSourceDotenvOptions {\n /**\n * When set, keys containing this separator are split into nested objects.\n *\n * For example, with `groupSeparator: '__'` the key `WEBHOOK__secret` becomes\n * `{ WEBHOOK: { secret: '...' } }`. Supports arbitrary nesting depth.\n *\n * @example\n * ```typescript\n * // .env\n * // WEBHOOK__secret=abc\n * // WEBHOOK__header=X-Sig\n * // DATABASE_URL=postgres://localhost/db\n *\n * const source = new AppConfigSourceDotenv('./.env', { groupSeparator: '__' });\n * await source.load();\n * // → { WEBHOOK: { secret: 'abc', header: 'X-Sig' }, DATABASE_URL: 'postgres://localhost/db' }\n * ```\n */\n groupSeparator?: string;\n}\n\n/**\n * Configuration source that loads environment variables from a `.env` file.\n *\n * This source uses the `dotenv` package to load environment variables from a `.env` file.\n * If no file path is provided, it will look for a `.env` file in the current working directory.\n * All values are strings as provided by the environment file.\n *\n * When the `groupSeparator` option is set, keys that contain the separator are automatically\n * collapsed into nested objects. This is useful for grouping related env vars under a shared\n * prefix (e.g. `WEBHOOK__secret` and `WEBHOOK__header` → `{ WEBHOOK: { secret, header } }`).\n *\n * @example\n * ```typescript\n * // Load from default .env file\n * const source1 = new AppConfigSourceDotenv();\n * const config1 = await source1.load();\n *\n * // Load from custom path\n * const source2 = new AppConfigSourceDotenv('./config/.env.local');\n * const config2 = await source2.load();\n *\n * // Group keys with __ separator into nested objects\n * const source3 = new AppConfigSourceDotenv('./.env', { groupSeparator: '__' });\n * const config3 = await source3.load();\n * ```\n */\nexport class AppConfigSourceDotenv implements AppConfigSource {\n /**\n * Creates a new AppConfigSourceDotenv instance.\n *\n * @param filePath - Optional path to the `.env` file. If not provided, `dotenv` will\n * look for a `.env` file in the current working directory.\n * @param options - Optional configuration options.\n */\n constructor(\n private readonly filePath?: string,\n private readonly options?: AppConfigSourceDotenvOptions,\n ) {}\n\n /**\n * Loads environment variables from the `.env` file.\n *\n * Uses `dotenv.config()` to parse the file and load variables into the returned object.\n * If `options.groupSeparator` is set the flat keys are transformed into a nested object\n * before being returned.\n *\n * @returns A promise that resolves to an object containing the parsed environment variables.\n * @throws {Error} If there's an error reading or parsing the `.env` file.\n */\n async load(): Promise<Record<string, unknown>> {\n const result = dotenv.config({ path: this.filePath, quiet: true });\n if (result.error) {\n throw result.error;\n }\n const parsed = result.parsed ?? {};\n\n if (this.options?.groupSeparator) {\n return nestKeys(parsed, this.options.groupSeparator);\n }\n\n return parsed;\n }\n}\n","import { existsSync } from 'node:fs';\nimport { AppConfigSource } from '../app.config.source.js';\nimport { readFile } from 'node:fs/promises';\nimport { AppConfigSourceFileOptions } from '../app.config.source.options.js';\n\n/**\n * Configuration source that loads configuration from a JSON file.\n *\n * This source reads a JSON file from the filesystem and parses it as a configuration object.\n * By default, it will return an empty object if the file doesn't exist instead of throwing an error.\n *\n * @example\n * ```typescript\n * // Load from JSON file, ignore if missing\n * const source1 = new AppConfigSourceJson('./config.json');\n *\n * // Load from JSON file, throw error if missing\n * const source2 = new AppConfigSourceJson('./config.json', {\n * ignoreMissingFile: false\n * });\n *\n * // Load with custom encoding\n * const source3 = new AppConfigSourceJson('./config.json', {\n * encoding: 'utf16le'\n * });\n * ```\n */\nexport class AppConfigSourceJson implements AppConfigSource {\n private readonly options: AppConfigSourceFileOptions;\n\n /**\n * Creates a new AppConfigSourceJson instance.\n *\n * @param filePath - The path to the JSON file to load.\n * @param options - Optional configuration for the source behavior.\n */\n constructor(\n private readonly filePath: string,\n options?: AppConfigSourceFileOptions,\n ) {\n this.options = {\n ignoreMissingFile: true,\n encoding: 'utf8',\n ...(options ?? {}),\n };\n }\n\n /**\n * Loads configuration from the JSON file.\n *\n * If the file doesn't exist and `ignoreMissingFile` is `true`, returns an empty object.\n * Otherwise, reads and parses the JSON file.\n *\n * @returns A promise that resolves to the parsed JSON configuration object.\n * @throws {Error} If the file doesn't exist and `ignoreMissingFile` is `false`,\n * or if the file contains invalid JSON.\n */\n async load(): Promise<Record<string, unknown>> {\n if (!existsSync(this.filePath) && this.options.ignoreMissingFile) {\n return {};\n }\n\n const file = await readFile(this.filePath, {\n encoding: this.options.encoding,\n });\n return JSON.parse(file.toString());\n }\n}\n","import { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport YAML from 'yaml';\nimport { AppConfigSource } from '../app.config.source.js';\nimport { AppConfigSourceFileOptions } from '../app.config.source.options.js';\n\n/**\n * Configuration source that loads configuration from a YAML file.\n *\n * This source reads a YAML file from the filesystem and parses it as a configuration object.\n * By default, it will return an empty object if the file doesn't exist instead of throwing an error.\n * Supports both `.yaml` and `.yml` file extensions.\n *\n * @example\n * ```typescript\n * // Load from YAML file, ignore if missing\n * const source1 = new AppConfigSourceYaml('./config.yaml');\n *\n * // Load from YAML file, throw error if missing\n * const source2 = new AppConfigSourceYaml('./config.yaml', {\n * ignoreMissingFile: false\n * });\n *\n * // Load with custom encoding\n * const source3 = new AppConfigSourceYaml('./config.yaml', {\n * encoding: 'utf16le'\n * });\n * ```\n */\nexport class AppConfigSourceYaml implements AppConfigSource {\n private readonly options: AppConfigSourceFileOptions;\n\n /**\n * Creates a new AppConfigSourceYaml instance.\n *\n * @param filePath - The path to the YAML file to load.\n * @param options - Optional configuration for the source behavior.\n */\n constructor(\n private readonly filePath: string,\n options?: AppConfigSourceFileOptions,\n ) {\n this.options = {\n ignoreMissingFile: true,\n encoding: 'utf8',\n ...(options ?? {}),\n };\n }\n\n /**\n * Loads configuration from the YAML file.\n *\n * If the file doesn't exist and `ignoreMissingFile` is `true`, returns an empty object.\n * Otherwise, reads and parses the YAML file.\n *\n * @returns A promise that resolves to the parsed YAML configuration object.\n * @throws {Error} If the file doesn't exist and `ignoreMissingFile` is `false`,\n * or if the file contains invalid YAML.\n */\n async load(): Promise<Record<string, unknown>> {\n if (!existsSync(this.filePath) && this.options.ignoreMissingFile) {\n return {};\n }\n\n const file = await readFile(this.filePath, {\n encoding: this.options.encoding,\n });\n return YAML.parse(file.toString());\n }\n}\n","import { AppConfigProvider } from '../app.config.provider.js';\nimport { ObjectVisitorMeta } from '../object.visitor.js';\nimport { tryParseJson } from '../helpers.js';\n\n/**\n * Provider that resolves environment variable references in configuration values.\n *\n * This provider matches string values using a regex pattern and replaces them with\n * values from `process.env`. The default pattern matches `${env:KEY}` and extracts\n * the key part to look up in the environment.\n *\n * After replacement, the result is attempted to be parsed as JSON. If parsing succeeds,\n * the parsed value is used; otherwise, the string value is used.\n *\n * @example\n * ```typescript\n * // With default pattern /\\$\\{env:(.+)\\}/g\n * // Value: \"${env:DATABASE_URL}\"\n * // Looks up: process.env.DATABASE_URL\n *\n * // Custom pattern\n * const provider = new AppConfigProviderDotenv(/\\$\\{([^}]+)\\}/g);\n * // Value: \"${DATABASE_URL}\"\n * // Looks up: process.env.DATABASE_URL\n * ```\n */\nexport class AppConfigProviderDotenv implements AppConfigProvider {\n private readonly prefix: RegExp;\n\n /**\n * Creates a new AppConfigProviderDotenv instance.\n *\n * @param prefix - A regex pattern or string to match environment variable references.\n * If a string is provided, it will be converted to a RegExp. The regex must have\n * at least one capture group that extracts the environment variable key.\n * Defaults to `/\\$\\{env:(.+)\\}/g` which matches `${env:KEY}` patterns.\n *\n * @example\n * ```typescript\n * // Default pattern\n * const provider1 = new AppConfigProviderDotenv();\n *\n * // Custom regex pattern\n * const provider2 = new AppConfigProviderDotenv(/\\$\\{([^}]+)\\}/g);\n *\n * // String pattern (converted to RegExp)\n * const provider3 = new AppConfigProviderDotenv('env:');\n * ```\n */\n constructor(prefix: string | RegExp = /\\$\\{env:(.+)\\}/g) {\n this.prefix = typeof prefix === 'string' ? new RegExp(prefix) : prefix;\n }\n\n /**\n * Checks if this provider can parse the given value.\n *\n * @param value - The string value to check.\n * @returns `true` if the value matches the provider's regex pattern, `false` otherwise.\n */\n canParse(value: string): boolean {\n this.prefix.lastIndex = 0;\n return this.prefix.test(value);\n }\n\n /**\n * Parses the value by replacing environment variable references with actual values.\n *\n * The method:\n * 1. Finds all matches of the regex pattern in the value\n * 2. Replaces each match with the corresponding value from `process.env`\n * 3. Attempts to parse the result as JSON\n * 4. Updates the configuration object with the final value\n *\n * @param value - The string value containing environment variable references.\n * @param meta - Metadata about the value's location in the configuration object.\n * @returns A promise that resolves when the transformation is complete.\n *\n * @example\n * ```typescript\n * // If process.env.DATABASE_URL = \"postgres://localhost/db\"\n * // Value: \"${env:DATABASE_URL}\"\n * // Result: \"postgres://localhost/db\"\n *\n * // If process.env.PORT = \"3000\"\n * // Value: \"${env:PORT}\"\n * // Result: 3000 (parsed as JSON number)\n * ```\n */\n async parse(value: string, meta: ObjectVisitorMeta): Promise<void> {\n const matches = value.matchAll(this.prefix);\n\n let result = value;\n for (const [found, key] of matches) {\n result = result.replaceAll(found, process.env[key!] ?? '');\n }\n\n if (meta.arrayIndex !== undefined && Array.isArray(meta.owner)) {\n meta.owner[meta.arrayIndex] = tryParseJson(result ?? '');\n } else {\n (meta.owner as Record<string, unknown>)[meta.propertyPath] = tryParseJson(result ?? '');\n }\n }\n}\n","import { Injectable } from 'injectkit';\nimport { AppConfigProvider } from '../app.config.provider.js';\nimport { ObjectVisitorMeta } from '../object.visitor.js';\nimport { SecretManagerServiceClient } from '@google-cloud/secret-manager';\nimport { ServerkitError } from '@maroonedsoftware/errors';\nimport { tryParseJson } from '../helpers.js';\n\n/**\n * Provider that resolves Google Cloud Platform Secret Manager references in configuration values.\n *\n * This provider matches string values using a regex pattern and replaces them with\n * secrets fetched from GCP Secret Manager. The default pattern matches `${gcp:SECRET_NAME}`\n * and extracts the secret name to look up in Secret Manager.\n *\n * After retrieval, the secret value is attempted to be parsed as JSON. If parsing succeeds,\n * the parsed value is used; otherwise, the string value is used.\n *\n * @remarks\n * This provider requires valid GCP credentials to be configured. It uses the\n * `@google-cloud/secret-manager` package and will use Application Default Credentials (ADC).\n *\n * @example\n * ```typescript\n * // With default pattern /\\$\\{gcp:(.+)\\}/g\n * // Value: \"${gcp:DATABASE_PASSWORD}\"\n * // Fetches: projects/{projectId}/secrets/DATABASE_PASSWORD/versions/latest\n *\n * const config = await new AppConfigBuilder()\n * .addSource(new AppConfigSourceJson('./config.json'))\n * .addProvider(new AppConfigProviderGcpSecrets('my-gcp-project'))\n * .build();\n * ```\n */\n@Injectable()\nexport class AppConfigProviderGcpSecrets implements AppConfigProvider {\n private readonly secretmanagerClient = new SecretManagerServiceClient();\n private readonly prefix: RegExp;\n\n /**\n * Creates a new AppConfigProviderGcpSecrets instance.\n *\n * @param projectId - The GCP project ID where secrets are stored.\n * @param prefix - A regex pattern or string to match secret references.\n * If a string is provided, it will be converted to a RegExp. The regex must have\n * at least one capture group that extracts the secret name.\n * Defaults to `/\\$\\{gcp:(.+)\\}/g` which matches `${gcp:SECRET_NAME}` patterns.\n *\n * @example\n * ```typescript\n * // Default pattern\n * const provider1 = new AppConfigProviderGcpSecrets('my-project');\n *\n * // Custom regex pattern\n * const provider2 = new AppConfigProviderGcpSecrets('my-project', /\\$\\{secret:([^}]+)\\}/g);\n * ```\n */\n constructor(\n private readonly projectId: string,\n prefix: string | RegExp = /\\$\\{gcp:(.+)\\}/g,\n ) {\n this.prefix = typeof prefix === 'string' ? new RegExp(prefix) : prefix;\n }\n\n /**\n * Checks if this provider can parse the given value.\n *\n * @param value - The string value to check.\n * @returns `true` if the value matches the provider's regex pattern, `false` otherwise.\n */\n canParse(value: string): boolean {\n // `.test()` with a `/g`-flagged regex advances `lastIndex`, which can cause a\n // false negative on a subsequent call against the same string. Reset before\n // testing so behavior is independent of call order.\n this.prefix.lastIndex = 0;\n return this.prefix.test(value);\n }\n\n /**\n * Fetches a secret from GCP Secret Manager.\n *\n * @param secretId - The name of the secret to fetch.\n * @returns A promise that resolves to the secret value.\n * @throws {ServerkitError} When Secret Manager rejects the access request (e.g. missing\n * secret, IAM denial, network failure). The original error is attached via `withCause`\n * and the failing `secretId` / `projectId` are recorded in `internalDetails`. Surfacing\n * the failure prevents callers booting with an empty password / API key.\n * @internal\n */\n private async getSecret(secretId: string): Promise<string> {\n try {\n const [secret] = await this.secretmanagerClient.accessSecretVersion({\n name: `projects/${this.projectId}/secrets/${secretId}/versions/latest`,\n });\n return secret.payload?.data?.toString() ?? '';\n } catch (error) {\n // Surface failures loudly: silently returning `''` lets services boot with\n // an empty password / API key, which is far worse than a hard failure here.\n throw new ServerkitError(`AppConfigProviderGcpSecrets: failed to resolve secret \"${secretId}\" in project \"${this.projectId}\"`)\n .withCause(error as Error)\n .withInternalDetails({ secretId, projectId: this.projectId });\n }\n }\n\n /**\n * Parses the value by replacing GCP secret references with actual secret values.\n *\n * The method:\n * 1. Finds all matches of the regex pattern in the value\n * 2. Fetches each secret from GCP Secret Manager in parallel\n * 3. Attempts to parse each result as JSON\n * 4. Updates the configuration object with the final value\n *\n * @param value - The string value containing GCP secret references.\n * @param meta - Metadata about the value's location in the configuration object.\n * @returns A promise that resolves when all secrets have been fetched and the\n * transformation is complete.\n * @throws {ServerkitError} Propagated from {@link getSecret} when any referenced secret\n * cannot be resolved. The build call site is expected to fail loud and stop boot.\n *\n * @example\n * ```typescript\n * // If GCP secret \"API_KEY\" contains \"sk-abc123\"\n * // Value: \"${gcp:API_KEY}\"\n * // Result: \"sk-abc123\"\n *\n * // If GCP secret \"CONFIG\" contains '{\"retries\": 3}'\n * // Value: \"${gcp:CONFIG}\"\n * // Result: { retries: 3 } (parsed as JSON object)\n * ```\n */\n async parse(value: string, meta: ObjectVisitorMeta): Promise<void> {\n const tasks: Promise<void>[] = [];\n const matches = value.matchAll(this.prefix);\n\n for (const [, key] of matches) {\n const task = this.getSecret(key!).then(value => {\n if (meta.arrayIndex !== undefined && Array.isArray(meta.owner)) {\n meta.owner[meta.arrayIndex] = tryParseJson(value);\n } else {\n (meta.owner as Record<string, unknown>)[meta.propertyPath] = tryParseJson(value);\n }\n });\n tasks.push(task);\n }\n\n await Promise.all(tasks);\n }\n}\n"],"mappings":"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","names":["AppConfig","config","get","key","getAs","getString","String","getNumber","Number","getBoolean","Boolean","getObject","deepmerge","objectVisitor","obj","callback","visit","path","owner","propertyPath","arrayIndex","Array","isArray","forEach","item","index","entries","Object","entry","length","propertyType","AppConfigBuilder","sources","providers","addSource","source","push","addProvider","provider","build","sourceTasks","Promise","all","map","x","load","mergedConfig","length","deepmerge","tasks","parse","value","meta","find","canParse","objectVisitor","AppConfig","tryParseJson","text","JSON","parse","_","value","nestKeys","record","separator","result","key","Object","entries","parts","split","length","current","i","part","dotenv","AppConfigSourceDotenv","filePath","options","load","result","dotenv","config","path","quiet","error","parsed","groupSeparator","nestKeys","existsSync","readFile","AppConfigSourceJson","options","filePath","ignoreMissingFile","encoding","load","existsSync","file","readFile","JSON","parse","toString","existsSync","readFile","YAML","AppConfigSourceYaml","options","filePath","ignoreMissingFile","encoding","load","existsSync","file","readFile","YAML","parse","toString","AppConfigProviderDotenv","prefix","RegExp","canParse","value","lastIndex","test","parse","meta","matches","matchAll","result","found","key","replaceAll","process","env","arrayIndex","undefined","Array","isArray","owner","tryParseJson","propertyPath","Injectable","SecretManagerServiceClient","ServerkitError","AppConfigProviderGcpSecrets","secretmanagerClient","SecretManagerServiceClient","prefix","projectId","RegExp","canParse","value","lastIndex","test","getSecret","secretId","secret","accessSecretVersion","name","payload","data","toString","error","ServerkitError","withCause","withInternalDetails","parse","meta","tasks","matches","matchAll","key","task","then","arrayIndex","undefined","Array","isArray","owner","tryParseJson","propertyPath","push","Promise","all"]}
|
|
1
|
+
{"version":3,"sources":["../src/app.config.ts","../src/app.config.builder.ts","../src/object.visitor.ts","../src/helpers.ts","../src/sources/app.config.source.dotenv.ts","../src/sources/app.config.source.json.ts","../src/sources/app.config.source.yaml.ts","../src/providers/app.config.provider.dotenv.ts","../src/providers/app.config.provider.gcp.secrets.ts","../src/providers/app.config.provider.aws.secrets.ts","../src/options/app.config.options.ts","../src/options/app.config.store.ts","../src/options/app.config.options.monitor.ts","../src/options/app.config.options.manager.ts","../src/options/app.config.options.registration.ts"],"sourcesContent":["/**\n * Configuration container that provides type-safe access to configuration values.\n *\n * @template T - The type of the configuration object. Defaults to `Record<string, unknown>`.\n *\n * @example\n * ```typescript\n * const config = new AppConfig({\n * database: { host: 'localhost', port: 5432 },\n * api: { timeout: 5000 }\n * });\n *\n * const host = config.get('database').host; // Type-safe access\n * ```\n */\nexport class AppConfig<T = Record<string, unknown>> {\n /**\n * Creates a new AppConfig instance with the provided configuration.\n *\n * @param config - The configuration object to wrap.\n */\n constructor(private readonly config: T) {}\n\n /**\n * Retrieves a configuration value by key.\n *\n * @template K - The key type, must be a key of T.\n * @param key - The configuration key to retrieve.\n * @returns The configuration value for the given key.\n *\n * @example\n * ```typescript\n * const config = new AppConfig({ port: 3000, host: 'localhost' });\n * const port = config.get('port'); // Returns 3000, typed as number\n * ```\n */\n get(key: keyof T): T[keyof T] {\n return this.config[key];\n }\n\n /**\n * Retrieves a configuration value cast to a specific type.\n *\n * Unlike `get()`, which returns `T[keyof T]`, this method lets you cast the\n * value to an arbitrary type `U`. Use this when the TypeScript type of the\n * stored value differs from what you need at the call site — for example,\n * when reading a nested object as a typed interface.\n *\n * @template U - The type to cast the value to.\n * @param key - The configuration key to retrieve.\n * @returns The configuration value cast to `U`.\n *\n * @example\n * ```typescript\n * interface DbConfig { host: string; port: number }\n *\n * const config = new AppConfig({ database: { host: 'localhost', port: 5432 } });\n * const db = config.getAs<DbConfig>('database');\n * console.log(db.host); // 'localhost'\n * ```\n */\n getAs<U>(key: keyof T): U {\n return this.config[key] as U;\n }\n\n /**\n * Retrieves a configuration value as a string.\n *\n * The value is converted to a string using `String()`. This is useful when\n * you need to ensure a value is a string regardless of its original type.\n *\n * @template K - The key type, must be a key of T.\n * @param key - The configuration key to retrieve.\n * @returns The configuration value converted to a string.\n *\n * @example\n * ```typescript\n * const config = new AppConfig({ port: 3000, enabled: true });\n * const portStr = config.getString('port'); // Returns \"3000\"\n * const enabledStr = config.getString('enabled'); // Returns \"true\"\n * ```\n */\n getString(key: keyof T): string {\n return String(this.get(key));\n }\n\n /**\n * Retrieves a configuration value as a number.\n *\n * The value is converted to a number using `Number()`. This is useful when\n * you need to ensure a value is a number regardless of its original type.\n * Note: Invalid conversions will result in `NaN`.\n *\n * @template K - The key type, must be a key of T.\n * @param key - The configuration key to retrieve.\n * @returns The configuration value converted to a number.\n *\n * @example\n * ```typescript\n * const config = new AppConfig({ port: '3000', timeout: '5000' });\n * const port = config.getNumber('port'); // Returns 3000\n * const timeout = config.getNumber('timeout'); // Returns 5000\n * ```\n */\n getNumber(key: keyof T): number {\n return Number(this.config[key]);\n }\n\n /**\n * Retrieves a configuration value as a boolean.\n *\n * The value is converted to a boolean using `Boolean()`. This is useful when\n * you need to ensure a value is a boolean regardless of its original type.\n * Note: Only falsy values (false, 0, '', null, undefined, NaN) become false.\n *\n * @template K - The key type, must be a key of T.\n * @param key - The configuration key to retrieve.\n * @returns The configuration value converted to a boolean.\n *\n * @example\n * ```typescript\n * const config = new AppConfig({ enabled: 'true', debug: 1 });\n * const enabled = config.getBoolean('enabled'); // Returns true\n * const debug = config.getBoolean('debug'); // Returns true\n * ```\n */\n getBoolean(key: keyof T): boolean {\n return Boolean(this.config[key]);\n }\n\n /**\n * Retrieves a configuration value as an object.\n *\n * The value is cast to an object type. This is useful when you know a value\n * is an object and want to access it with object methods.\n *\n * @template K - The key type, must be a key of T.\n * @param key - The configuration key to retrieve.\n * @returns The configuration value cast as an object.\n *\n * @example\n * ```typescript\n * const config = new AppConfig({\n * database: { host: 'localhost', port: 5432 }\n * });\n * const db = config.getObject('database'); // Returns { host: 'localhost', port: 5432 }\n * ```\n */\n getObject(key: keyof T): object {\n return this.config[key] as object;\n }\n}\n","import { deepmerge } from 'deepmerge-ts';\nimport { AppConfig } from './app.config.js';\nimport { AppConfigProvider } from './app.config.provider.js';\nimport { AppConfigSource } from './app.config.source.js';\nimport { objectVisitor, ObjectVisitorMeta } from './object.visitor.js';\n\n/**\n * Builder for constructing AppConfig instances from multiple sources with value transformation.\n *\n * The builder allows you to:\n * - Load configuration from multiple sources (files, environment variables, etc.)\n * - Merge configurations with later sources overriding earlier ones\n * - Transform string values using providers (e.g., resolving environment variable references)\n *\n * @example\n * ```typescript\n * const config = await new AppConfigBuilder()\n * .addSource(new AppConfigSourceJson('./config.json'))\n * .addSource(new AppConfigSourceDotenv())\n * .addProvider(new AppConfigProviderDotenv())\n * .build();\n * ```\n */\nexport class AppConfigBuilder {\n private readonly sources: AppConfigSource[] = [];\n private readonly providers: AppConfigProvider[] = [];\n\n /**\n * Adds a configuration source to the builder.\n *\n * Sources are loaded in the order they are added, and later sources override earlier ones\n * when merging configurations.\n *\n * @param source - The configuration source to add.\n * @returns The builder instance for method chaining.\n *\n * @example\n * ```typescript\n * builder\n * .addSource(new AppConfigSourceJson('./default.json'))\n * .addSource(new AppConfigSourceJson('./local.json'));\n * ```\n */\n addSource(source: AppConfigSource) {\n this.sources.push(source);\n return this;\n }\n\n /**\n * Adds a provider to transform string values during configuration building.\n *\n * Providers are applied to all string values found in the merged configuration.\n * The first provider that can parse a value will be used to transform it.\n *\n * @param provider - The provider to add.\n * @returns The builder instance for method chaining.\n *\n * @example\n * ```typescript\n * builder.addProvider(new AppConfigProviderDotenv());\n * ```\n */\n addProvider(provider: AppConfigProvider) {\n this.providers.push(provider);\n return this;\n }\n\n /**\n * Builds the AppConfig instance by loading all sources, merging them, and applying providers.\n *\n * The build process:\n * 1. Loads all sources in parallel\n * 2. Merges configurations (later sources override earlier ones)\n * 3. Traverses the merged configuration and applies providers to string values\n * 4. Returns the final AppConfig instance\n *\n * @template T - The type of the configuration object. Defaults to `Record<string, unknown>`.\n * @returns A promise that resolves to the built AppConfig instance.\n *\n * @example\n * ```typescript\n * const config = await builder.build<MyConfigType>();\n * const value = config.get('someKey');\n * ```\n */\n async build<T = Record<string, unknown>>(): Promise<AppConfig<T>> {\n const sourceTasks = await Promise.all(this.sources.map(x => x.load()));\n // `deepmerge` with zero arguments returns `undefined`, which would crash\n // every downstream consumer with an opaque \"cannot read property of\n // undefined\". A misconfigured builder should still yield a usable empty\n // config object so the error surfaces at the missing-key call site.\n const mergedConfig = (sourceTasks.length === 0 ? {} : deepmerge(...sourceTasks)) as T;\n\n const tasks: Promise<void>[] = [];\n const parse = (value: unknown, meta: ObjectVisitorMeta) => {\n if (typeof value === 'string') {\n const provider = this.providers.find(x => x.canParse(value));\n if (provider) {\n tasks.push(provider.parse(value, meta));\n }\n }\n };\n\n objectVisitor(mergedConfig, parse);\n await Promise.all(tasks);\n\n return new AppConfig<T>(mergedConfig);\n }\n}\n","/**\n * Metadata about a value's location within an object structure.\n */\nexport type ObjectVisitorMeta = {\n /** The full path to the value (e.g., \"database.host\" or \"items[0]\"). */\n path: string;\n /** The object that owns this property. */\n owner: object;\n /** The property name or array index path (e.g., \"host\" or \"items[0]\"). */\n propertyPath: string;\n /** The type of the property value. */\n propertyType: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function';\n /** The array index if the value is in an array, undefined otherwise. */\n arrayIndex?: number;\n};\n\n/**\n * Callback function invoked for each primitive value found during object traversal.\n *\n * @param value - The primitive value found.\n * @param meta - Metadata about the value's location in the object structure.\n */\nexport type ObjectVisitorCallback = (value: unknown, meta: ObjectVisitorMeta) => void;\n\n/**\n * Traverses an object structure and invokes a callback for each primitive value found.\n *\n * The visitor recursively traverses objects and arrays, calling the callback for each\n * primitive value (string, number, boolean, bigint) encountered. It skips functions,\n * symbols, null, and undefined values.\n *\n * @param obj - The object to traverse. Can be any value.\n * @param callback - The callback function to invoke for each primitive value.\n *\n * @example\n * ```typescript\n * const config = {\n * database: { host: 'localhost', port: 5432 },\n * items: ['a', 'b', 'c']\n * };\n *\n * objectVisitor(config, (value, meta) => {\n * console.log(`${meta.path} = ${value}`);\n * });\n * // Output:\n * // database.host = localhost\n * // database.port = 5432\n * // items[0] = a\n * // items[1] = b\n * // items[2] = c\n * ```\n */\nexport const objectVisitor = (obj: unknown, callback: ObjectVisitorCallback): void => {\n const visit = (\n obj: unknown,\n callback: ObjectVisitorCallback,\n path: string = '',\n owner: object = {},\n propertyPath: string = '',\n arrayIndex?: number,\n ): void => {\n if (!obj) {\n return;\n }\n\n switch (typeof obj) {\n case 'object':\n if (Array.isArray(obj)) {\n obj.forEach((item, index) => {\n visit(item, callback, path + `[${index}]`, obj, propertyPath + `[${index}]`, index);\n });\n } else {\n const entries = Object.entries(obj);\n for (const entry of entries) {\n visit(entry[1], callback, path + (path.length > 0 ? '.' : '') + entry[0], obj, entry[0]);\n }\n }\n break;\n case 'function':\n case 'symbol':\n case 'undefined':\n break;\n default:\n callback(obj, {\n owner,\n propertyPath,\n path,\n propertyType: typeof obj,\n arrayIndex,\n });\n break;\n }\n };\n\n visit(obj, callback);\n};\n","/**\n * Attempts to parse a string as JSON, returning the original string if parsing fails.\n *\n * @param text - The text to parse.\n * @returns The parsed JSON value, or the original text if parsing fails.\n */\nexport function tryParseJson(text: string): unknown {\n try {\n return JSON.parse(text, (_, value) => value);\n } catch {\n return text;\n }\n}\n\n/**\n * Recursively compares two values for structural equality.\n *\n * Used to suppress no-op config-reload notifications: a secret re-fetched from a\n * secret manager often produces a value that is structurally identical to the\n * one already held, and reloading it should not bounce live consumers (e.g. a DB\n * pool listening via `onChange`).\n *\n * Handles primitives, arrays, and plain objects by value. Two `NaN`s compare as\n * unequal (matching `===` semantics); functions and other exotic values compare\n * by reference. Key order is ignored for objects.\n *\n * @param a - The first value to compare.\n * @param b - The second value to compare.\n * @returns `true` if the values are structurally equal, `false` otherwise.\n */\nexport function structurallyEqual(a: unknown, b: unknown): boolean {\n if (a === b) {\n return true;\n }\n if (typeof a !== typeof b || a === null || b === null || typeof a !== 'object') {\n return false;\n }\n\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {\n return false;\n }\n return a.every((item, index) => structurallyEqual(item, b[index]));\n }\n\n const aRecord = a as Record<string, unknown>;\n const bRecord = b as Record<string, unknown>;\n const aKeys = Object.keys(aRecord);\n const bKeys = Object.keys(bRecord);\n if (aKeys.length !== bKeys.length) {\n return false;\n }\n return aKeys.every(key => Object.prototype.hasOwnProperty.call(bRecord, key) && structurallyEqual(aRecord[key], bRecord[key]));\n}\n\n/**\n * Transforms a flat key/value record into a nested object by splitting keys on a\n * separator string.\n *\n * Each key is split into path segments. Intermediate objects are created as needed.\n * If a path segment collides with an existing non-object value it is replaced by the\n * new object. Keys that do not contain the separator are passed through unchanged.\n *\n * Supports arbitrary nesting depth — a key with N separators produces N+1 levels.\n *\n * @param record - The flat key/value record to transform.\n * @param separator - The string used to delimit path segments (e.g. `'__'`).\n * @returns A new nested object.\n *\n * @example\n * ```typescript\n * nestKeys(\n * {\n * WEBHOOK__secret: 'abc',\n * WEBHOOK__header: 'X-Sig',\n * DATABASE_URL: 'postgres://localhost/db',\n * },\n * '__',\n * );\n * // → { WEBHOOK: { secret: 'abc', header: 'X-Sig' }, DATABASE_URL: 'postgres://localhost/db' }\n * ```\n */\nexport function nestKeys(record: Record<string, unknown>, separator: string): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(record)) {\n const parts = key.split(separator);\n\n if (parts.length === 1) {\n result[key] = value;\n } else {\n let current = result;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]!;\n if (typeof current[part] !== 'object' || current[part] === null) {\n current[part] = {};\n }\n current = current[part] as Record<string, unknown>;\n }\n current[parts[parts.length - 1]!] = value;\n }\n }\n\n return result;\n}\n","import { AppConfigSource } from '../app.config.source.js';\nimport { nestKeys } from '../helpers.js';\nimport dotenv from 'dotenv';\n\n/**\n * Options for {@link AppConfigSourceDotenv}.\n */\nexport interface AppConfigSourceDotenvOptions {\n /**\n * When set, keys containing this separator are split into nested objects.\n *\n * For example, with `groupSeparator: '__'` the key `WEBHOOK__secret` becomes\n * `{ WEBHOOK: { secret: '...' } }`. Supports arbitrary nesting depth.\n *\n * @example\n * ```typescript\n * // .env\n * // WEBHOOK__secret=abc\n * // WEBHOOK__header=X-Sig\n * // DATABASE_URL=postgres://localhost/db\n *\n * const source = new AppConfigSourceDotenv('./.env', { groupSeparator: '__' });\n * await source.load();\n * // → { WEBHOOK: { secret: 'abc', header: 'X-Sig' }, DATABASE_URL: 'postgres://localhost/db' }\n * ```\n */\n groupSeparator?: string;\n}\n\n/**\n * Configuration source that loads environment variables from a `.env` file.\n *\n * This source uses the `dotenv` package to load environment variables from a `.env` file.\n * If no file path is provided, it will look for a `.env` file in the current working directory.\n * All values are strings as provided by the environment file.\n *\n * When the `groupSeparator` option is set, keys that contain the separator are automatically\n * collapsed into nested objects. This is useful for grouping related env vars under a shared\n * prefix (e.g. `WEBHOOK__secret` and `WEBHOOK__header` → `{ WEBHOOK: { secret, header } }`).\n *\n * @example\n * ```typescript\n * // Load from default .env file\n * const source1 = new AppConfigSourceDotenv();\n * const config1 = await source1.load();\n *\n * // Load from custom path\n * const source2 = new AppConfigSourceDotenv('./config/.env.local');\n * const config2 = await source2.load();\n *\n * // Group keys with __ separator into nested objects\n * const source3 = new AppConfigSourceDotenv('./.env', { groupSeparator: '__' });\n * const config3 = await source3.load();\n * ```\n */\nexport class AppConfigSourceDotenv implements AppConfigSource {\n /**\n * Creates a new AppConfigSourceDotenv instance.\n *\n * @param filePath - Optional path to the `.env` file. If not provided, `dotenv` will\n * look for a `.env` file in the current working directory.\n * @param options - Optional configuration options.\n */\n constructor(\n private readonly filePath?: string,\n private readonly options?: AppConfigSourceDotenvOptions,\n ) {}\n\n /**\n * Loads environment variables from the `.env` file.\n *\n * Uses `dotenv.config()` to parse the file and load variables into the returned object.\n * If `options.groupSeparator` is set the flat keys are transformed into a nested object\n * before being returned.\n *\n * @returns A promise that resolves to an object containing the parsed environment variables.\n * @throws {Error} If there's an error reading or parsing the `.env` file.\n */\n async load(): Promise<Record<string, unknown>> {\n const result = dotenv.config({ path: this.filePath, quiet: true });\n if (result.error) {\n throw result.error;\n }\n const parsed = result.parsed ?? {};\n\n if (this.options?.groupSeparator) {\n return nestKeys(parsed, this.options.groupSeparator);\n }\n\n return parsed;\n }\n}\n","import { existsSync } from 'node:fs';\nimport { AppConfigSource } from '../app.config.source.js';\nimport { readFile } from 'node:fs/promises';\nimport { AppConfigSourceFileOptions } from '../app.config.source.options.js';\n\n/**\n * Configuration source that loads configuration from a JSON file.\n *\n * This source reads a JSON file from the filesystem and parses it as a configuration object.\n * By default, it will return an empty object if the file doesn't exist instead of throwing an error.\n *\n * @example\n * ```typescript\n * // Load from JSON file, ignore if missing\n * const source1 = new AppConfigSourceJson('./config.json');\n *\n * // Load from JSON file, throw error if missing\n * const source2 = new AppConfigSourceJson('./config.json', {\n * ignoreMissingFile: false\n * });\n *\n * // Load with custom encoding\n * const source3 = new AppConfigSourceJson('./config.json', {\n * encoding: 'utf16le'\n * });\n * ```\n */\nexport class AppConfigSourceJson implements AppConfigSource {\n private readonly options: AppConfigSourceFileOptions;\n\n /**\n * Creates a new AppConfigSourceJson instance.\n *\n * @param filePath - The path to the JSON file to load.\n * @param options - Optional configuration for the source behavior.\n */\n constructor(\n private readonly filePath: string,\n options?: AppConfigSourceFileOptions,\n ) {\n this.options = {\n ignoreMissingFile: true,\n encoding: 'utf8',\n ...(options ?? {}),\n };\n }\n\n /**\n * Loads configuration from the JSON file.\n *\n * If the file doesn't exist and `ignoreMissingFile` is `true`, returns an empty object.\n * Otherwise, reads and parses the JSON file.\n *\n * @returns A promise that resolves to the parsed JSON configuration object.\n * @throws {Error} If the file doesn't exist and `ignoreMissingFile` is `false`,\n * or if the file contains invalid JSON.\n */\n async load(): Promise<Record<string, unknown>> {\n if (!existsSync(this.filePath) && this.options.ignoreMissingFile) {\n return {};\n }\n\n const file = await readFile(this.filePath, {\n encoding: this.options.encoding,\n });\n return JSON.parse(file.toString());\n }\n}\n","import { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport YAML from 'yaml';\nimport { AppConfigSource } from '../app.config.source.js';\nimport { AppConfigSourceFileOptions } from '../app.config.source.options.js';\n\n/**\n * Configuration source that loads configuration from a YAML file.\n *\n * This source reads a YAML file from the filesystem and parses it as a configuration object.\n * By default, it will return an empty object if the file doesn't exist instead of throwing an error.\n * Supports both `.yaml` and `.yml` file extensions.\n *\n * @example\n * ```typescript\n * // Load from YAML file, ignore if missing\n * const source1 = new AppConfigSourceYaml('./config.yaml');\n *\n * // Load from YAML file, throw error if missing\n * const source2 = new AppConfigSourceYaml('./config.yaml', {\n * ignoreMissingFile: false\n * });\n *\n * // Load with custom encoding\n * const source3 = new AppConfigSourceYaml('./config.yaml', {\n * encoding: 'utf16le'\n * });\n * ```\n */\nexport class AppConfigSourceYaml implements AppConfigSource {\n private readonly options: AppConfigSourceFileOptions;\n\n /**\n * Creates a new AppConfigSourceYaml instance.\n *\n * @param filePath - The path to the YAML file to load.\n * @param options - Optional configuration for the source behavior.\n */\n constructor(\n private readonly filePath: string,\n options?: AppConfigSourceFileOptions,\n ) {\n this.options = {\n ignoreMissingFile: true,\n encoding: 'utf8',\n ...(options ?? {}),\n };\n }\n\n /**\n * Loads configuration from the YAML file.\n *\n * If the file doesn't exist and `ignoreMissingFile` is `true`, returns an empty object.\n * Otherwise, reads and parses the YAML file.\n *\n * @returns A promise that resolves to the parsed YAML configuration object.\n * @throws {Error} If the file doesn't exist and `ignoreMissingFile` is `false`,\n * or if the file contains invalid YAML.\n */\n async load(): Promise<Record<string, unknown>> {\n if (!existsSync(this.filePath) && this.options.ignoreMissingFile) {\n return {};\n }\n\n const file = await readFile(this.filePath, {\n encoding: this.options.encoding,\n });\n return YAML.parse(file.toString());\n }\n}\n","import { AppConfigProvider } from '../app.config.provider.js';\nimport { ObjectVisitorMeta } from '../object.visitor.js';\nimport { tryParseJson } from '../helpers.js';\n\n/**\n * Provider that resolves environment variable references in configuration values.\n *\n * This provider matches string values using a regex pattern and replaces them with\n * values from `process.env`. The default pattern matches `${env:KEY}` and extracts\n * the key part to look up in the environment.\n *\n * After replacement, the result is attempted to be parsed as JSON. If parsing succeeds,\n * the parsed value is used; otherwise, the string value is used.\n *\n * @example\n * ```typescript\n * // With default pattern /\\$\\{env:(.+)\\}/g\n * // Value: \"${env:DATABASE_URL}\"\n * // Looks up: process.env.DATABASE_URL\n *\n * // Custom pattern\n * const provider = new AppConfigProviderDotenv(/\\$\\{([^}]+)\\}/g);\n * // Value: \"${DATABASE_URL}\"\n * // Looks up: process.env.DATABASE_URL\n * ```\n */\nexport class AppConfigProviderDotenv implements AppConfigProvider {\n private readonly prefix: RegExp;\n\n /**\n * Creates a new AppConfigProviderDotenv instance.\n *\n * @param prefix - A regex pattern or string to match environment variable references.\n * If a string is provided, it will be converted to a RegExp. The regex must have\n * at least one capture group that extracts the environment variable key.\n * Defaults to `/\\$\\{env:(.+)\\}/g` which matches `${env:KEY}` patterns.\n *\n * @example\n * ```typescript\n * // Default pattern\n * const provider1 = new AppConfigProviderDotenv();\n *\n * // Custom regex pattern\n * const provider2 = new AppConfigProviderDotenv(/\\$\\{([^}]+)\\}/g);\n *\n * // String pattern (converted to RegExp)\n * const provider3 = new AppConfigProviderDotenv('env:');\n * ```\n */\n constructor(prefix: string | RegExp = /\\$\\{env:(.+)\\}/g) {\n this.prefix = typeof prefix === 'string' ? new RegExp(prefix) : prefix;\n }\n\n /**\n * Checks if this provider can parse the given value.\n *\n * @param value - The string value to check.\n * @returns `true` if the value matches the provider's regex pattern, `false` otherwise.\n */\n canParse(value: string): boolean {\n this.prefix.lastIndex = 0;\n return this.prefix.test(value);\n }\n\n /**\n * Parses the value by replacing environment variable references with actual values.\n *\n * The method:\n * 1. Finds all matches of the regex pattern in the value\n * 2. Replaces each match with the corresponding value from `process.env`\n * 3. Attempts to parse the result as JSON\n * 4. Updates the configuration object with the final value\n *\n * @param value - The string value containing environment variable references.\n * @param meta - Metadata about the value's location in the configuration object.\n * @returns A promise that resolves when the transformation is complete.\n *\n * @example\n * ```typescript\n * // If process.env.DATABASE_URL = \"postgres://localhost/db\"\n * // Value: \"${env:DATABASE_URL}\"\n * // Result: \"postgres://localhost/db\"\n *\n * // If process.env.PORT = \"3000\"\n * // Value: \"${env:PORT}\"\n * // Result: 3000 (parsed as JSON number)\n * ```\n */\n async parse(value: string, meta: ObjectVisitorMeta): Promise<void> {\n const matches = value.matchAll(this.prefix);\n\n let result = value;\n for (const [found, key] of matches) {\n result = result.replaceAll(found, process.env[key!] ?? '');\n }\n\n if (meta.arrayIndex !== undefined && Array.isArray(meta.owner)) {\n meta.owner[meta.arrayIndex] = tryParseJson(result ?? '');\n } else {\n (meta.owner as Record<string, unknown>)[meta.propertyPath] = tryParseJson(result ?? '');\n }\n }\n}\n","import { Injectable } from 'injectkit';\nimport { AppConfigProvider } from '../app.config.provider.js';\nimport { ObjectVisitorMeta } from '../object.visitor.js';\nimport { SecretManagerServiceClient } from '@google-cloud/secret-manager';\nimport { ServerkitError } from '@maroonedsoftware/errors';\nimport { tryParseJson } from '../helpers.js';\n\n/**\n * Provider that resolves Google Cloud Platform Secret Manager references in configuration values.\n *\n * This provider matches string values using a regex pattern and replaces them with\n * secrets fetched from GCP Secret Manager. The default pattern matches `${gcp:SECRET_NAME}`\n * and extracts the secret name to look up in Secret Manager.\n *\n * After retrieval, the secret value is attempted to be parsed as JSON. If parsing succeeds,\n * the parsed value is used; otherwise, the string value is used.\n *\n * @remarks\n * This provider requires valid GCP credentials to be configured. It uses the\n * `@google-cloud/secret-manager` package and will use Application Default Credentials (ADC).\n *\n * @example\n * ```typescript\n * // With default pattern /\\$\\{gcp:(.+)\\}/g\n * // Value: \"${gcp:DATABASE_PASSWORD}\"\n * // Fetches: projects/{projectId}/secrets/DATABASE_PASSWORD/versions/latest\n *\n * const config = await new AppConfigBuilder()\n * .addSource(new AppConfigSourceJson('./config.json'))\n * .addProvider(new AppConfigProviderGcpSecrets('my-gcp-project'))\n * .build();\n * ```\n */\n@Injectable()\nexport class AppConfigProviderGcpSecrets implements AppConfigProvider {\n private readonly secretmanagerClient = new SecretManagerServiceClient();\n private readonly prefix: RegExp;\n\n /**\n * Creates a new AppConfigProviderGcpSecrets instance.\n *\n * @param projectId - The GCP project ID where secrets are stored.\n * @param prefix - A regex pattern or string to match secret references.\n * If a string is provided, it will be converted to a RegExp. The regex must have\n * at least one capture group that extracts the secret name.\n * Defaults to `/\\$\\{gcp:(.+)\\}/g` which matches `${gcp:SECRET_NAME}` patterns.\n *\n * @example\n * ```typescript\n * // Default pattern\n * const provider1 = new AppConfigProviderGcpSecrets('my-project');\n *\n * // Custom regex pattern\n * const provider2 = new AppConfigProviderGcpSecrets('my-project', /\\$\\{secret:([^}]+)\\}/g);\n * ```\n */\n constructor(\n private readonly projectId: string,\n prefix: string | RegExp = /\\$\\{gcp:(.+)\\}/g,\n ) {\n this.prefix = typeof prefix === 'string' ? new RegExp(prefix) : prefix;\n }\n\n /**\n * Checks if this provider can parse the given value.\n *\n * @param value - The string value to check.\n * @returns `true` if the value matches the provider's regex pattern, `false` otherwise.\n */\n canParse(value: string): boolean {\n // `.test()` with a `/g`-flagged regex advances `lastIndex`, which can cause a\n // false negative on a subsequent call against the same string. Reset before\n // testing so behavior is independent of call order.\n this.prefix.lastIndex = 0;\n return this.prefix.test(value);\n }\n\n /**\n * Fetches a secret from GCP Secret Manager.\n *\n * @param secretId - The name of the secret to fetch.\n * @returns A promise that resolves to the secret value.\n * @throws {ServerkitError} When Secret Manager rejects the access request (e.g. missing\n * secret, IAM denial, network failure). The original error is attached via `withCause`\n * and the failing `secretId` / `projectId` are recorded in `internalDetails`. Surfacing\n * the failure prevents callers booting with an empty password / API key.\n * @internal\n */\n private async getSecret(secretId: string): Promise<string> {\n try {\n const [secret] = await this.secretmanagerClient.accessSecretVersion({\n name: `projects/${this.projectId}/secrets/${secretId}/versions/latest`,\n });\n return secret.payload?.data?.toString() ?? '';\n } catch (error) {\n // Surface failures loudly: silently returning `''` lets services boot with\n // an empty password / API key, which is far worse than a hard failure here.\n throw new ServerkitError(`AppConfigProviderGcpSecrets: failed to resolve secret \"${secretId}\" in project \"${this.projectId}\"`)\n .withCause(error as Error)\n .withInternalDetails({ secretId, projectId: this.projectId });\n }\n }\n\n /**\n * Parses the value by replacing GCP secret references with actual secret values.\n *\n * The method:\n * 1. Finds all matches of the regex pattern in the value\n * 2. Fetches each secret from GCP Secret Manager in parallel\n * 3. Attempts to parse each result as JSON\n * 4. Updates the configuration object with the final value\n *\n * @param value - The string value containing GCP secret references.\n * @param meta - Metadata about the value's location in the configuration object.\n * @returns A promise that resolves when all secrets have been fetched and the\n * transformation is complete.\n * @throws {ServerkitError} Propagated from {@link getSecret} when any referenced secret\n * cannot be resolved. The build call site is expected to fail loud and stop boot.\n *\n * @example\n * ```typescript\n * // If GCP secret \"API_KEY\" contains \"sk-abc123\"\n * // Value: \"${gcp:API_KEY}\"\n * // Result: \"sk-abc123\"\n *\n * // If GCP secret \"CONFIG\" contains '{\"retries\": 3}'\n * // Value: \"${gcp:CONFIG}\"\n * // Result: { retries: 3 } (parsed as JSON object)\n * ```\n */\n async parse(value: string, meta: ObjectVisitorMeta): Promise<void> {\n const tasks: Promise<void>[] = [];\n const matches = value.matchAll(this.prefix);\n\n for (const [, key] of matches) {\n const task = this.getSecret(key!).then(value => {\n if (meta.arrayIndex !== undefined && Array.isArray(meta.owner)) {\n meta.owner[meta.arrayIndex] = tryParseJson(value);\n } else {\n (meta.owner as Record<string, unknown>)[meta.propertyPath] = tryParseJson(value);\n }\n });\n tasks.push(task);\n }\n\n await Promise.all(tasks);\n }\n}\n","import { Injectable } from 'injectkit';\nimport { AppConfigProvider } from '../app.config.provider.js';\nimport { ObjectVisitorMeta } from '../object.visitor.js';\nimport { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager';\nimport { ServerkitError } from '@maroonedsoftware/errors';\nimport { tryParseJson } from '../helpers.js';\n\n/**\n * Provider that resolves AWS Secrets Manager references in configuration values.\n *\n * This provider matches string values using a regex pattern and replaces them with\n * secrets fetched from AWS Secrets Manager. The default pattern matches `${aws:SECRET_ID}`\n * and extracts the secret id (a name or ARN) to look up in Secrets Manager.\n *\n * After retrieval, the secret value is attempted to be parsed as JSON. If parsing succeeds,\n * the parsed value is used; otherwise, the string value is used.\n *\n * @remarks\n * This provider requires valid AWS credentials to be configured. It uses the\n * `@aws-sdk/client-secrets-manager` package and resolves credentials and region from the\n * standard AWS provider chain (environment variables, shared config/credentials files,\n * instance/task roles). The region can be passed explicitly to override the chain.\n *\n * @example\n * ```typescript\n * // With default pattern /\\$\\{aws:(.+)\\}/g\n * // Value: \"${aws:DATABASE_PASSWORD}\"\n * // Fetches the latest version of the \"DATABASE_PASSWORD\" secret\n *\n * const config = await new AppConfigBuilder()\n * .addSource(new AppConfigSourceJson('./config.json'))\n * .addProvider(new AppConfigProviderAwsSecrets('us-east-1'))\n * .build();\n * ```\n */\n@Injectable()\nexport class AppConfigProviderAwsSecrets implements AppConfigProvider {\n private readonly secretsManagerClient: SecretsManagerClient;\n private readonly prefix: RegExp;\n\n /**\n * Creates a new AppConfigProviderAwsSecrets instance.\n *\n * @param region - The AWS region where secrets are stored. If omitted, the region is\n * resolved from the standard AWS provider chain (e.g. `AWS_REGION`).\n * @param prefix - A regex pattern or string to match secret references.\n * If a string is provided, it will be converted to a RegExp. The regex must have\n * at least one capture group that extracts the secret id.\n * Defaults to `/\\$\\{aws:(.+)\\}/g` which matches `${aws:SECRET_ID}` patterns.\n *\n * @example\n * ```typescript\n * // Default pattern, region from the AWS provider chain\n * const provider1 = new AppConfigProviderAwsSecrets();\n *\n * // Explicit region\n * const provider2 = new AppConfigProviderAwsSecrets('us-east-1');\n *\n * // Custom regex pattern\n * const provider3 = new AppConfigProviderAwsSecrets('us-east-1', /\\$\\{secret:([^}]+)\\}/g);\n * ```\n */\n constructor(\n private readonly region?: string,\n prefix: string | RegExp = /\\$\\{aws:(.+)\\}/g,\n ) {\n this.secretsManagerClient = new SecretsManagerClient(region ? { region } : {});\n this.prefix = typeof prefix === 'string' ? new RegExp(prefix) : prefix;\n }\n\n /**\n * Checks if this provider can parse the given value.\n *\n * @param value - The string value to check.\n * @returns `true` if the value matches the provider's regex pattern, `false` otherwise.\n */\n canParse(value: string): boolean {\n // `.test()` with a `/g`-flagged regex advances `lastIndex`, which can cause a\n // false negative on a subsequent call against the same string. Reset before\n // testing so behavior is independent of call order.\n this.prefix.lastIndex = 0;\n return this.prefix.test(value);\n }\n\n /**\n * Fetches a secret from AWS Secrets Manager.\n *\n * @param secretId - The id (name or ARN) of the secret to fetch.\n * @returns A promise that resolves to the secret value.\n * @throws {ServerkitError} When Secrets Manager rejects the access request (e.g. missing\n * secret, IAM denial, network failure). The original error is attached via `withCause`\n * and the failing `secretId` / `region` are recorded in `internalDetails`. Surfacing\n * the failure prevents callers booting with an empty password / API key.\n * @internal\n */\n private async getSecret(secretId: string): Promise<string> {\n try {\n const response = await this.secretsManagerClient.send(new GetSecretValueCommand({ SecretId: secretId }));\n if (response.SecretString !== undefined) {\n return response.SecretString;\n }\n // Binary secrets are returned as a Uint8Array; decode to UTF-8 so JSON parsing\n // and string assignment behave the same as for `SecretString`.\n return response.SecretBinary ? Buffer.from(response.SecretBinary).toString('utf-8') : '';\n } catch (error) {\n // Surface failures loudly: silently returning `''` lets services boot with\n // an empty password / API key, which is far worse than a hard failure here.\n throw new ServerkitError(`AppConfigProviderAwsSecrets: failed to resolve secret \"${secretId}\" in region \"${this.region ?? 'default'}\"`)\n .withCause(error as Error)\n .withInternalDetails({ secretId, region: this.region });\n }\n }\n\n /**\n * Parses the value by replacing AWS secret references with actual secret values.\n *\n * The method:\n * 1. Finds all matches of the regex pattern in the value\n * 2. Fetches each secret from AWS Secrets Manager in parallel\n * 3. Attempts to parse each result as JSON\n * 4. Updates the configuration object with the final value\n *\n * @param value - The string value containing AWS secret references.\n * @param meta - Metadata about the value's location in the configuration object.\n * @returns A promise that resolves when all secrets have been fetched and the\n * transformation is complete.\n * @throws {ServerkitError} Propagated from {@link getSecret} when any referenced secret\n * cannot be resolved. The build call site is expected to fail loud and stop boot.\n *\n * @example\n * ```typescript\n * // If AWS secret \"API_KEY\" contains \"sk-abc123\"\n * // Value: \"${aws:API_KEY}\"\n * // Result: \"sk-abc123\"\n *\n * // If AWS secret \"CONFIG\" contains '{\"retries\": 3}'\n * // Value: \"${aws:CONFIG}\"\n * // Result: { retries: 3 } (parsed as JSON object)\n * ```\n */\n async parse(value: string, meta: ObjectVisitorMeta): Promise<void> {\n const tasks: Promise<void>[] = [];\n const matches = value.matchAll(this.prefix);\n\n for (const [, key] of matches) {\n const task = this.getSecret(key!).then(value => {\n if (meta.arrayIndex !== undefined && Array.isArray(meta.owner)) {\n meta.owner[meta.arrayIndex] = tryParseJson(value);\n } else {\n (meta.owner as Record<string, unknown>)[meta.propertyPath] = tryParseJson(value);\n }\n });\n tasks.push(task);\n }\n\n await Promise.all(tasks);\n }\n}\n","import { Injectable } from 'injectkit';\n\n/**\n * Accessor for a boot-time snapshot of a configuration section — the ServerKit\n * analog of C#'s `IOptions<T>`.\n *\n * The value is captured once when the container is built and never changes for\n * the lifetime of the process. This is the same guarantee as injecting a typed\n * config object directly (e.g. `SlackConfig`); use it when a consumer never needs\n * to observe a reload.\n *\n * Declared as an abstract `@Injectable()` class so it can serve as a DI token.\n * Because InjectKit resolves by a runtime class identity (and generic type\n * arguments erase), each configuration section declares its own token by\n * subclassing this base — mirroring how `SlackConfig` / `Logger` are modeled:\n *\n * ```ts\n * @Injectable() export abstract class SlackOptions extends AppConfigOptions<SlackConfig> {}\n * ```\n *\n * @template T - The shape of the configuration section.\n */\n@Injectable()\nexport abstract class AppConfigOptions<T> {\n /** The configuration value captured at container-build time. */\n abstract readonly value: T;\n}\n\n/**\n * Accessor for a per-request-stable view of a configuration section — the\n * ServerKit analog of C#'s `IOptionsSnapshot<T>`.\n *\n * Registered as a scoped service, so the value is resolved once per request\n * (ServerKit mints a scoped container per request in `serverKitContextMiddleware`)\n * and stays constant for the duration of that request, while picking up the\n * latest reloaded value at the start of the next one.\n *\n * Declare a per-section token by subclassing:\n *\n * ```ts\n * @Injectable() export abstract class SlackOptionsSnapshot extends AppConfigOptionsSnapshot<SlackConfig> {}\n * ```\n *\n * @template T - The shape of the configuration section.\n */\n@Injectable()\nexport abstract class AppConfigOptionsSnapshot<T> {\n /** The configuration value for the current request scope. */\n abstract readonly value: T;\n}\n\n/**\n * Live accessor for a configuration section — the ServerKit analog of C#'s\n * `IOptionsMonitor<T>`.\n *\n * Registered as a singleton whose `current` value is swapped in place whenever\n * the underlying {@link AppConfigStore} reloads. Singletons should read\n * `current` at use-time (never cache it in a field) so they always observe the\n * latest value, and may subscribe via {@link AppConfigOptionsMonitor.onChange}\n * to actively react to a change — for example to rebuild a connection pool or\n * reconnect a client when a secret rotates.\n *\n * Declare a per-section token by subclassing:\n *\n * ```ts\n * @Injectable() export abstract class SlackOptionsMonitor extends AppConfigOptionsMonitor<SlackConfig> {}\n * ```\n *\n * @template T - The shape of the configuration section.\n */\n@Injectable()\nexport abstract class AppConfigOptionsMonitor<T> {\n /** The latest configuration value. Read at use-time; do not cache in a field. */\n abstract readonly current: T;\n\n /**\n * Subscribes to value changes.\n *\n * The listener is invoked after a reload swaps in a structurally different\n * value; reloads that produce an identical value do not fire it. The listener\n * may be async — its rejection is reported and isolated so it cannot break the\n * swap or other listeners.\n *\n * @param listener - Called with the new value after each change.\n * @returns A function that removes the listener when called.\n */\n abstract onChange(listener: (value: T) => void | Promise<void>): () => void;\n}\n","import { AppConfig } from '../app.config.js';\nimport { AppConfigBuilder } from '../app.config.builder.js';\n\n/**\n * A subscriber notified whenever the store swaps in a freshly built config.\n *\n * @template TRoot - The root configuration type.\n */\nexport type AppConfigStoreListener<TRoot> = (config: AppConfig<TRoot>) => void;\n\n/**\n * Holds the current {@link AppConfig} and can rebuild it on demand, broadcasting\n * the swap to subscribers.\n *\n * `AppConfig` itself is immutable; this store is the single mutable source of\n * truth for \"which config is current\". A reload re-runs the full\n * {@link AppConfigBuilder} pipeline — re-reading every source and re-resolving\n * every provider (including GCP/AWS Secret Manager references) — so a rotated\n * secret is picked up without restarting the process.\n *\n * The store delivers only the `reload()` primitive and its notification fan-out;\n * the trigger that decides *when* to reload (a timer, a GCP Pub-Sub message, an\n * AWS EventBridge event) lives in the consuming application.\n *\n * @template TRoot - The root configuration type produced by the builder.\n *\n * @example\n * ```typescript\n * const builder = new AppConfigBuilder().addSource(...).addProvider(...);\n * const store = new AppConfigStore(builder, await builder.build<RootConfig>());\n *\n * // later, from a watch trigger:\n * await store.reload().catch(err => logger.error('config reload failed', err));\n * ```\n */\nexport class AppConfigStore<TRoot = Record<string, unknown>> {\n private config: AppConfig<TRoot>;\n private readonly listeners = new Set<AppConfigStoreListener<TRoot>>();\n\n /**\n * Creates a store seeded with an already-built config.\n *\n * @param builder - The builder used to rebuild the config on each `reload()`.\n * Pass the same builder instance that produced `initial`.\n * @param initial - The config to serve until the first successful reload.\n */\n constructor(\n private readonly builder: AppConfigBuilder,\n initial: AppConfig<TRoot>,\n ) {\n this.config = initial;\n }\n\n /**\n * The config currently in effect.\n */\n get current(): AppConfig<TRoot> {\n return this.config;\n }\n\n /**\n * Rebuilds the config and, on success, swaps it in and notifies subscribers.\n *\n * The new config is built fully before anything is swapped, so a failed\n * rebuild (e.g. a secret that is momentarily unresolvable) leaves the current\n * config untouched and the error is rethrown for the caller to log. This keeps\n * a running process on its last-good values rather than crashing it — unlike\n * boot, where a build failure is meant to stop startup.\n *\n * @returns A promise that resolves once the swap and notifications complete.\n * @throws Propagates any error thrown while building the new config; the\n * current config is left in place.\n */\n async reload(): Promise<void> {\n const next = await this.builder.build<TRoot>();\n this.config = next;\n for (const listener of this.listeners) {\n listener(next);\n }\n }\n\n /**\n * Subscribes to config swaps.\n *\n * @param listener - Called with the new config after each successful reload.\n * @returns A function that removes the listener when called.\n */\n subscribe(listener: AppConfigStoreListener<TRoot>): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n}\n","import { Logger } from '@maroonedsoftware/logger';\nimport { structurallyEqual } from '../helpers.js';\nimport { AppConfigOptionsMonitor } from './app.config.options.js';\n\n/**\n * Concrete {@link AppConfigOptionsMonitor} backing the live (singleton) options\n * tier.\n *\n * Created and fed by {@link AppConfigOptionsManager}: it holds the latest value\n * for one configuration section and {@link AppConfigOptionsManager} calls\n * {@link AppConfigOptionsMonitorImpl.update} whenever the underlying store\n * reloads.\n *\n * @template T - The shape of the configuration section.\n */\nexport class AppConfigOptionsMonitorImpl<T> extends AppConfigOptionsMonitor<T> {\n private value: T;\n private readonly listeners = new Set<(value: T) => void | Promise<void>>();\n\n /**\n * @param initial - The value to serve until the first {@link AppConfigOptionsMonitorImpl.update}.\n * @param logger - Used to report listener failures.\n */\n constructor(\n initial: T,\n private readonly logger: Logger,\n ) {\n super();\n this.value = initial;\n }\n\n /**\n * The latest value for this section.\n */\n get current(): T {\n return this.value;\n }\n\n /**\n * Subscribes to value changes. See {@link AppConfigOptionsMonitor.onChange}.\n *\n * @param listener - Called with the new value after each change.\n * @returns A function that removes the listener when called.\n */\n onChange(listener: (value: T) => void | Promise<void>): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Swaps in a new value and notifies listeners.\n *\n * A structurally-equal value is ignored so a secret re-fetched unchanged does\n * not bounce live consumers. The value is swapped before listeners run, so a\n * listener reading `current` sees the new value. Each listener is invoked in\n * isolation: a throw or rejection is reported via the logger and does not stop\n * the swap or the other listeners.\n *\n * @param next - The newly resolved value for this section.\n */\n update(next: T): void {\n if (structurallyEqual(this.value, next)) {\n return;\n }\n this.value = next;\n for (const listener of this.listeners) {\n Promise.resolve()\n .then(() => listener(next))\n .catch((err: unknown) => this.logger.error('AppConfigOptionsMonitor: onChange listener failed', err));\n }\n }\n}\n","import { Logger } from '@maroonedsoftware/logger';\nimport { AppConfigOptions, AppConfigOptionsMonitor } from './app.config.options.js';\nimport { AppConfigOptionsMonitorImpl } from './app.config.options.monitor.js';\nimport { AppConfigStore } from './app.config.store.js';\n\n/**\n * Owns the live options monitors for a config and keeps them in sync with an\n * {@link AppConfigStore}.\n *\n * On construction it subscribes to the store; whenever the store reloads, every\n * monitor it has handed out is updated with its section's latest value (sliced\n * via `AppConfig.getAs`). Monitors are created lazily and cached per section, so\n * repeated calls to {@link AppConfigOptionsManager.monitor} for the same key\n * return the same instance.\n *\n * @template TRoot - The root configuration type held by the store.\n *\n * @example\n * ```typescript\n * const store = new AppConfigStore(builder, await builder.build<RootConfig>());\n * const manager = new AppConfigOptionsManager(store, logger);\n * const slack = manager.monitor('slack');\n * slack.current; // latest SlackConfig, updated on every store.reload()\n * ```\n */\nexport class AppConfigOptionsManager<TRoot = Record<string, unknown>> {\n private readonly monitors = new Map<keyof TRoot, AppConfigOptionsMonitorImpl<unknown>>();\n\n /**\n * @param store - The reloadable config store to track.\n * @param logger - Passed to each monitor for listener-error reporting.\n */\n constructor(\n private readonly store: AppConfigStore<TRoot>,\n private readonly logger: Logger,\n ) {\n this.store.subscribe(config => {\n for (const [key, monitor] of this.monitors) {\n monitor.update(config.getAs(key));\n }\n });\n }\n\n /**\n * Returns the live monitor for a section, creating it on first use.\n *\n * @param key - The configuration section key.\n * @returns A monitor whose `current` tracks the latest value for `key`.\n */\n monitor<K extends keyof TRoot>(key: K): AppConfigOptionsMonitor<TRoot[K]> {\n let monitor = this.monitors.get(key);\n if (!monitor) {\n monitor = new AppConfigOptionsMonitorImpl<unknown>(this.store.current.getAs(key), this.logger);\n this.monitors.set(key, monitor);\n }\n return monitor as unknown as AppConfigOptionsMonitor<TRoot[K]>;\n }\n\n /**\n * Returns a boot-snapshot accessor for a section.\n *\n * The value is read from the config current at call time and never updated —\n * the static (`IOptions`) tier. Call this during registration so the captured\n * value is the one in effect at container-build time.\n *\n * @param key - The configuration section key.\n * @returns An {@link AppConfigOptions} holding the section's snapshot value.\n */\n options<K extends keyof TRoot>(key: K): AppConfigOptions<TRoot[K]> {\n return { value: this.store.current.getAs<TRoot[K]>(key) };\n }\n}\n","import { Identifier, Registry } from 'injectkit';\nimport { AppConfigOptions, AppConfigOptionsMonitor, AppConfigOptionsSnapshot } from './app.config.options.js';\nimport { AppConfigOptionsManager } from './app.config.options.manager.js';\nimport { AppConfigStore } from './app.config.store.js';\n\n/**\n * The DI tokens to register for a configuration section, one per options tier.\n *\n * Each token is the abstract class a section declares by subclassing the\n * corresponding base (e.g. `class SlackOptionsMonitor extends AppConfigOptionsMonitor<SlackConfig> {}`).\n * Pass only the tiers a section actually uses.\n *\n * @template T - The shape of the configuration section.\n */\nexport interface AppConfigOptionsTokens<T> {\n /** Token for the static boot-snapshot tier ({@link AppConfigOptions}). */\n options?: Identifier<AppConfigOptions<T>>;\n /** Token for the per-request scoped tier ({@link AppConfigOptionsSnapshot}). */\n snapshot?: Identifier<AppConfigOptionsSnapshot<T>>;\n /** Token for the live singleton tier ({@link AppConfigOptionsMonitor}). */\n monitor?: Identifier<AppConfigOptionsMonitor<T>>;\n}\n\n/**\n * Wires the requested options tiers for one configuration section into a registry.\n *\n * - `options` is registered as a singleton boot snapshot.\n * - `monitor` is registered as the singleton live monitor owned by the manager.\n * - `snapshot` is registered as a scoped factory, so each request scope resolves\n * the value current at the start of that request.\n *\n * Uses the standard InjectKit registration API (`register(token).useInstance(...)`\n * / `.useFactory(...).asScoped()`), so consumers inject the tokens the same way\n * they inject any other service.\n *\n * @template TRoot - The root configuration type.\n * @template K - The section key within `TRoot`.\n * @param registry - The registry to register into (before `build()`).\n * @param store - The reloadable store (read by the scoped snapshot factory).\n * @param manager - The manager that owns the live monitor and boot snapshot.\n * @param key - The configuration section key.\n * @param tokens - The per-tier tokens to register; omit a tier to skip it.\n *\n * @example\n * ```typescript\n * registerAppConfigOptions(registry, store, manager, 'slack', {\n * monitor: SlackOptionsMonitor,\n * snapshot: SlackOptionsSnapshot,\n * });\n * ```\n */\nexport function registerAppConfigOptions<TRoot, K extends keyof TRoot & string>(\n registry: Registry,\n store: AppConfigStore<TRoot>,\n manager: AppConfigOptionsManager<TRoot>,\n key: K,\n tokens: AppConfigOptionsTokens<TRoot[K]>,\n): void {\n if (tokens.options) {\n registry.register(tokens.options).useInstance(manager.options(key));\n }\n if (tokens.monitor) {\n registry.register(tokens.monitor).useInstance(manager.monitor(key));\n }\n if (tokens.snapshot) {\n registry\n .register(tokens.snapshot)\n .useFactory(() => ({ value: store.current.getAs<TRoot[K]>(key) }))\n .asScoped();\n }\n}\n"],"mappings":"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","names":["AppConfig","config","get","key","getAs","getString","String","getNumber","Number","getBoolean","Boolean","getObject","deepmerge","objectVisitor","obj","callback","visit","path","owner","propertyPath","arrayIndex","Array","isArray","forEach","item","index","entries","Object","entry","length","propertyType","AppConfigBuilder","sources","providers","addSource","source","push","addProvider","provider","build","sourceTasks","Promise","all","map","x","load","mergedConfig","length","deepmerge","tasks","parse","value","meta","find","canParse","objectVisitor","AppConfig","tryParseJson","text","JSON","parse","_","value","structurallyEqual","a","b","Array","isArray","length","every","item","index","aRecord","bRecord","aKeys","Object","keys","bKeys","key","prototype","hasOwnProperty","call","nestKeys","record","separator","result","entries","parts","split","current","i","part","dotenv","AppConfigSourceDotenv","filePath","options","load","result","dotenv","config","path","quiet","error","parsed","groupSeparator","nestKeys","existsSync","readFile","AppConfigSourceJson","options","filePath","ignoreMissingFile","encoding","load","existsSync","file","readFile","JSON","parse","toString","existsSync","readFile","YAML","AppConfigSourceYaml","options","filePath","ignoreMissingFile","encoding","load","existsSync","file","readFile","YAML","parse","toString","AppConfigProviderDotenv","prefix","RegExp","canParse","value","lastIndex","test","parse","meta","matches","matchAll","result","found","key","replaceAll","process","env","arrayIndex","undefined","Array","isArray","owner","tryParseJson","propertyPath","Injectable","SecretManagerServiceClient","ServerkitError","AppConfigProviderGcpSecrets","secretmanagerClient","SecretManagerServiceClient","prefix","projectId","RegExp","canParse","value","lastIndex","test","getSecret","secretId","secret","accessSecretVersion","name","payload","data","toString","error","ServerkitError","withCause","withInternalDetails","parse","meta","tasks","matches","matchAll","key","task","then","arrayIndex","undefined","Array","isArray","owner","tryParseJson","propertyPath","push","Promise","all","Injectable","GetSecretValueCommand","SecretsManagerClient","ServerkitError","AppConfigProviderAwsSecrets","secretsManagerClient","prefix","region","SecretsManagerClient","RegExp","canParse","value","lastIndex","test","getSecret","secretId","response","send","GetSecretValueCommand","SecretId","SecretString","undefined","SecretBinary","Buffer","from","toString","error","ServerkitError","withCause","withInternalDetails","parse","meta","tasks","matches","matchAll","key","task","then","arrayIndex","Array","isArray","owner","tryParseJson","propertyPath","push","Promise","all","Injectable","AppConfigOptions","AppConfigOptionsSnapshot","AppConfigOptionsMonitor","AppConfigStore","config","listeners","Set","builder","initial","current","reload","next","build","listener","subscribe","add","delete","AppConfigOptionsMonitorImpl","AppConfigOptionsMonitor","value","listeners","Set","initial","logger","current","onChange","listener","add","delete","update","next","structurallyEqual","Promise","resolve","then","catch","err","error","AppConfigOptionsManager","monitors","Map","store","logger","subscribe","config","key","monitor","update","getAs","get","AppConfigOptionsMonitorImpl","current","set","options","value","registerAppConfigOptions","registry","store","manager","key","tokens","options","register","useInstance","monitor","snapshot","useFactory","value","current","getAs","asScoped"]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessor for a boot-time snapshot of a configuration section — the ServerKit
|
|
3
|
+
* analog of C#'s `IOptions<T>`.
|
|
4
|
+
*
|
|
5
|
+
* The value is captured once when the container is built and never changes for
|
|
6
|
+
* the lifetime of the process. This is the same guarantee as injecting a typed
|
|
7
|
+
* config object directly (e.g. `SlackConfig`); use it when a consumer never needs
|
|
8
|
+
* to observe a reload.
|
|
9
|
+
*
|
|
10
|
+
* Declared as an abstract `@Injectable()` class so it can serve as a DI token.
|
|
11
|
+
* Because InjectKit resolves by a runtime class identity (and generic type
|
|
12
|
+
* arguments erase), each configuration section declares its own token by
|
|
13
|
+
* subclassing this base — mirroring how `SlackConfig` / `Logger` are modeled:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* @Injectable() export abstract class SlackOptions extends AppConfigOptions<SlackConfig> {}
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @template T - The shape of the configuration section.
|
|
20
|
+
*/
|
|
21
|
+
export declare abstract class AppConfigOptions<T> {
|
|
22
|
+
/** The configuration value captured at container-build time. */
|
|
23
|
+
abstract readonly value: T;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Accessor for a per-request-stable view of a configuration section — the
|
|
27
|
+
* ServerKit analog of C#'s `IOptionsSnapshot<T>`.
|
|
28
|
+
*
|
|
29
|
+
* Registered as a scoped service, so the value is resolved once per request
|
|
30
|
+
* (ServerKit mints a scoped container per request in `serverKitContextMiddleware`)
|
|
31
|
+
* and stays constant for the duration of that request, while picking up the
|
|
32
|
+
* latest reloaded value at the start of the next one.
|
|
33
|
+
*
|
|
34
|
+
* Declare a per-section token by subclassing:
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* @Injectable() export abstract class SlackOptionsSnapshot extends AppConfigOptionsSnapshot<SlackConfig> {}
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @template T - The shape of the configuration section.
|
|
41
|
+
*/
|
|
42
|
+
export declare abstract class AppConfigOptionsSnapshot<T> {
|
|
43
|
+
/** The configuration value for the current request scope. */
|
|
44
|
+
abstract readonly value: T;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Live accessor for a configuration section — the ServerKit analog of C#'s
|
|
48
|
+
* `IOptionsMonitor<T>`.
|
|
49
|
+
*
|
|
50
|
+
* Registered as a singleton whose `current` value is swapped in place whenever
|
|
51
|
+
* the underlying {@link AppConfigStore} reloads. Singletons should read
|
|
52
|
+
* `current` at use-time (never cache it in a field) so they always observe the
|
|
53
|
+
* latest value, and may subscribe via {@link AppConfigOptionsMonitor.onChange}
|
|
54
|
+
* to actively react to a change — for example to rebuild a connection pool or
|
|
55
|
+
* reconnect a client when a secret rotates.
|
|
56
|
+
*
|
|
57
|
+
* Declare a per-section token by subclassing:
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* @Injectable() export abstract class SlackOptionsMonitor extends AppConfigOptionsMonitor<SlackConfig> {}
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @template T - The shape of the configuration section.
|
|
64
|
+
*/
|
|
65
|
+
export declare abstract class AppConfigOptionsMonitor<T> {
|
|
66
|
+
/** The latest configuration value. Read at use-time; do not cache in a field. */
|
|
67
|
+
abstract readonly current: T;
|
|
68
|
+
/**
|
|
69
|
+
* Subscribes to value changes.
|
|
70
|
+
*
|
|
71
|
+
* The listener is invoked after a reload swaps in a structurally different
|
|
72
|
+
* value; reloads that produce an identical value do not fire it. The listener
|
|
73
|
+
* may be async — its rejection is reported and isolated so it cannot break the
|
|
74
|
+
* swap or other listeners.
|
|
75
|
+
*
|
|
76
|
+
* @param listener - Called with the new value after each change.
|
|
77
|
+
* @returns A function that removes the listener when called.
|
|
78
|
+
*/
|
|
79
|
+
abstract onChange(listener: (value: T) => void | Promise<void>): () => void;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=app.config.options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.config.options.d.ts","sourceRoot":"","sources":["../../src/options/app.config.options.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,8BACsB,gBAAgB,CAAC,CAAC;IACtC,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,8BACsB,wBAAwB,CAAC,CAAC;IAC9C,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,8BACsB,uBAAuB,CAAC,CAAC;IAC7C,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IAE7B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI;CAC5E"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Logger } from '@maroonedsoftware/logger';
|
|
2
|
+
import { AppConfigOptions, AppConfigOptionsMonitor } from './app.config.options.js';
|
|
3
|
+
import { AppConfigStore } from './app.config.store.js';
|
|
4
|
+
/**
|
|
5
|
+
* Owns the live options monitors for a config and keeps them in sync with an
|
|
6
|
+
* {@link AppConfigStore}.
|
|
7
|
+
*
|
|
8
|
+
* On construction it subscribes to the store; whenever the store reloads, every
|
|
9
|
+
* monitor it has handed out is updated with its section's latest value (sliced
|
|
10
|
+
* via `AppConfig.getAs`). Monitors are created lazily and cached per section, so
|
|
11
|
+
* repeated calls to {@link AppConfigOptionsManager.monitor} for the same key
|
|
12
|
+
* return the same instance.
|
|
13
|
+
*
|
|
14
|
+
* @template TRoot - The root configuration type held by the store.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const store = new AppConfigStore(builder, await builder.build<RootConfig>());
|
|
19
|
+
* const manager = new AppConfigOptionsManager(store, logger);
|
|
20
|
+
* const slack = manager.monitor('slack');
|
|
21
|
+
* slack.current; // latest SlackConfig, updated on every store.reload()
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare class AppConfigOptionsManager<TRoot = Record<string, unknown>> {
|
|
25
|
+
private readonly store;
|
|
26
|
+
private readonly logger;
|
|
27
|
+
private readonly monitors;
|
|
28
|
+
/**
|
|
29
|
+
* @param store - The reloadable config store to track.
|
|
30
|
+
* @param logger - Passed to each monitor for listener-error reporting.
|
|
31
|
+
*/
|
|
32
|
+
constructor(store: AppConfigStore<TRoot>, logger: Logger);
|
|
33
|
+
/**
|
|
34
|
+
* Returns the live monitor for a section, creating it on first use.
|
|
35
|
+
*
|
|
36
|
+
* @param key - The configuration section key.
|
|
37
|
+
* @returns A monitor whose `current` tracks the latest value for `key`.
|
|
38
|
+
*/
|
|
39
|
+
monitor<K extends keyof TRoot>(key: K): AppConfigOptionsMonitor<TRoot[K]>;
|
|
40
|
+
/**
|
|
41
|
+
* Returns a boot-snapshot accessor for a section.
|
|
42
|
+
*
|
|
43
|
+
* The value is read from the config current at call time and never updated —
|
|
44
|
+
* the static (`IOptions`) tier. Call this during registration so the captured
|
|
45
|
+
* value is the one in effect at container-build time.
|
|
46
|
+
*
|
|
47
|
+
* @param key - The configuration section key.
|
|
48
|
+
* @returns An {@link AppConfigOptions} holding the section's snapshot value.
|
|
49
|
+
*/
|
|
50
|
+
options<K extends keyof TRoot>(key: K): AppConfigOptions<TRoot[K]>;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=app.config.options.manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.config.options.manager.d.ts","sourceRoot":"","sources":["../../src/options/app.config.options.manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAEpF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,uBAAuB,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAQhE,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IARzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgE;IAEzF;;;OAGG;gBAEgB,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,EAC5B,MAAM,EAAE,MAAM;IASjC;;;;;OAKG;IACH,OAAO,CAAC,CAAC,SAAS,MAAM,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,uBAAuB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IASzE;;;;;;;;;OASG;IACH,OAAO,CAAC,CAAC,SAAS,MAAM,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;CAGnE"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Logger } from '@maroonedsoftware/logger';
|
|
2
|
+
import { AppConfigOptionsMonitor } from './app.config.options.js';
|
|
3
|
+
/**
|
|
4
|
+
* Concrete {@link AppConfigOptionsMonitor} backing the live (singleton) options
|
|
5
|
+
* tier.
|
|
6
|
+
*
|
|
7
|
+
* Created and fed by {@link AppConfigOptionsManager}: it holds the latest value
|
|
8
|
+
* for one configuration section and {@link AppConfigOptionsManager} calls
|
|
9
|
+
* {@link AppConfigOptionsMonitorImpl.update} whenever the underlying store
|
|
10
|
+
* reloads.
|
|
11
|
+
*
|
|
12
|
+
* @template T - The shape of the configuration section.
|
|
13
|
+
*/
|
|
14
|
+
export declare class AppConfigOptionsMonitorImpl<T> extends AppConfigOptionsMonitor<T> {
|
|
15
|
+
private readonly logger;
|
|
16
|
+
private value;
|
|
17
|
+
private readonly listeners;
|
|
18
|
+
/**
|
|
19
|
+
* @param initial - The value to serve until the first {@link AppConfigOptionsMonitorImpl.update}.
|
|
20
|
+
* @param logger - Used to report listener failures.
|
|
21
|
+
*/
|
|
22
|
+
constructor(initial: T, logger: Logger);
|
|
23
|
+
/**
|
|
24
|
+
* The latest value for this section.
|
|
25
|
+
*/
|
|
26
|
+
get current(): T;
|
|
27
|
+
/**
|
|
28
|
+
* Subscribes to value changes. See {@link AppConfigOptionsMonitor.onChange}.
|
|
29
|
+
*
|
|
30
|
+
* @param listener - Called with the new value after each change.
|
|
31
|
+
* @returns A function that removes the listener when called.
|
|
32
|
+
*/
|
|
33
|
+
onChange(listener: (value: T) => void | Promise<void>): () => void;
|
|
34
|
+
/**
|
|
35
|
+
* Swaps in a new value and notifies listeners.
|
|
36
|
+
*
|
|
37
|
+
* A structurally-equal value is ignored so a secret re-fetched unchanged does
|
|
38
|
+
* not bounce live consumers. The value is swapped before listeners run, so a
|
|
39
|
+
* listener reading `current` sees the new value. Each listener is invoked in
|
|
40
|
+
* isolation: a throw or rejection is reported via the logger and does not stop
|
|
41
|
+
* the swap or the other listeners.
|
|
42
|
+
*
|
|
43
|
+
* @param next - The newly resolved value for this section.
|
|
44
|
+
*/
|
|
45
|
+
update(next: T): void;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=app.config.options.monitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.config.options.monitor.d.ts","sourceRoot":"","sources":["../../src/options/app.config.options.monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAElD,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAElE;;;;;;;;;;GAUG;AACH,qBAAa,2BAA2B,CAAC,CAAC,CAAE,SAAQ,uBAAuB,CAAC,CAAC,CAAC;IAU1E,OAAO,CAAC,QAAQ,CAAC,MAAM;IATzB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiD;IAE3E;;;OAGG;gBAED,OAAO,EAAE,CAAC,EACO,MAAM,EAAE,MAAM;IAMjC;;OAEG;IACH,IAAI,OAAO,IAAI,CAAC,CAEf;IAED;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI;IAOlE;;;;;;;;;;OAUG;IACH,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI;CAWtB"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Identifier, Registry } from 'injectkit';
|
|
2
|
+
import { AppConfigOptions, AppConfigOptionsMonitor, AppConfigOptionsSnapshot } from './app.config.options.js';
|
|
3
|
+
import { AppConfigOptionsManager } from './app.config.options.manager.js';
|
|
4
|
+
import { AppConfigStore } from './app.config.store.js';
|
|
5
|
+
/**
|
|
6
|
+
* The DI tokens to register for a configuration section, one per options tier.
|
|
7
|
+
*
|
|
8
|
+
* Each token is the abstract class a section declares by subclassing the
|
|
9
|
+
* corresponding base (e.g. `class SlackOptionsMonitor extends AppConfigOptionsMonitor<SlackConfig> {}`).
|
|
10
|
+
* Pass only the tiers a section actually uses.
|
|
11
|
+
*
|
|
12
|
+
* @template T - The shape of the configuration section.
|
|
13
|
+
*/
|
|
14
|
+
export interface AppConfigOptionsTokens<T> {
|
|
15
|
+
/** Token for the static boot-snapshot tier ({@link AppConfigOptions}). */
|
|
16
|
+
options?: Identifier<AppConfigOptions<T>>;
|
|
17
|
+
/** Token for the per-request scoped tier ({@link AppConfigOptionsSnapshot}). */
|
|
18
|
+
snapshot?: Identifier<AppConfigOptionsSnapshot<T>>;
|
|
19
|
+
/** Token for the live singleton tier ({@link AppConfigOptionsMonitor}). */
|
|
20
|
+
monitor?: Identifier<AppConfigOptionsMonitor<T>>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Wires the requested options tiers for one configuration section into a registry.
|
|
24
|
+
*
|
|
25
|
+
* - `options` is registered as a singleton boot snapshot.
|
|
26
|
+
* - `monitor` is registered as the singleton live monitor owned by the manager.
|
|
27
|
+
* - `snapshot` is registered as a scoped factory, so each request scope resolves
|
|
28
|
+
* the value current at the start of that request.
|
|
29
|
+
*
|
|
30
|
+
* Uses the standard InjectKit registration API (`register(token).useInstance(...)`
|
|
31
|
+
* / `.useFactory(...).asScoped()`), so consumers inject the tokens the same way
|
|
32
|
+
* they inject any other service.
|
|
33
|
+
*
|
|
34
|
+
* @template TRoot - The root configuration type.
|
|
35
|
+
* @template K - The section key within `TRoot`.
|
|
36
|
+
* @param registry - The registry to register into (before `build()`).
|
|
37
|
+
* @param store - The reloadable store (read by the scoped snapshot factory).
|
|
38
|
+
* @param manager - The manager that owns the live monitor and boot snapshot.
|
|
39
|
+
* @param key - The configuration section key.
|
|
40
|
+
* @param tokens - The per-tier tokens to register; omit a tier to skip it.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* registerAppConfigOptions(registry, store, manager, 'slack', {
|
|
45
|
+
* monitor: SlackOptionsMonitor,
|
|
46
|
+
* snapshot: SlackOptionsSnapshot,
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function registerAppConfigOptions<TRoot, K extends keyof TRoot & string>(registry: Registry, store: AppConfigStore<TRoot>, manager: AppConfigOptionsManager<TRoot>, key: K, tokens: AppConfigOptionsTokens<TRoot[K]>): void;
|
|
51
|
+
//# sourceMappingURL=app.config.options.registration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.config.options.registration.d.ts","sourceRoot":"","sources":["../../src/options/app.config.options.registration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAC9G,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD;;;;;;;;GAQG;AACH,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,0EAA0E;IAC1E,OAAO,CAAC,EAAE,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1C,gFAAgF;IAChF,QAAQ,CAAC,EAAE,UAAU,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,2EAA2E;IAC3E,OAAO,CAAC,EAAE,UAAU,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CAAC;CAClD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,CAAC,SAAS,MAAM,KAAK,GAAG,MAAM,EAC5E,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,EAC5B,OAAO,EAAE,uBAAuB,CAAC,KAAK,CAAC,EACvC,GAAG,EAAE,CAAC,EACN,MAAM,EAAE,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GACvC,IAAI,CAaN"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { AppConfig } from '../app.config.js';
|
|
2
|
+
import { AppConfigBuilder } from '../app.config.builder.js';
|
|
3
|
+
/**
|
|
4
|
+
* A subscriber notified whenever the store swaps in a freshly built config.
|
|
5
|
+
*
|
|
6
|
+
* @template TRoot - The root configuration type.
|
|
7
|
+
*/
|
|
8
|
+
export type AppConfigStoreListener<TRoot> = (config: AppConfig<TRoot>) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Holds the current {@link AppConfig} and can rebuild it on demand, broadcasting
|
|
11
|
+
* the swap to subscribers.
|
|
12
|
+
*
|
|
13
|
+
* `AppConfig` itself is immutable; this store is the single mutable source of
|
|
14
|
+
* truth for "which config is current". A reload re-runs the full
|
|
15
|
+
* {@link AppConfigBuilder} pipeline — re-reading every source and re-resolving
|
|
16
|
+
* every provider (including GCP/AWS Secret Manager references) — so a rotated
|
|
17
|
+
* secret is picked up without restarting the process.
|
|
18
|
+
*
|
|
19
|
+
* The store delivers only the `reload()` primitive and its notification fan-out;
|
|
20
|
+
* the trigger that decides *when* to reload (a timer, a GCP Pub-Sub message, an
|
|
21
|
+
* AWS EventBridge event) lives in the consuming application.
|
|
22
|
+
*
|
|
23
|
+
* @template TRoot - The root configuration type produced by the builder.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const builder = new AppConfigBuilder().addSource(...).addProvider(...);
|
|
28
|
+
* const store = new AppConfigStore(builder, await builder.build<RootConfig>());
|
|
29
|
+
*
|
|
30
|
+
* // later, from a watch trigger:
|
|
31
|
+
* await store.reload().catch(err => logger.error('config reload failed', err));
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare class AppConfigStore<TRoot = Record<string, unknown>> {
|
|
35
|
+
private readonly builder;
|
|
36
|
+
private config;
|
|
37
|
+
private readonly listeners;
|
|
38
|
+
/**
|
|
39
|
+
* Creates a store seeded with an already-built config.
|
|
40
|
+
*
|
|
41
|
+
* @param builder - The builder used to rebuild the config on each `reload()`.
|
|
42
|
+
* Pass the same builder instance that produced `initial`.
|
|
43
|
+
* @param initial - The config to serve until the first successful reload.
|
|
44
|
+
*/
|
|
45
|
+
constructor(builder: AppConfigBuilder, initial: AppConfig<TRoot>);
|
|
46
|
+
/**
|
|
47
|
+
* The config currently in effect.
|
|
48
|
+
*/
|
|
49
|
+
get current(): AppConfig<TRoot>;
|
|
50
|
+
/**
|
|
51
|
+
* Rebuilds the config and, on success, swaps it in and notifies subscribers.
|
|
52
|
+
*
|
|
53
|
+
* The new config is built fully before anything is swapped, so a failed
|
|
54
|
+
* rebuild (e.g. a secret that is momentarily unresolvable) leaves the current
|
|
55
|
+
* config untouched and the error is rethrown for the caller to log. This keeps
|
|
56
|
+
* a running process on its last-good values rather than crashing it — unlike
|
|
57
|
+
* boot, where a build failure is meant to stop startup.
|
|
58
|
+
*
|
|
59
|
+
* @returns A promise that resolves once the swap and notifications complete.
|
|
60
|
+
* @throws Propagates any error thrown while building the new config; the
|
|
61
|
+
* current config is left in place.
|
|
62
|
+
*/
|
|
63
|
+
reload(): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Subscribes to config swaps.
|
|
66
|
+
*
|
|
67
|
+
* @param listener - Called with the new config after each successful reload.
|
|
68
|
+
* @returns A function that removes the listener when called.
|
|
69
|
+
*/
|
|
70
|
+
subscribe(listener: AppConfigStoreListener<TRoot>): () => void;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=app.config.store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.config.store.d.ts","sourceRoot":"","sources":["../../src/options/app.config.store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAE5D;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,CAAC,KAAK,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,cAAc,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAYvD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAX1B,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4C;IAEtE;;;;;;OAMG;gBAEgB,OAAO,EAAE,gBAAgB,EAC1C,OAAO,EAAE,SAAS,CAAC,KAAK,CAAC;IAK3B;;OAEG;IACH,IAAI,OAAO,IAAI,SAAS,CAAC,KAAK,CAAC,CAE9B;IAED;;;;;;;;;;;;OAYG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ7B;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,EAAE,sBAAsB,CAAC,KAAK,CAAC,GAAG,MAAM,IAAI;CAM/D"}
|