@galaxus/chabis-application-config 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +243 -0
- package/dist/index.d.ts +297 -0
- package/dist/index.js +8672 -0
- package/dist/index.js.map +1 -0
- package/package.json +100 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Digitec Galaxus AG
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# @galaxus/chabis-application-config
|
|
2
|
+
|
|
3
|
+
Type-safe application configuration for Node and TypeScript. Describe your config
|
|
4
|
+
shape with a **zod** or **valibot** schema and get back one fully typed object,
|
|
5
|
+
merged from layered YAML/TOML files and cloud secrets and validated against that
|
|
6
|
+
schema
|
|
7
|
+
|
|
8
|
+
It implements the Chabis `application-config` contract: the same file layering,
|
|
9
|
+
merge semantics, and secret resolution as the Python `chabis-application-config`
|
|
10
|
+
library, so a service keeps one config layout across stacks
|
|
11
|
+
|
|
12
|
+
What you get:
|
|
13
|
+
|
|
14
|
+
- Layered `settings`, `settings.{env}`, and `.secrets` files in YAML or TOML,
|
|
15
|
+
deep-merged in a fixed priority order
|
|
16
|
+
- Secrets from Google Cloud Parameter Manager or Azure Key Vault, filling
|
|
17
|
+
placeholders left in those files
|
|
18
|
+
- GPM versions resolve to the latest automatically, no version pin to bump
|
|
19
|
+
- Environment-variable overrides derived from your schema, no magic prefix
|
|
20
|
+
- A single typed object validated against your schema, or a thrown error
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pnpm add @galaxus/chabis-application-config
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Bring a schema library as a peer dependency, zod or valibot:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pnpm add zod
|
|
32
|
+
# or
|
|
33
|
+
pnpm add valibot @valibot/to-json-schema
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Google Cloud Parameter Manager is included out of the box. Azure Key Vault is
|
|
37
|
+
optional, install its SDKs only if you use an `AzureKeyVault` provider instead:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
pnpm add @azure/identity @azure/keyvault-secrets
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { z } from "zod";
|
|
47
|
+
import { loadConfig } from "@galaxus/chabis-application-config";
|
|
48
|
+
|
|
49
|
+
const Schema = z.object({
|
|
50
|
+
LogLevel:
|
|
51
|
+
z.string().default("INFO"),
|
|
52
|
+
Database:
|
|
53
|
+
z.object({ Url: z.string() }),
|
|
54
|
+
Auth:
|
|
55
|
+
z.object({ ClientId: z.string(), TenantId: z.string(), Secret: z.string() }),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const config = await loadConfig(Schema, {
|
|
59
|
+
// directory holding the settings.*.{yaml,toml} files, defaults to process.cwd()
|
|
60
|
+
settingsDir: "./configs",
|
|
61
|
+
// selects settings.{env}.*, defaults to process.env.ENV ?? "dev"
|
|
62
|
+
env: process.env.ENV ?? "dev",
|
|
63
|
+
});
|
|
64
|
+
// config is fully typed: config.Auth.Secret, config.Database.Url
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Typesafe:
|
|
68
|
+
|
|
69
|
+
<img width="1268" height="635" alt="VSCode Typescript Tooltip" src="https://github.com/user-attachments/assets/0bc6b786-bd8c-48a3-aaf7-f9c31cda94a2" />
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
`loadConfig` is async (secret fetches are async) and memoized by
|
|
73
|
+
`(schema, settingsDir, env)`. Pass `{ fresh: true }` to bypass the cache, or call
|
|
74
|
+
`resetConfigCache()`
|
|
75
|
+
|
|
76
|
+
## Options
|
|
77
|
+
|
|
78
|
+
`loadConfig(schema, opts)` takes an optional second argument:
|
|
79
|
+
|
|
80
|
+
| Field | Type | Default | Purpose |
|
|
81
|
+
| --- | --- | --- | --- |
|
|
82
|
+
| `settingsDir` | `string` | `process.cwd()` | Directory holding the `settings.*.{yaml,toml}` files |
|
|
83
|
+
| `env` | `string` | `process.env.ENV ?? "dev"` | Selects the `settings.{env}.*` layer |
|
|
84
|
+
| `overrides` | `Record<string, unknown>` | `{}` | Highest-priority layer, deep-merged on top of everything |
|
|
85
|
+
| `envNestingDelimiter` | `string` | `"__"` | Separator between schema leaf segments in env-var names |
|
|
86
|
+
| `fresh` | `boolean` | `false` | Bypass the memoized result and force a fresh load |
|
|
87
|
+
|
|
88
|
+
`overrides` and environment variables are two different layers (tiers 1 and 2)
|
|
89
|
+
with two different shapes:
|
|
90
|
+
|
|
91
|
+
- `overrides` is a **nested object** passed in code, `{ Database: { Url: "…" } }`.
|
|
92
|
+
It is the `process.env`-free way to force a value, e.g. in tests
|
|
93
|
+
- Env vars are **flat, `__`-delimited strings**, `Database__Url=…`. See
|
|
94
|
+
[Environment-variable overrides](#environment-variable-overrides)
|
|
95
|
+
|
|
96
|
+
## Files
|
|
97
|
+
|
|
98
|
+
`loadConfig` reads, for a given `settingsDir` and `env`, any of these that exist:
|
|
99
|
+
|
|
100
|
+
| Base name | Purpose |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| `settings.{toml,yaml}` | Base configuration (lowest priority) |
|
|
103
|
+
| `settings.{env}.{toml,yaml}` | Environment-specific overrides |
|
|
104
|
+
| `.secrets.{toml,yaml}` | Local secrets, highest-priority file layer |
|
|
105
|
+
|
|
106
|
+
## How it resolves a value (priority, highest to lowest)
|
|
107
|
+
|
|
108
|
+
The order is 1:1 with the Python `settings_customise_sources`:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
1. opts.overrides
|
|
112
|
+
2. env vars (schema leaf) Database__Url -> { Database: { Url } }
|
|
113
|
+
3. Azure Key Vault secrets (when configured)
|
|
114
|
+
4. Google Parameter Manager secrets
|
|
115
|
+
5. .secrets.{toml,yaml}
|
|
116
|
+
6. settings.{env}.{toml,yaml}
|
|
117
|
+
7. settings.{toml,yaml}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Within the file tiers every TOML file outranks every YAML file
|
|
121
|
+
(`*toml_sources` before `*yaml_sources`), and base-name order is
|
|
122
|
+
`.secrets` > `settings.{env}` > `settings`, so the full file priority, high to low:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
.secrets.toml > settings.{env}.toml > settings.toml >
|
|
126
|
+
.secrets.yaml > settings.{env}.yaml > settings.yaml
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The secret tiers (3 to 4) outrank every settings file (5 to 7), so a
|
|
130
|
+
`"<Configured-in-GoogleParameterManager>"` placeholder written in
|
|
131
|
+
`settings.dev.yaml` is replaced by the real secret payload
|
|
132
|
+
|
|
133
|
+
### Merge semantics
|
|
134
|
+
|
|
135
|
+
Applied lowest to highest (pydantic-settings `deep_update`):
|
|
136
|
+
|
|
137
|
+
- Plain objects deep-merge recursively
|
|
138
|
+
- Every other value, **including arrays**, is replaced wholesale by the
|
|
139
|
+
higher-priority layer, no array concatenation
|
|
140
|
+
|
|
141
|
+
## Environment-variable overrides
|
|
142
|
+
|
|
143
|
+
Selection is schema-driven: the loader walks your schema's leaf paths and looks up
|
|
144
|
+
the env var named by joining each path with the nesting delimiter (`__`). A
|
|
145
|
+
`Database.Url` leaf is overridden by `Database__Url`, mapped back to
|
|
146
|
+
`{ Database: { Url } }`. Matching is case-sensitive against your verbatim keys, so
|
|
147
|
+
there is no prefix and ambient vars like `PATH` never leak in
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
Database__Url=postgres://… node app.js
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Values stay raw strings, coercion to number or bool is the schema's job
|
|
154
|
+
(`z.coerce.number()`). Change the delimiter per call:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
await loadConfig(Schema, { envNestingDelimiter: "." }); // Database.Url=…
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Only leaves declared in the schema are overridable, and only zod and valibot
|
|
161
|
+
schemas can be introspected. For any other vendor, env overrides quietly do not
|
|
162
|
+
apply rather than failing the load
|
|
163
|
+
|
|
164
|
+
`ENV` selects the environment and is **not** itself an override. To force a value
|
|
165
|
+
from code without touching `process.env`, pass a nested object to
|
|
166
|
+
[`overrides`](#options) instead (tier 1, outranks env vars)
|
|
167
|
+
|
|
168
|
+
## The `Secrets` block
|
|
169
|
+
|
|
170
|
+
Any settings file may carry a top-level `Secrets` array. It is metadata, stripped
|
|
171
|
+
before validation, and drives the providers. If more than one file declares it the
|
|
172
|
+
last in execution order wins (TOML before YAML); in practice exactly one file does
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
Secrets:
|
|
176
|
+
- Provider: GoogleParameterManager
|
|
177
|
+
GCPProject: my-project-dev
|
|
178
|
+
GCPParameterName: my-app-config
|
|
179
|
+
# GCPParameterVersion: v5 # OPTIONAL, omit for auto-latest
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- Auth via Application Default Credentials
|
|
183
|
+
- Without `GCPParameterVersion` the provider lists all versions and picks the
|
|
184
|
+
newest `createTime`
|
|
185
|
+
- The payload is parsed JSON-first, YAML-fallback, deep-merged whole at tier 4
|
|
186
|
+
- Any failure (no versions, list/fetch error, unparseable payload) **throws**
|
|
187
|
+
|
|
188
|
+
Azure:
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
Secrets:
|
|
192
|
+
- Provider: AzureKeyVault
|
|
193
|
+
AzureKeyVault: my-vault
|
|
194
|
+
Mode: per-leaf # default, one GET per schema leaf, Auth.Secret -> Auth--Secret
|
|
195
|
+
# Mode: single-blob # one secret holding a JSON/YAML blob
|
|
196
|
+
# SecretName: config # single-blob secret name (default "config")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- `per-leaf` (default): introspect the schema's leaf paths, GET one secret per leaf
|
|
200
|
+
with `.` → `--`, reassemble nested. Missing secrets are skipped (fall through to
|
|
201
|
+
a lower layer)
|
|
202
|
+
- `single-blob`: GET one secret whose payload is a JSON/YAML blob (any vendor)
|
|
203
|
+
- Auth via `DefaultAzureCredential`. SDKs are loaded only when Azure is configured
|
|
204
|
+
|
|
205
|
+
`SecretFiles` is reserved, declaring it throws `NotImplementedProviderError`
|
|
206
|
+
|
|
207
|
+
## Validation
|
|
208
|
+
|
|
209
|
+
Validation runs through the [Standard Schema](https://standardschema.dev)
|
|
210
|
+
interface, so zod, valibot, and arktype all work. On issues a
|
|
211
|
+
`ConfigValidationError` lists each `path: message`, on success the typed
|
|
212
|
+
`result.value` is returned
|
|
213
|
+
|
|
214
|
+
Unknown-key handling is your schema's call: zod strips unknown keys unless you use
|
|
215
|
+
`z.looseObject({...})` or `.loose()`, and any key you want kept must be declared or
|
|
216
|
+
it is stripped
|
|
217
|
+
|
|
218
|
+
Azure `per-leaf` mode reads your schema's leaf paths to derive secret names, which
|
|
219
|
+
works on zod and valibot only (via their JSON Schema emitters). Any other vendor
|
|
220
|
+
must use `Mode: single-blob`
|
|
221
|
+
|
|
222
|
+
## Errors
|
|
223
|
+
|
|
224
|
+
Everything surfaces fast as a thrown subclass of `ChabisConfigError`. The library
|
|
225
|
+
logs nothing
|
|
226
|
+
|
|
227
|
+
| Error | When |
|
|
228
|
+
| --- | --- |
|
|
229
|
+
| `SecretsConfigError` | Malformed `Secrets` block |
|
|
230
|
+
| `ProviderError` | GPM/Azure auth/list/fetch/parse failure (wraps the cause) |
|
|
231
|
+
| `IntrospectionError` | Azure `per-leaf` with a vendor that has no JSON-Schema emitter |
|
|
232
|
+
| `ConfigValidationError` | Standard Schema validation issues |
|
|
233
|
+
| `NotImplementedProviderError` | `SecretFiles` provider requested |
|
|
234
|
+
| `PlaceholderError` | `assertNoPlaceholders` found an unresolved placeholder |
|
|
235
|
+
|
|
236
|
+
`assertNoPlaceholders(config)` is an opt-in helper that throws if any leaf string
|
|
237
|
+
still starts with `"<Configured-in-"`, guarding against a provider key that never
|
|
238
|
+
matched a field
|
|
239
|
+
|
|
240
|
+
## More
|
|
241
|
+
|
|
242
|
+
See [`examples/zod`](examples/zod) and [`examples/valibot`](examples/valibot) for
|
|
243
|
+
runnable setups
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/** The Standard Typed interface. This is a base type extended by other specs. */
|
|
2
|
+
interface StandardTypedV1<Input = unknown, Output = Input> {
|
|
3
|
+
/** The Standard properties. */
|
|
4
|
+
readonly "~standard": StandardTypedV1.Props<Input, Output>;
|
|
5
|
+
}
|
|
6
|
+
declare namespace StandardTypedV1 {
|
|
7
|
+
/** The Standard Typed properties interface. */
|
|
8
|
+
interface Props<Input = unknown, Output = Input> {
|
|
9
|
+
/** The version number of the standard. */
|
|
10
|
+
readonly version: 1;
|
|
11
|
+
/** The vendor name of the schema library. */
|
|
12
|
+
readonly vendor: string;
|
|
13
|
+
/** Inferred types associated with the schema. */
|
|
14
|
+
readonly types?: Types<Input, Output> | undefined;
|
|
15
|
+
}
|
|
16
|
+
/** The Standard Typed types interface. */
|
|
17
|
+
interface Types<Input = unknown, Output = Input> {
|
|
18
|
+
/** The input type of the schema. */
|
|
19
|
+
readonly input: Input;
|
|
20
|
+
/** The output type of the schema. */
|
|
21
|
+
readonly output: Output;
|
|
22
|
+
}
|
|
23
|
+
/** Infers the input type of a Standard Typed. */
|
|
24
|
+
type InferInput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["input"];
|
|
25
|
+
/** Infers the output type of a Standard Typed. */
|
|
26
|
+
type InferOutput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["output"];
|
|
27
|
+
}
|
|
28
|
+
/** The Standard Schema interface. */
|
|
29
|
+
interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
30
|
+
/** The Standard Schema properties. */
|
|
31
|
+
readonly "~standard": StandardSchemaV1.Props<Input, Output>;
|
|
32
|
+
}
|
|
33
|
+
declare namespace StandardSchemaV1 {
|
|
34
|
+
/** The Standard Schema properties interface. */
|
|
35
|
+
interface Props<Input = unknown, Output = Input> extends StandardTypedV1.Props<Input, Output> {
|
|
36
|
+
/** Validates unknown input values. */
|
|
37
|
+
readonly validate: (value: unknown, options?: StandardSchemaV1.Options | undefined) => Result<Output> | Promise<Result<Output>>;
|
|
38
|
+
}
|
|
39
|
+
/** The result interface of the validate function. */
|
|
40
|
+
type Result<Output> = SuccessResult<Output> | FailureResult;
|
|
41
|
+
/** The result interface if validation succeeds. */
|
|
42
|
+
interface SuccessResult<Output> {
|
|
43
|
+
/** The typed output value. */
|
|
44
|
+
readonly value: Output;
|
|
45
|
+
/** A falsy value for `issues` indicates success. */
|
|
46
|
+
readonly issues?: undefined;
|
|
47
|
+
}
|
|
48
|
+
interface Options {
|
|
49
|
+
/** Explicit support for additional vendor-specific parameters, if needed. */
|
|
50
|
+
readonly libraryOptions?: Record<string, unknown> | undefined;
|
|
51
|
+
}
|
|
52
|
+
/** The result interface if validation fails. */
|
|
53
|
+
interface FailureResult {
|
|
54
|
+
/** The issues of failed validation. */
|
|
55
|
+
readonly issues: ReadonlyArray<Issue>;
|
|
56
|
+
}
|
|
57
|
+
/** The issue interface of the failure output. */
|
|
58
|
+
interface Issue {
|
|
59
|
+
/** The error message of the issue. */
|
|
60
|
+
readonly message: string;
|
|
61
|
+
/** The path of the issue, if any. */
|
|
62
|
+
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
|
|
63
|
+
}
|
|
64
|
+
/** The path segment interface of the issue. */
|
|
65
|
+
interface PathSegment {
|
|
66
|
+
/** The key representing a path segment. */
|
|
67
|
+
readonly key: PropertyKey;
|
|
68
|
+
}
|
|
69
|
+
/** The Standard types interface. */
|
|
70
|
+
interface Types<Input = unknown, Output = Input> extends StandardTypedV1.Types<Input, Output> {
|
|
71
|
+
}
|
|
72
|
+
/** Infers the input type of a Standard. */
|
|
73
|
+
type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
|
|
74
|
+
/** Infers the output type of a Standard. */
|
|
75
|
+
type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Orchestration: gather every layer in priority order, deep-merge, validate
|
|
80
|
+
* against the Standard Schema, and memoize the result.
|
|
81
|
+
*
|
|
82
|
+
* Priority chain (HIGHEST to lowest), 1:1 with the Python source order:
|
|
83
|
+
* overrides, env vars, Azure secrets, GPM secrets, settings files (TOML > YAML).
|
|
84
|
+
* The secrets tier outranks every settings file, which is why
|
|
85
|
+
* `"<Configured-in-…>"` placeholders get replaced by the real secret payload
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Recursively optional version of `T`: every nested object field may be
|
|
90
|
+
* omitted, so `overrides` can target a single leaf without restating its
|
|
91
|
+
* siblings. Arrays replace wholesale in the merge (decision #12), so their
|
|
92
|
+
* element type is kept intact rather than made partial
|
|
93
|
+
*/
|
|
94
|
+
type DeepPartial<T> = T extends readonly unknown[] ? T : T extends object ? {
|
|
95
|
+
[K in keyof T]?: DeepPartial<T[K]>;
|
|
96
|
+
} : T;
|
|
97
|
+
interface LoadConfigOptions<S extends StandardSchemaV1 = StandardSchemaV1> {
|
|
98
|
+
/** Directory containing the `settings.*.{yaml,toml}` files. Default `process.cwd()` */
|
|
99
|
+
settingsDir?: string;
|
|
100
|
+
/** Environment selecting the `settings.{env}.*` layer. Default `process.env.ENV ?? "dev"` */
|
|
101
|
+
env?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Highest-priority overrides, deep-merged on top of everything (parity with
|
|
104
|
+
* `init_settings`). Typed as a deep partial of the schema's input since
|
|
105
|
+
* overrides feed the merge before validation and may target any subset
|
|
106
|
+
*/
|
|
107
|
+
overrides?: DeepPartial<StandardSchemaV1.InferInput<S>>;
|
|
108
|
+
/** Delimiter separating schema leaf segments in env-var names. Default `"__"` (`Database__Url`) */
|
|
109
|
+
envNestingDelimiter?: string;
|
|
110
|
+
/** Skip the memoized singleton and force a fresh load. Default `false` */
|
|
111
|
+
fresh?: boolean;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Load, merge, validate, and return the typed config. Throws on any provider
|
|
115
|
+
* error, validation failure, or missing-required field. The override-independent
|
|
116
|
+
* base is memoized by `(schema, settingsDir, env)` unless `opts.fresh`, and
|
|
117
|
+
* `overrides` are applied on top of that cached base on every call
|
|
118
|
+
*/
|
|
119
|
+
declare function loadConfig<S extends StandardSchemaV1>(schema: S, opts?: LoadConfigOptions<S>): Promise<StandardSchemaV1.InferOutput<S>>;
|
|
120
|
+
/** Clear the singleton cache, mainly for tests */
|
|
121
|
+
declare function resetConfigCache(): void;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Secret-provider contract plus the shared JSON-first-then-YAML payload parser
|
|
125
|
+
* used by GPM and Azure single-blob mode (parity with the Python lib and the
|
|
126
|
+
* current `instrumentation.ts`)
|
|
127
|
+
*/
|
|
128
|
+
/** A secret source that returns a nested object to deep-merge at the secrets tier */
|
|
129
|
+
interface SecretProvider {
|
|
130
|
+
/** Resolve secrets into a nested plain object, already shaped like the config */
|
|
131
|
+
load(): Promise<Record<string, unknown>>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Parser for the top-level `Secrets` block. This is metadata, not part of the
|
|
136
|
+
* caller's config schema, so a tiny hand-written validator mirroring the Python
|
|
137
|
+
* `SecretsConfig` model handles it, never the consumer's schema
|
|
138
|
+
*/
|
|
139
|
+
interface GpmSecretsConfig {
|
|
140
|
+
Provider: "GoogleParameterManager";
|
|
141
|
+
GCPProject: string;
|
|
142
|
+
GCPParameterName: string;
|
|
143
|
+
/** Omit for auto-latest resolution */
|
|
144
|
+
GCPParameterVersion?: string;
|
|
145
|
+
}
|
|
146
|
+
interface AzureSecretsConfig {
|
|
147
|
+
Provider: "AzureKeyVault";
|
|
148
|
+
AzureKeyVault: string;
|
|
149
|
+
/** Defaults to `per-leaf` (Python parity). `single-blob` works with any vendor */
|
|
150
|
+
Mode?: "per-leaf" | "single-blob";
|
|
151
|
+
/** Secret holding the whole config blob in `single-blob` mode, default `config` */
|
|
152
|
+
SecretName?: string;
|
|
153
|
+
}
|
|
154
|
+
interface SecretFilesConfig {
|
|
155
|
+
Provider: "SecretFiles";
|
|
156
|
+
SecretFilePath: string;
|
|
157
|
+
}
|
|
158
|
+
type SecretsConfig = GpmSecretsConfig | AzureSecretsConfig | SecretFilesConfig;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Google Parameter Manager provider, mirrors `DgGoogleParameterSecretsSource`.
|
|
162
|
+
*
|
|
163
|
+
* When `GCPParameterVersion` is omitted it lists all versions and picks the
|
|
164
|
+
* newest by `createTime`, replacing the hardcoded `/versions/v5` of the current
|
|
165
|
+
* `instrumentation.ts`. Any failure throws (decision #10), GPM errors are never
|
|
166
|
+
* swallowed
|
|
167
|
+
*/
|
|
168
|
+
|
|
169
|
+
/** Minimal slice of the `@google-cloud/parametermanager` client we rely on */
|
|
170
|
+
interface ParameterVersion {
|
|
171
|
+
name?: string | null;
|
|
172
|
+
createTime?: {
|
|
173
|
+
seconds?: number | string | null;
|
|
174
|
+
nanos?: number | null;
|
|
175
|
+
} | null;
|
|
176
|
+
payload?: {
|
|
177
|
+
data?: Uint8Array | string | null;
|
|
178
|
+
} | null;
|
|
179
|
+
}
|
|
180
|
+
interface ParameterManagerClient {
|
|
181
|
+
parameterPath(project: string, location: string, parameter: string): string;
|
|
182
|
+
listParameterVersions(request: {
|
|
183
|
+
parent: string;
|
|
184
|
+
}): Promise<[ParameterVersion[], unknown, unknown]>;
|
|
185
|
+
getParameterVersion(request: {
|
|
186
|
+
name: string;
|
|
187
|
+
}): Promise<[ParameterVersion, unknown, unknown]>;
|
|
188
|
+
}
|
|
189
|
+
/** Factory for the SDK client, overridable so tests can inject a fake */
|
|
190
|
+
type GpmClientFactory = () => Promise<ParameterManagerClient>;
|
|
191
|
+
declare class GoogleParameterManagerProvider implements SecretProvider {
|
|
192
|
+
private readonly config;
|
|
193
|
+
private readonly clientFactory;
|
|
194
|
+
constructor(config: GpmSecretsConfig, clientFactory?: GpmClientFactory);
|
|
195
|
+
load(): Promise<Record<string, unknown>>;
|
|
196
|
+
private resolveLatestVersion;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Azure Key Vault provider, mirrors `DgAzureKeyvaultSecretsSource`.
|
|
201
|
+
*
|
|
202
|
+
* `per-leaf` (default, Python parity): introspect the schema to enumerate leaf
|
|
203
|
+
* paths, then one GET per leaf mapping `.` to `--` (`Auth.Secret` becomes
|
|
204
|
+
* `Auth--Secret`), reassembling a nested object. Missing secrets are skipped so
|
|
205
|
+
* the field falls through to a lower-priority layer.
|
|
206
|
+
*
|
|
207
|
+
* `single-blob`: fetch one secret whose payload is JSON/YAML (same shape as GPM)
|
|
208
|
+
* and deep-merge it. No introspection, works with any vendor.
|
|
209
|
+
*
|
|
210
|
+
* The Azure SDKs are dynamically imported so GPM-only consumers never load them
|
|
211
|
+
*/
|
|
212
|
+
|
|
213
|
+
/** Minimal slice of `@azure/keyvault-secrets`' `SecretClient` */
|
|
214
|
+
interface AzureSecretClient {
|
|
215
|
+
getSecret(name: string): Promise<{
|
|
216
|
+
value?: string | null;
|
|
217
|
+
}>;
|
|
218
|
+
}
|
|
219
|
+
/** Factory for the vault client, overridable so tests can inject a fake */
|
|
220
|
+
type AzureClientFactory = (vaultName: string) => Promise<AzureSecretClient>;
|
|
221
|
+
declare class AzureKeyVaultProvider implements SecretProvider {
|
|
222
|
+
private readonly config;
|
|
223
|
+
private readonly schema;
|
|
224
|
+
private readonly clientFactory;
|
|
225
|
+
constructor(config: AzureSecretsConfig, schema: StandardSchemaV1, clientFactory?: AzureClientFactory);
|
|
226
|
+
load(): Promise<Record<string, unknown>>;
|
|
227
|
+
private buildClient;
|
|
228
|
+
private loadPerLeaf;
|
|
229
|
+
private loadSingleBlob;
|
|
230
|
+
/** GET one secret, return undefined when missing (parity: swallow + skip) */
|
|
231
|
+
private tryGetSecret;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Error hierarchy. Every failure mode throws (decision #10) so a misconfigured
|
|
236
|
+
* deploy fails fast at boot instead of running on placeholder strings where
|
|
237
|
+
* real secrets should be
|
|
238
|
+
*/
|
|
239
|
+
/** Base class for every error this library throws */
|
|
240
|
+
declare class ChabisConfigError extends Error {
|
|
241
|
+
constructor(message: string, options?: {
|
|
242
|
+
cause?: unknown;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/** Malformed `Secrets` block: missing required provider fields, unknown provider */
|
|
246
|
+
declare class SecretsConfigError extends ChabisConfigError {
|
|
247
|
+
}
|
|
248
|
+
/** A secret provider (GPM/Azure) failed to authenticate, list, fetch, or parse */
|
|
249
|
+
declare class ProviderError extends ChabisConfigError {
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Azure `per-leaf` mode requested with a schema whose vendor has no registered
|
|
253
|
+
* JSON-Schema introspector, so leaf paths can't be enumerated
|
|
254
|
+
*/
|
|
255
|
+
declare class IntrospectionError extends ChabisConfigError {
|
|
256
|
+
}
|
|
257
|
+
/** The merged config failed Standard Schema validation */
|
|
258
|
+
declare class ConfigValidationError extends ChabisConfigError {
|
|
259
|
+
}
|
|
260
|
+
/** Provider declared in the spec but not yet implemented (`SecretFiles`) */
|
|
261
|
+
declare class NotImplementedProviderError extends ChabisConfigError {
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Opt-in placeholder guard (SPEC §10). A GPM/Azure key that matched no field
|
|
266
|
+
* path leaves its `"<Configured-in-…>"` placeholder untouched. Not run
|
|
267
|
+
* automatically: the consumer calls it after `loadConfig` if they want the check
|
|
268
|
+
*/
|
|
269
|
+
|
|
270
|
+
/** Thrown when an unresolved placeholder remains */
|
|
271
|
+
declare class PlaceholderError extends ChabisConfigError {
|
|
272
|
+
}
|
|
273
|
+
/** Throw if any leaf string in `obj` still starts with `marker`, meaning a secret never resolved */
|
|
274
|
+
declare function assertNoPlaceholders(obj: unknown, marker?: string): void;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Vendor-detected JSON-Schema introspection (decision #9 / SPEC §9a).
|
|
278
|
+
*
|
|
279
|
+
* Standard Schema's `validate` does not expose fields, but the schema library
|
|
280
|
+
* can: zod v4 and valibot each emit a JSON Schema whose `properties` we recurse
|
|
281
|
+
* to recover the same leaf paths the Python Azure provider gets from
|
|
282
|
+
* `model_cls.model_fields`. The right emitter is picked from
|
|
283
|
+
* `schema["~standard"].vendor`, not by feature-detecting a method, because
|
|
284
|
+
* `z.toJSONSchema(schema)` and valibot's `toJsonSchema(schema)` are module-level
|
|
285
|
+
* functions, not methods on the schema object
|
|
286
|
+
*/
|
|
287
|
+
|
|
288
|
+
/** Vendors that can be introspected for Azure `per-leaf` mode */
|
|
289
|
+
declare function supportedIntrospectionVendors(): string[];
|
|
290
|
+
/**
|
|
291
|
+
* Enumerate every leaf path of a schema (`[["Auth","Secret"], ["Database","Url"], …]`)
|
|
292
|
+
* via its vendor's JSON-Schema emitter. Throws `IntrospectionError` when the
|
|
293
|
+
* vendor has no registered introspector
|
|
294
|
+
*/
|
|
295
|
+
declare function introspectLeafPaths(schema: StandardSchemaV1): Promise<string[][]>;
|
|
296
|
+
|
|
297
|
+
export { AzureKeyVaultProvider, type AzureSecretsConfig, ChabisConfigError, ConfigValidationError, type DeepPartial, GoogleParameterManagerProvider, type GpmSecretsConfig, IntrospectionError, type LoadConfigOptions, NotImplementedProviderError, PlaceholderError, ProviderError, type SecretFilesConfig, type SecretProvider, type SecretsConfig, SecretsConfigError, assertNoPlaceholders, introspectLeafPaths, loadConfig, resetConfigCache, supportedIntrospectionVendors };
|