@secrefs/node 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/index.cjs +804 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +581 -0
- package/dist/index.d.ts +581 -0
- package/dist/index.js +749 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.cjs +79 -0
- package/dist/parser.cjs.map +1 -0
- package/dist/parser.d.cts +41 -0
- package/dist/parser.d.ts +41 -0
- package/dist/parser.js +60 -0
- package/dist/parser.js.map +1 -0
- package/dist/secrefs.cjs +876 -0
- package/dist/secrefs.cjs.map +1 -0
- package/package.json +83 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
|
|
2
|
+
import vaultFactory from 'node-vault';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The provider contract every SecRefs backend (AWS, Vault, local, or a
|
|
6
|
+
* custom one you bring yourself) implements. Providers never log, print,
|
|
7
|
+
* or persist the values they return - that discipline is enforced by the
|
|
8
|
+
* resolver and CLI layers above them, which only ever handle secret values
|
|
9
|
+
* long enough to hand them to `process.env` or a spawned child process.
|
|
10
|
+
*/
|
|
11
|
+
interface SecretFetchRequest {
|
|
12
|
+
/** The provider-specific secret path/id, as written after `sec://<provider>/`. */
|
|
13
|
+
path: string;
|
|
14
|
+
/** Optional dot-notation field to extract from a JSON secret payload. */
|
|
15
|
+
field?: string;
|
|
16
|
+
}
|
|
17
|
+
interface ProviderHealth {
|
|
18
|
+
provider: string;
|
|
19
|
+
ok: boolean;
|
|
20
|
+
/** Human-readable diagnostic. Never contains secret material. */
|
|
21
|
+
message?: string;
|
|
22
|
+
}
|
|
23
|
+
interface ISecretProvider {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
/** Fetch and resolve a single secret reference to its final string value. */
|
|
26
|
+
fetchOne(request: SecretFetchRequest): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* Fetch multiple secret references. Implementations may batch/dedupe
|
|
29
|
+
* against the backend where possible; the default behavior (provided by
|
|
30
|
+
* {@link BaseSecretProvider}) is concurrent individual fetches via
|
|
31
|
+
* `Promise.allSettled`, surfacing the first failure with full context.
|
|
32
|
+
*/
|
|
33
|
+
fetchBatch(requests: SecretFetchRequest[]): Promise<string[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Lightweight reachability/auth probe used by `secrefs check`. Must never
|
|
36
|
+
* throw for expected failure modes (bad credentials, unreachable host) -
|
|
37
|
+
* those are reported via the returned {@link ProviderHealth}.
|
|
38
|
+
*/
|
|
39
|
+
healthCheck(): Promise<ProviderHealth>;
|
|
40
|
+
}
|
|
41
|
+
declare class SecretFetchError extends Error {
|
|
42
|
+
readonly provider: string;
|
|
43
|
+
readonly path: string;
|
|
44
|
+
constructor(provider: string, path: string, cause: unknown);
|
|
45
|
+
}
|
|
46
|
+
declare abstract class BaseSecretProvider implements ISecretProvider {
|
|
47
|
+
abstract readonly name: string;
|
|
48
|
+
abstract fetchOne(request: SecretFetchRequest): Promise<string>;
|
|
49
|
+
fetchBatch(requests: SecretFetchRequest[]): Promise<string[]>;
|
|
50
|
+
abstract healthCheck(): Promise<ProviderHealth>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Extracts a (possibly dot-nested) field from a JSON-encoded secret. If no
|
|
54
|
+
* field is requested, the raw string is returned unchanged. Throws a plain
|
|
55
|
+
* `Error` (never leaking the secret value itself) when the payload isn't
|
|
56
|
+
* valid JSON or the field path doesn't resolve to a value.
|
|
57
|
+
*/
|
|
58
|
+
declare function extractField(raw: string, field: string | undefined, context: {
|
|
59
|
+
provider: string;
|
|
60
|
+
path: string;
|
|
61
|
+
}): string;
|
|
62
|
+
|
|
63
|
+
type ProviderRegistry = Record<string, ISecretProvider>;
|
|
64
|
+
interface ExpandOptions {
|
|
65
|
+
providers: ProviderRegistry;
|
|
66
|
+
/**
|
|
67
|
+
* When true (default), a value that starts with `sec://` but fails to
|
|
68
|
+
* parse throws immediately. When false, such values are left untouched.
|
|
69
|
+
* This only affects syntactically malformed references - unknown
|
|
70
|
+
* providers and provider-side fetch failures always surface as errors,
|
|
71
|
+
* aggregated in a {@link SecRefsResolutionError}.
|
|
72
|
+
*/
|
|
73
|
+
strict?: boolean;
|
|
74
|
+
}
|
|
75
|
+
interface ResolutionFailure {
|
|
76
|
+
/** The env var / map key the reference was assigned to. */
|
|
77
|
+
key: string;
|
|
78
|
+
/** The original `sec://` string. */
|
|
79
|
+
ref: string;
|
|
80
|
+
message: string;
|
|
81
|
+
}
|
|
82
|
+
declare class SecRefsResolutionError extends Error {
|
|
83
|
+
readonly errors: ResolutionFailure[];
|
|
84
|
+
constructor(errors: ResolutionFailure[]);
|
|
85
|
+
}
|
|
86
|
+
interface CheckResult {
|
|
87
|
+
key: string;
|
|
88
|
+
ref: string;
|
|
89
|
+
provider: string;
|
|
90
|
+
ok: boolean;
|
|
91
|
+
/** Present only when ok is false. Never contains the secret value. */
|
|
92
|
+
message?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Expands every `sec://` value in a plain key/value map, resolving all
|
|
96
|
+
* references concurrently via `Promise.allSettled`. Non-reference values
|
|
97
|
+
* pass through untouched. Never writes anything to disk - the caller
|
|
98
|
+
* decides what to do with the returned map (assign to `process.env`,
|
|
99
|
+
* template into a string, etc).
|
|
100
|
+
*
|
|
101
|
+
* Throws {@link SecRefsResolutionError} aggregating every failed
|
|
102
|
+
* reference if any fail to resolve.
|
|
103
|
+
*/
|
|
104
|
+
declare function expandKeyValueMap(input: Record<string, string | undefined>, options: ExpandOptions): Promise<Record<string, string>>;
|
|
105
|
+
/**
|
|
106
|
+
* Expands `sec://` values found in `process.env`, mutating it in place.
|
|
107
|
+
* Returns the list of env var names that were rewritten.
|
|
108
|
+
*/
|
|
109
|
+
declare function expandProcessEnv(options: ExpandOptions): Promise<string[]>;
|
|
110
|
+
/**
|
|
111
|
+
* Dry-run validation: resolves every `sec://` reference found in `input`
|
|
112
|
+
* but reports only ok/failure per reference - the secret values themselves
|
|
113
|
+
* are never returned or logged. Used by `secrefs check`.
|
|
114
|
+
*/
|
|
115
|
+
declare function checkReferences(input: Record<string, string | undefined>, options: ExpandOptions): Promise<CheckResult[]>;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* URI parser for SecRefs' `sec://` reference format:
|
|
119
|
+
*
|
|
120
|
+
* sec://<provider-alias>/<secret-path-or-id>[#<json-field>]
|
|
121
|
+
*
|
|
122
|
+
* sec://aws/prod/db#password
|
|
123
|
+
* sec://vault/secret/data/stripe#key
|
|
124
|
+
* sec://local/mock-db#password
|
|
125
|
+
*
|
|
126
|
+
* The provider alias is a bare identifier (letters/digits/`-`/`_`), the path
|
|
127
|
+
* is opaque to this parser (providers interpret it however their backend
|
|
128
|
+
* needs), and the optional `#field` fragment supports dot-notation for
|
|
129
|
+
* traversing nested JSON secrets (e.g. `#nested.value`).
|
|
130
|
+
*/
|
|
131
|
+
interface ParsedSecretRef {
|
|
132
|
+
/** The original, unmodified reference string. */
|
|
133
|
+
raw: string;
|
|
134
|
+
/** Lowercased provider alias, e.g. "aws", "vault", "local". */
|
|
135
|
+
provider: string;
|
|
136
|
+
/** The secret path/id as understood by the provider. */
|
|
137
|
+
path: string;
|
|
138
|
+
/** Optional dot-notation field to extract from a JSON secret. */
|
|
139
|
+
field?: string;
|
|
140
|
+
}
|
|
141
|
+
declare class SecRefParseError extends Error {
|
|
142
|
+
readonly raw: string;
|
|
143
|
+
readonly reason: string;
|
|
144
|
+
constructor(raw: string, reason: string);
|
|
145
|
+
}
|
|
146
|
+
/** True if `value` is a string that looks like a `sec://` reference at all. */
|
|
147
|
+
declare function isSecretRef(value: unknown): value is string;
|
|
148
|
+
/**
|
|
149
|
+
* Parses a `sec://` reference string. Throws {@link SecRefParseError} if the
|
|
150
|
+
* value isn't a string, doesn't start with `sec://`, or doesn't match the
|
|
151
|
+
* full `<provider>/<path>[#field]` shape.
|
|
152
|
+
*/
|
|
153
|
+
declare function parseSecretRef(raw: unknown): ParsedSecretRef;
|
|
154
|
+
/** Best-effort parse that returns `null` instead of throwing. */
|
|
155
|
+
declare function tryParseSecretRef(raw: unknown): ParsedSecretRef | null;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* dotenv treats `#` as a start-of-comment marker even mid-value (unless
|
|
159
|
+
* the value is quoted), which silently truncates the `#field` fragment
|
|
160
|
+
* off an unquoted `sec://provider/path#field` reference - the exact
|
|
161
|
+
* format SecRefs itself uses. Rather than require every `sec://` value in
|
|
162
|
+
* `.env` to be quoted (an easy thing to forget, with no error if you do),
|
|
163
|
+
* this re-scans the raw file for unquoted `sec://` assignments and
|
|
164
|
+
* restores the value dotenv would otherwise clip.
|
|
165
|
+
*
|
|
166
|
+
* Note: because of this, an unquoted `sec://` value can't have a
|
|
167
|
+
* trailing inline comment on the same line - put comments on their own
|
|
168
|
+
* line instead. Quoted values (`KEY="sec://...#field"`) are unaffected
|
|
169
|
+
* and already handled correctly by dotenv itself.
|
|
170
|
+
*/
|
|
171
|
+
declare function recoverTruncatedSecRefs(rawText: string, parsed: Record<string, string>): Record<string, string>;
|
|
172
|
+
/** Parses a `.env` file's contents, correctly preserving `sec://...#field` fragments. */
|
|
173
|
+
declare function parseEnvFileText(rawText: string): Record<string, string>;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* A cache that expires, used by every provider that fetches over the
|
|
177
|
+
* network.
|
|
178
|
+
*
|
|
179
|
+
* The default TTL is **zero** — every read re-fetches. That's deliberate
|
|
180
|
+
* and it's the whole point of the product: a `sec://` reference is a
|
|
181
|
+
* stable name for a value that changes underneath it. A consumer holding
|
|
182
|
+
* the reference is supposed to see a rotated secret without being
|
|
183
|
+
* redeployed, and a cache with no expiry silently breaks exactly that.
|
|
184
|
+
* Before this existed, a long-running process fetched once and held the
|
|
185
|
+
* old value until restart.
|
|
186
|
+
*
|
|
187
|
+
* A non-zero TTL is a real tradeoff, not a mistake: every expansion is a
|
|
188
|
+
* network round trip, so a busy caller may want to trade a bounded window
|
|
189
|
+
* of staleness for latency and API-rate-limit headroom. `ttlMs: 30_000`
|
|
190
|
+
* means "a rotation reaches me within 30 seconds" — usually fine, and it
|
|
191
|
+
* should be a decision someone made rather than a default they inherited.
|
|
192
|
+
*/
|
|
193
|
+
interface TtlCacheOptions {
|
|
194
|
+
/** Milliseconds an entry stays fresh. `0` (default) disables caching
|
|
195
|
+
* entirely - every `fetch` call goes to the source. */
|
|
196
|
+
ttlMs?: number;
|
|
197
|
+
/** Injected in tests so expiry doesn't require real waiting. */
|
|
198
|
+
now?: () => number;
|
|
199
|
+
}
|
|
200
|
+
declare class TtlCache<T> {
|
|
201
|
+
/** Settled values, only populated when a TTL is configured. */
|
|
202
|
+
private readonly entries;
|
|
203
|
+
/** Requests currently in flight, tracked separately from `entries`
|
|
204
|
+
* because coalescing and caching are different things: sharing an
|
|
205
|
+
* unsettled request holds no value past the moment it resolves, so it
|
|
206
|
+
* stays correct even with caching fully disabled. */
|
|
207
|
+
private readonly inFlight;
|
|
208
|
+
private readonly ttlMs;
|
|
209
|
+
private readonly now;
|
|
210
|
+
constructor(options?: TtlCacheOptions);
|
|
211
|
+
/**
|
|
212
|
+
* Returns the cached value for `key` if it's still fresh, otherwise
|
|
213
|
+
* calls `load` and caches that. In-flight promises are shared, so N
|
|
214
|
+
* concurrent expansions of the same reference make one request rather
|
|
215
|
+
* than N even when the TTL is zero - that's request coalescing, not
|
|
216
|
+
* caching, and it doesn't hold a value past its use.
|
|
217
|
+
*
|
|
218
|
+
* A rejected load is evicted rather than remembered, so a transient
|
|
219
|
+
* failure doesn't become a sticky one.
|
|
220
|
+
*/
|
|
221
|
+
fetch(key: string, load: () => Promise<T>): Promise<T>;
|
|
222
|
+
/** Drops everything - used when a credential changes underneath the
|
|
223
|
+
* cache and anything fetched with the old one is suspect. */
|
|
224
|
+
clear(): void;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Thin HTTP client for a running control plane's credential-broker
|
|
229
|
+
* endpoint (docs/control-plane-design.md §7). This is the piece §10
|
|
230
|
+
* flagged as the missing link: every provider that supports
|
|
231
|
+
* control-plane-sourced credentials (AwsSecretsManagerProvider,
|
|
232
|
+
* BitwardenProvider - see their `controlPlane` constructor option)
|
|
233
|
+
* constructs one of these instead of only ever reading ambient env vars.
|
|
234
|
+
*
|
|
235
|
+
* Deliberately just an HTTP wrapper with no retry/backoff/circuit-
|
|
236
|
+
* breaking logic - a mint failure surfaces as a normal rejected promise,
|
|
237
|
+
* same as any other provider fetch failure, and the caller's existing
|
|
238
|
+
* error handling (resolver.ts's Promise.allSettled aggregation) already
|
|
239
|
+
* does the right thing with that.
|
|
240
|
+
*/
|
|
241
|
+
interface MintedAwsCredentials {
|
|
242
|
+
accessKeyId: string;
|
|
243
|
+
secretAccessKey: string;
|
|
244
|
+
sessionToken: string;
|
|
245
|
+
/** ISO-8601 expiration timestamp. */
|
|
246
|
+
expiration: string;
|
|
247
|
+
}
|
|
248
|
+
interface MintedBitwardenCredentials {
|
|
249
|
+
accessToken: string;
|
|
250
|
+
organizationId?: string;
|
|
251
|
+
/** Explicitly not a TTL promise - see apps/control-plane/src/providers/bitwarden.ts. */
|
|
252
|
+
note: string;
|
|
253
|
+
}
|
|
254
|
+
type MintCredentialResponse = {
|
|
255
|
+
provider: "aws";
|
|
256
|
+
credentials: MintedAwsCredentials;
|
|
257
|
+
} | {
|
|
258
|
+
provider: "bitwarden";
|
|
259
|
+
credentials: MintedBitwardenCredentials;
|
|
260
|
+
};
|
|
261
|
+
/** What a provider's `controlPlane` constructor option needs - shared
|
|
262
|
+
* shape between `AwsSecretsManagerProvider` and `BitwardenProvider` (and
|
|
263
|
+
* any future control-plane-aware provider). */
|
|
264
|
+
interface ControlPlaneCredentialSource {
|
|
265
|
+
/** Base URL of a running control plane, e.g. from $SECREFS_CONTROL_PLANE_URL. */
|
|
266
|
+
baseUrl: string;
|
|
267
|
+
/** Bootstrap token or a verified OIDC token, e.g. from $SECREFS_CONTROL_PLANE_TOKEN. */
|
|
268
|
+
token: string;
|
|
269
|
+
/** Which `VaultConnection` alias this provider instance represents -
|
|
270
|
+
* this is what the control plane's RBAC grants are actually scoped
|
|
271
|
+
* against, not the `sec://` alias this provider happens to be
|
|
272
|
+
* registered under (though in practice they're usually the same
|
|
273
|
+
* string). */
|
|
274
|
+
alias: string;
|
|
275
|
+
/** Injected for testing - defaults to a real `ControlPlaneClient`. */
|
|
276
|
+
client?: ControlPlaneClient;
|
|
277
|
+
}
|
|
278
|
+
interface ControlPlaneClientOptions {
|
|
279
|
+
/** Base URL of a running control plane, e.g. from $SECREFS_CONTROL_PLANE_URL. */
|
|
280
|
+
baseUrl: string;
|
|
281
|
+
/** Bootstrap token or a verified OIDC token, e.g. from $SECREFS_CONTROL_PLANE_TOKEN. */
|
|
282
|
+
token: string;
|
|
283
|
+
/** Injected for testing - defaults to the global `fetch`. */
|
|
284
|
+
fetchImpl?: typeof fetch;
|
|
285
|
+
}
|
|
286
|
+
/** Thrown for a well-formed error response from the control plane (401,
|
|
287
|
+
* 403, 502, ...) - `status` and `message` come straight from its `{ error }`
|
|
288
|
+
* body, so a denial reason (e.g. "no grant authorizes path...") reaches
|
|
289
|
+
* the caller verbatim rather than as an opaque HTTP failure. */
|
|
290
|
+
declare class ControlPlaneRequestError extends Error {
|
|
291
|
+
readonly status: number;
|
|
292
|
+
constructor(status: number, message: string);
|
|
293
|
+
}
|
|
294
|
+
declare class ControlPlaneClient {
|
|
295
|
+
private readonly baseUrl;
|
|
296
|
+
private readonly token;
|
|
297
|
+
private readonly fetchImpl;
|
|
298
|
+
constructor(options: ControlPlaneClientOptions);
|
|
299
|
+
/** Authenticates, authorizes, and resolves a credential for `alias`/`path`
|
|
300
|
+
* - see the control plane's `POST /v1/credentials/mint`. Throws
|
|
301
|
+
* `ControlPlaneRequestError` for any non-2xx response. */
|
|
302
|
+
mintCredential(alias: string, path: string): Promise<MintCredentialResponse>;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
interface AwsProviderOptions {
|
|
306
|
+
region?: string;
|
|
307
|
+
/** Inject a pre-configured client (primarily for testing) - also wins
|
|
308
|
+
* over `controlPlane` if both are set, since a test that supplies an
|
|
309
|
+
* explicit client wants full control regardless of the mode. */
|
|
310
|
+
client?: SecretsManagerClient;
|
|
311
|
+
/**
|
|
312
|
+
* Sources per-request AWS credentials from a running control plane
|
|
313
|
+
* (docs/control-plane-design.md §7/§10) instead of the ambient default
|
|
314
|
+
* credential chain. Every `fetchOne` call mints a fresh, request-scoped
|
|
315
|
+
* credential via `sts:AssumeRole` on the control plane's side - see
|
|
316
|
+
* `apps/control-plane/src/providers/awsSts.ts`. Mutually exclusive
|
|
317
|
+
* with ambient auth in spirit (not enforced - `client` still wins if
|
|
318
|
+
* both are given, for testing).
|
|
319
|
+
*/
|
|
320
|
+
controlPlane?: ControlPlaneCredentialSource;
|
|
321
|
+
/**
|
|
322
|
+
* How long a fetched secret value may be reused, in milliseconds.
|
|
323
|
+
* Defaults to 0 - every expansion re-fetches, so a rotated secret
|
|
324
|
+
* reaches a long-running consumer without a redeploy. Raise it to
|
|
325
|
+
* trade a bounded window of staleness for fewer round trips. See
|
|
326
|
+
* ../ttlCache.ts.
|
|
327
|
+
*/
|
|
328
|
+
cacheTtlMs?: number;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* AWS Secrets Manager provider. Two credential-sourcing modes:
|
|
332
|
+
*
|
|
333
|
+
* - **Ambient (default)**: the AWS SDK v3 default credential provider
|
|
334
|
+
* chain - environment variables, shared config/credentials files,
|
|
335
|
+
* ECS/EC2 instance metadata, or an assumed IAM role. No credentials
|
|
336
|
+
* ever need to live in SecRefs configuration itself. One client is
|
|
337
|
+
* built lazily and reused for the provider's lifetime.
|
|
338
|
+
* - **Control-plane-sourced** (`controlPlane` option): a fresh,
|
|
339
|
+
* request-scoped credential is minted per `fetchOne` call via the
|
|
340
|
+
* control plane's `/v1/credentials/mint`, so a distinct
|
|
341
|
+
* `SecretsManagerClient` is constructed per path rather than reused -
|
|
342
|
+
* each one only ever has the narrow scope that one mint granted.
|
|
343
|
+
*
|
|
344
|
+
* Raw secret values fetched per-path are cached in memory for the lifetime
|
|
345
|
+
* of the provider instance either way, so multiple `#field` references
|
|
346
|
+
* against the same secret only cost one API call (and, in control-plane
|
|
347
|
+
* mode, one mint).
|
|
348
|
+
*/
|
|
349
|
+
declare class AwsSecretsManagerProvider extends BaseSecretProvider {
|
|
350
|
+
readonly name = "aws";
|
|
351
|
+
private readonly explicitClient?;
|
|
352
|
+
private readonly region?;
|
|
353
|
+
private readonly controlPlane?;
|
|
354
|
+
private readonly controlPlaneClient?;
|
|
355
|
+
private ambientClient;
|
|
356
|
+
private readonly rawCache;
|
|
357
|
+
constructor(options?: AwsProviderOptions);
|
|
358
|
+
/** Resolves the `SecretsManagerClient` to use for one `path` - lazily
|
|
359
|
+
* built and reused in ambient mode, freshly minted per call in
|
|
360
|
+
* control-plane mode. `explicitClient` (test injection) always wins. */
|
|
361
|
+
private clientFor;
|
|
362
|
+
private buildClientFromMintedCredentials;
|
|
363
|
+
private getRaw;
|
|
364
|
+
fetchOne(request: SecretFetchRequest): Promise<string>;
|
|
365
|
+
healthCheck(): Promise<ProviderHealth>;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
interface VaultProviderOptions {
|
|
369
|
+
/** Defaults to $VAULT_ADDR. */
|
|
370
|
+
endpoint?: string;
|
|
371
|
+
/** Defaults to $VAULT_TOKEN. */
|
|
372
|
+
token?: string;
|
|
373
|
+
/** Inject a pre-configured client (primarily for testing). */
|
|
374
|
+
client?: ReturnType<typeof vaultFactory>;
|
|
375
|
+
/** How long a fetched secret may be reused, in ms. Defaults to 0 -
|
|
376
|
+
* every expansion re-fetches, so rotation reaches a long-running
|
|
377
|
+
* consumer without a redeploy. See ../ttlCache.ts. */
|
|
378
|
+
cacheTtlMs?: number;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* HashiCorp Vault provider supporting both KV v1 and KV v2 secrets engines.
|
|
382
|
+
* Auth is ambient via `VAULT_ADDR`/`VAULT_TOKEN` - point `path` at whatever
|
|
383
|
+
* the Vault HTTP API itself expects (KV v2 mounts include a literal `data/`
|
|
384
|
+
* segment, e.g. `secret/data/stripe`; KV v1 mounts do not).
|
|
385
|
+
*
|
|
386
|
+
* The client is constructed lazily on first use so that simply having a
|
|
387
|
+
* `VaultProvider` in your provider registry doesn't require Vault to be
|
|
388
|
+
* configured if you never actually reference `sec://vault/...`.
|
|
389
|
+
*/
|
|
390
|
+
declare class VaultProvider extends BaseSecretProvider {
|
|
391
|
+
readonly name = "vault";
|
|
392
|
+
private readonly explicitClient?;
|
|
393
|
+
private readonly endpoint?;
|
|
394
|
+
private readonly token?;
|
|
395
|
+
private client;
|
|
396
|
+
private readonly dataCache;
|
|
397
|
+
constructor(options?: VaultProviderOptions);
|
|
398
|
+
private getClient;
|
|
399
|
+
private getData;
|
|
400
|
+
fetchOne(request: SecretFetchRequest): Promise<string>;
|
|
401
|
+
healthCheck(): Promise<ProviderHealth>;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
interface LocalProviderOptions {
|
|
405
|
+
/** Overrides the file path. Defaults to $SECREFS_LOCAL_FILE or ./.secrefs.local.json */
|
|
406
|
+
filePath?: string;
|
|
407
|
+
/** Keep the parsed file in memory instead of re-reading per fetch.
|
|
408
|
+
* Off by default so edits take effect immediately. */
|
|
409
|
+
cacheFile?: boolean;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Reads secrets from a gitignored, developer-local JSON file. Intended for
|
|
413
|
+
* local development only - never point this at anything checked into
|
|
414
|
+
* version control. Each top-level key is a secret path; its value is either
|
|
415
|
+
* a plain string (returned as-is when no `#field` is requested) or an
|
|
416
|
+
* object (JSON-stringified, then field-extracted as needed).
|
|
417
|
+
*
|
|
418
|
+
* Example `.secrefs.local.json`:
|
|
419
|
+
* ```json
|
|
420
|
+
* { "mock-db": { "password": "hunter2", "user": "postgres" } }
|
|
421
|
+
* ```
|
|
422
|
+
*/
|
|
423
|
+
declare class LocalProvider extends BaseSecretProvider {
|
|
424
|
+
readonly name = "local";
|
|
425
|
+
private readonly filePath;
|
|
426
|
+
/** Re-read on every fetch. The file is local and tiny, and caching
|
|
427
|
+
* it meant editing it mid-session silently did nothing. */
|
|
428
|
+
private cache;
|
|
429
|
+
private readonly cacheFile;
|
|
430
|
+
constructor(options?: LocalProviderOptions);
|
|
431
|
+
private load;
|
|
432
|
+
fetchOne(request: SecretFetchRequest): Promise<string>;
|
|
433
|
+
healthCheck(): Promise<ProviderHealth>;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The subset of `@bitwarden/sdk-napi`'s `BitwardenClient` this provider
|
|
437
|
+
* calls - kept narrow so tests can inject a plain mock object instead of
|
|
438
|
+
* a real client, the same pattern `VaultProvider`/`AwsSecretsManagerProvider`
|
|
439
|
+
* use for their own SDK clients. */
|
|
440
|
+
interface BitwardenClientLike {
|
|
441
|
+
auth(): {
|
|
442
|
+
loginAccessToken(accessToken: string, stateFile?: string): Promise<void>;
|
|
443
|
+
};
|
|
444
|
+
secrets(): {
|
|
445
|
+
get(id: string): Promise<{
|
|
446
|
+
value: string;
|
|
447
|
+
}>;
|
|
448
|
+
list(organizationId: string): Promise<{
|
|
449
|
+
data: {
|
|
450
|
+
id: string;
|
|
451
|
+
key: string;
|
|
452
|
+
}[];
|
|
453
|
+
}>;
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
interface BitwardenProviderOptions {
|
|
457
|
+
/** Defaults to $BWS_ACCESS_TOKEN. Ignored if `controlPlane` is set. */
|
|
458
|
+
accessToken?: string;
|
|
459
|
+
/** Required only to resolve a `path` given as a secret *name* rather
|
|
460
|
+
* than its UUID (see class docs). Defaults to $BWS_ORGANIZATION_ID.
|
|
461
|
+
* Ignored if `controlPlane` is set - the control plane's distributed
|
|
462
|
+
* credential supplies this instead. */
|
|
463
|
+
organizationId?: string;
|
|
464
|
+
/** Self-hosted instance override. Defaults to $BWS_API_URL. */
|
|
465
|
+
apiUrl?: string;
|
|
466
|
+
/** Self-hosted instance override. Defaults to $BWS_IDENTITY_URL. */
|
|
467
|
+
identityUrl?: string;
|
|
468
|
+
/**
|
|
469
|
+
* Opt-in path to an encrypted session-state file the SDK can reuse
|
|
470
|
+
* across calls to reduce auth rate-limiting (Bitwarden's own docs
|
|
471
|
+
* describe this file's contents as fully encrypted, not plaintext
|
|
472
|
+
* secret material). Omitted by default - this provider re-authenticates
|
|
473
|
+
* in memory each time it's constructed and writes nothing to disk
|
|
474
|
+
* unless a caller opts in.
|
|
475
|
+
*/
|
|
476
|
+
stateFile?: string;
|
|
477
|
+
/**
|
|
478
|
+
* Sources the access token/organizationId from a running control plane
|
|
479
|
+
* (docs/control-plane-design.md §7/§10, and §8 for why Bitwarden's
|
|
480
|
+
* distribution here isn't the same as AWS's per-request minting -
|
|
481
|
+
* see apps/control-plane/src/providers/bitwarden.ts). Every `fetchOne`
|
|
482
|
+
* call still requests a distribution for its specific `path`, so the
|
|
483
|
+
* control plane's RBAC `Grant.path_pattern` is enforced per secret even
|
|
484
|
+
* though the underlying Bitwarden token itself isn't scoped that
|
|
485
|
+
* narrowly - "SDK-side enforcement" as documented on the control-plane
|
|
486
|
+
* side.
|
|
487
|
+
*/
|
|
488
|
+
controlPlane?: ControlPlaneCredentialSource;
|
|
489
|
+
/** Inject a pre-configured client (primarily for testing). */
|
|
490
|
+
client?: BitwardenClientLike;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Bitwarden **Secrets Manager** provider (not the password vault - see
|
|
494
|
+
* https://bitwarden.com/help/secrets-manager-overview/). Two structural
|
|
495
|
+
* differences from `AwsSecretsManagerProvider`/`VaultProvider` worth
|
|
496
|
+
* knowing before using this:
|
|
497
|
+
*
|
|
498
|
+
* 1. **Secrets are end-to-end encrypted.** There is no plain authenticated
|
|
499
|
+
* REST call to fetch a value - the official SDK derives a decryption
|
|
500
|
+
* key from the access token during login and decrypts client-side.
|
|
501
|
+
* That's why this provider depends on `@bitwarden/sdk-napi` (a beta
|
|
502
|
+
* Node-API binding maintained by Bitwarden) rather than a bare `fetch`.
|
|
503
|
+
* 2. **Bitwarden addresses secrets by UUID, with no path hierarchy** the
|
|
504
|
+
* way AWS/Vault secret names have. `path` may be that UUID directly, or
|
|
505
|
+
* - if `organizationId` is configured (ambient mode) or supplied by the
|
|
506
|
+
* control plane (control-plane mode) - a human-readable secret *name*
|
|
507
|
+
* (Bitwarden's "key" field), resolved via one cached `secrets().list()`
|
|
508
|
+
* call. With neither, only UUID paths work.
|
|
509
|
+
*/
|
|
510
|
+
declare class BitwardenProvider extends BaseSecretProvider {
|
|
511
|
+
readonly name = "bitwarden";
|
|
512
|
+
private readonly explicitClient?;
|
|
513
|
+
private readonly ambientAccessToken?;
|
|
514
|
+
private readonly ambientOrganizationId?;
|
|
515
|
+
private readonly apiUrl?;
|
|
516
|
+
private readonly identityUrl?;
|
|
517
|
+
private readonly stateFile?;
|
|
518
|
+
private readonly controlPlane?;
|
|
519
|
+
private readonly controlPlaneClient?;
|
|
520
|
+
private client;
|
|
521
|
+
private loggedInAccessToken;
|
|
522
|
+
private loggedIn;
|
|
523
|
+
private organizationId;
|
|
524
|
+
/** Secret name -> id, populated by one `list()` call the first time a
|
|
525
|
+
* non-UUID path is requested. Invalidated if `organizationId` ever
|
|
526
|
+
* changes (control-plane mode, defensively - static in practice). */
|
|
527
|
+
private nameToId;
|
|
528
|
+
constructor(options?: BitwardenProviderOptions);
|
|
529
|
+
private getClient;
|
|
530
|
+
private loginWith;
|
|
531
|
+
/** Ensures a session exists for `path`. Ambient mode logs in once
|
|
532
|
+
* (memoized) with the ambient token; control-plane mode requests a
|
|
533
|
+
* distribution for this specific `path` every call - see the
|
|
534
|
+
* `controlPlane` option's docs for why that RBAC check has to be
|
|
535
|
+
* per-path even though the token it returns doesn't vary. */
|
|
536
|
+
private ensureLoggedInFor;
|
|
537
|
+
/** Assumes `ensureLoggedInFor(path)` has already run for this exact
|
|
538
|
+
* `path` - callers always do that first, so `this.organizationId` is
|
|
539
|
+
* already whatever this path's session resolved to. */
|
|
540
|
+
private resolveSecretId;
|
|
541
|
+
fetchOne(request: SecretFetchRequest): Promise<string>;
|
|
542
|
+
healthCheck(): Promise<ProviderHealth>;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Builds the default provider registry: aws, vault, local, bitwarden. */
|
|
546
|
+
declare function createDefaultProviders(): ProviderRegistry;
|
|
547
|
+
interface SecRefsOptions {
|
|
548
|
+
providers?: ProviderRegistry;
|
|
549
|
+
strict?: boolean;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* The primary library entry point. Instantiate your own (with a custom
|
|
553
|
+
* provider registry) or use the default `secRefs` singleton below.
|
|
554
|
+
*/
|
|
555
|
+
declare class SecRefs {
|
|
556
|
+
readonly providers: ProviderRegistry;
|
|
557
|
+
readonly strict: boolean;
|
|
558
|
+
constructor(options?: SecRefsOptions);
|
|
559
|
+
private get expandOptions();
|
|
560
|
+
/**
|
|
561
|
+
* Expands every `sec://` value found in `process.env`, mutating it in
|
|
562
|
+
* place. Returns the list of env var names that were rewritten.
|
|
563
|
+
*/
|
|
564
|
+
init(): Promise<string[]>;
|
|
565
|
+
/**
|
|
566
|
+
* Expands `sec://` values in an arbitrary key/value map (e.g. a parsed
|
|
567
|
+
* `.env` file) without touching `process.env`.
|
|
568
|
+
*/
|
|
569
|
+
expandEnv(env: Record<string, string | undefined>): Promise<Record<string, string>>;
|
|
570
|
+
/** Expands a single string if it's a `sec://` reference; otherwise returns it unchanged. */
|
|
571
|
+
expandString(value: string): Promise<string>;
|
|
572
|
+
/**
|
|
573
|
+
* Dry-run validation of every `sec://` reference in `env` (defaults to
|
|
574
|
+
* `process.env`). Never returns plaintext secret values.
|
|
575
|
+
*/
|
|
576
|
+
check(env?: Record<string, string | undefined>): Promise<Awaited<ReturnType<typeof checkReferences>>>;
|
|
577
|
+
}
|
|
578
|
+
/** Convenience singleton mirroring `secRefs.init()` / `secRefs.expandEnv()` / `secRefs.expandString()`. */
|
|
579
|
+
declare const secRefs: SecRefs;
|
|
580
|
+
|
|
581
|
+
export { type AwsProviderOptions, AwsSecretsManagerProvider, BaseSecretProvider, type BitwardenClientLike, BitwardenProvider, type BitwardenProviderOptions, type CheckResult, ControlPlaneClient, type ControlPlaneClientOptions, type ControlPlaneCredentialSource, ControlPlaneRequestError, type ExpandOptions, type ISecretProvider, LocalProvider, type LocalProviderOptions, type MintCredentialResponse, type MintedAwsCredentials, type MintedBitwardenCredentials, type ParsedSecretRef, type ProviderHealth, type ProviderRegistry, type ResolutionFailure, SecRefParseError, SecRefs, type SecRefsOptions, SecRefsResolutionError, SecretFetchError, type SecretFetchRequest, TtlCache, type TtlCacheOptions, VaultProvider, type VaultProviderOptions, checkReferences, createDefaultProviders, expandKeyValueMap, expandProcessEnv, extractField, isSecretRef, parseEnvFileText, parseSecretRef, recoverTruncatedSecRefs, secRefs, tryParseSecretRef };
|