vaultkeeper 0.6.0 → 0.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/dist/index.d.cts CHANGED
@@ -1,3 +1,6 @@
1
+ /// <reference types="node" />
2
+ import { Buffer } from 'node:buffer';
3
+
1
4
  /**
2
5
  * Error hierarchy for vaultkeeper.
3
6
  */
@@ -37,6 +40,69 @@ declare class DeviceNotPresentError extends VaultError {
37
40
  declare class AuthorizationDeniedError extends VaultError {
38
41
  constructor(message: string);
39
42
  }
43
+ /**
44
+ * Thrown when an operation requires a backend capability (e.g.
45
+ * presence-per-use) that the active backend cannot provide.
46
+ *
47
+ * @remarks
48
+ * This is a configuration/backend mismatch, not a runtime authorization
49
+ * failure: the requirement was asserted against a backend whose configured
50
+ * instance does not advertise the capability, so no credential, session, or
51
+ * device is ever touched before this is thrown. It is fixable by switching to
52
+ * (or reconfiguring) a backend that provides the capability — inspect
53
+ * {@link NotCapableError.capability} for which one was required and
54
+ * {@link NotCapableError.backendType} for the backend that lacked it. Distinct
55
+ * from {@link AuthorizationDeniedError} (a human/token rejection) and
56
+ * {@link PresenceDeclinedError} (a human declined a fresh action).
57
+ *
58
+ * @public
59
+ */
60
+ declare class NotCapableError extends VaultError {
61
+ /** The `type` identifier of the active backend that lacked the capability. */
62
+ readonly backendType: string;
63
+ /**
64
+ * The machine-readable capability key that was required but not advertised
65
+ * (e.g. `'presencePerUse'`).
66
+ */
67
+ readonly capability: string;
68
+ constructor(message: string, backendType: string, capability: string);
69
+ }
70
+ /**
71
+ * Thrown when a required fresh, per-use human presence action was explicitly
72
+ * declined by the human (e.g. a biometric or touch prompt was cancelled).
73
+ *
74
+ * @remarks
75
+ * Distinct from {@link AuthorizationDeniedError}, which signals a token or
76
+ * capability rejection, and from {@link PresenceTimeoutError}, which signals the
77
+ * device was present but no action happened in time. A declined presence action
78
+ * means the human was asked and said no.
79
+ *
80
+ * @public
81
+ */
82
+ declare class PresenceDeclinedError extends VaultError {
83
+ /** The `type` identifier of the backend that requested the presence action. */
84
+ readonly backendType: string;
85
+ constructor(message: string, backendType: string);
86
+ }
87
+ /**
88
+ * Thrown when a required fresh, per-use human presence action did not happen
89
+ * within the allotted time — the device was present and ready, but no touch,
90
+ * tap, or biometric approval was performed before the timeout elapsed.
91
+ *
92
+ * @remarks
93
+ * Distinct from {@link DeviceNotPresentError} (the device itself was absent) and
94
+ * from {@link PresenceDeclinedError} (the human actively declined). Inspect
95
+ * {@link PresenceTimeoutError.timeoutMs} for how long the operation waited.
96
+ *
97
+ * @public
98
+ */
99
+ declare class PresenceTimeoutError extends VaultError {
100
+ /** The `type` identifier of the backend that requested the presence action. */
101
+ readonly backendType: string;
102
+ /** How long (in milliseconds) the operation waited for the presence action. */
103
+ readonly timeoutMs: number;
104
+ constructor(message: string, backendType: string, timeoutMs: number);
105
+ }
40
106
  /**
41
107
  * Thrown when no configured backend is available or reachable.
42
108
  * Inspect `reason` for a machine-readable cause and `attempted` for the list
@@ -135,8 +201,43 @@ declare class IdentityMismatchError extends VaultError {
135
201
  constructor(message: string, previousHash: string, currentHash: string);
136
202
  }
137
203
  /**
138
- * Thrown when a delegated `exec()` call fails due to a process-level error
139
- * (e.g. the command binary is not found or cannot be spawned).
204
+ * Thrown by {@link VaultKeeper.setup} when the caller does not make an
205
+ * unambiguous executable-trust decision.
206
+ *
207
+ * `setup()` deliberately has no default trust behaviour: the caller must either
208
+ * pass a real `executablePath` (which runs trust-on-first-use verification) or
209
+ * explicitly opt out with `skipTrust: true` (a development-only escape hatch).
210
+ * Supplying neither — or both at once — throws this error rather than silently
211
+ * skipping verification. Passing the retired `'dev'` sentinel as `executablePath`
212
+ * also throws this error. Inspect {@link ExecutableTrustRequiredError.reason} to
213
+ * distinguish the cases.
214
+ *
215
+ * @public
216
+ */
217
+ declare class ExecutableTrustRequiredError extends VaultError {
218
+ /**
219
+ * Machine-readable discriminator for why the trust choice was rejected.
220
+ * `'missing-choice'` means neither `executablePath` nor `skipTrust: true`
221
+ * was provided, so no trust decision was expressed. `'conflicting-choice'`
222
+ * means both `executablePath` and `skipTrust: true` were provided, which
223
+ * are mutually exclusive intents. `'legacy-dev-sentinel'` means
224
+ * `executablePath` was the retired literal `'dev'` opt-out sentinel, which is
225
+ * no longer supported and must be replaced with `skipTrust: true`.
226
+ */
227
+ readonly reason: 'missing-choice' | 'conflicting-choice' | 'legacy-dev-sentinel';
228
+ constructor(message: string, reason: 'missing-choice' | 'conflicting-choice' | 'legacy-dev-sentinel');
229
+ }
230
+ /**
231
+ * Thrown when spawning or running a subprocess fails.
232
+ *
233
+ * This covers a delegated `exec()` call failing due to an invalid request
234
+ * (e.g. a `{{secret}}` placeholder in the `command` field) or a
235
+ * process-level error (e.g. the command binary is not found or cannot be
236
+ * spawned) — and also vaultkeeper's own internal tool invocations (doctor
237
+ * version probes, the keychain/secret-tool/dpapi/yubikey credential helpers),
238
+ * which run through the same subprocess utility. Callers that need to handle
239
+ * only delegated-exec failures should scope their `catch` to the code paths
240
+ * that call `exec()`, not discriminate on this type alone.
140
241
  *
141
242
  * @public
142
243
  */
@@ -148,9 +249,10 @@ declare class ExecError extends VaultError {
148
249
  constructor(message: string, command: string);
149
250
  }
150
251
  /**
151
- * Thrown when a JWE string cannot be parsed because it is structurally
152
- * malformed (e.g. wrong number of segments, invalid Base64URL header,
153
- * or unparseable JSON header).
252
+ * Thrown when a JWE string is invalid or cannot be processed for example,
253
+ * it is structurally malformed (wrong number of segments, invalid
254
+ * Base64URL), decryption fails (wrong key, tampered ciphertext), or the
255
+ * decrypted payload does not match the expected claims schema.
154
256
  *
155
257
  * @public
156
258
  */
@@ -167,8 +269,10 @@ declare class AccessorConsumedError extends VaultError {
167
269
  constructor(message: string);
168
270
  }
169
271
  /**
170
- * Thrown when a caller requests a signing/verification algorithm that is not
171
- * in the allowed set (e.g. `'md5'`).
272
+ * Thrown when a caller requests a signing key algorithm that is not a
273
+ * supported JOSE algorithm identifier. The signing algorithm registry uses
274
+ * strict JOSE identifiers (currently `'EdDSA'`); an unrecognized value is
275
+ * rejected rather than defaulted.
172
276
  *
173
277
  * @public
174
278
  */
@@ -183,6 +287,183 @@ declare class InvalidAlgorithmError extends VaultError {
183
287
  readonly allowed: string[];
184
288
  constructor(message: string, algorithm: string, allowed: string[]);
185
289
  }
290
+ /**
291
+ * Thrown when signing-key material cannot be parsed. Two paths raise it. During
292
+ * verification, the supplied public key is not a structurally parseable SPKI PEM
293
+ * public key (or a private key was passed where a public key is required); this
294
+ * is an operational fault distinct from a signature that simply does not verify,
295
+ * which returns `false`. During key use, a stored signing key decrypts cleanly
296
+ * but is not valid private key material — corrupt or tampered on disk — when
297
+ * exporting its public half or signing with it (`getPublicKey`/`signWithKey`).
298
+ * The message never echoes any part of the key material.
299
+ *
300
+ * @public
301
+ */
302
+ declare class InvalidKeyMaterialError extends VaultError {
303
+ constructor(message: string);
304
+ }
305
+ /**
306
+ * Thrown when a named signing key does not exist in the active backend — for
307
+ * example `key export` or `sign` is asked for a name that was never enrolled
308
+ * with `key create`. This is distinct from {@link SecretNotFoundError}: signing
309
+ * keys occupy their own namespace and are never returned as ordinary secrets.
310
+ *
311
+ * @public
312
+ */
313
+ declare class SigningKeyNotFoundError extends VaultError {
314
+ /**
315
+ * The signing-key name that was requested (the caller-facing `--name`, not
316
+ * the internal namespaced identifier).
317
+ */
318
+ readonly keyName: string;
319
+ constructor(message: string, keyName: string);
320
+ }
321
+ /**
322
+ * Thrown when `key create` (or `createSigningKey`) is asked to enroll a signing
323
+ * key under a name that already exists. Enrollment never silently overwrites an
324
+ * existing key, because a regenerated keypair would invalidate every public key
325
+ * that was previously exported and pinned by a verifier.
326
+ *
327
+ * @public
328
+ */
329
+ declare class SigningKeyAlreadyExistsError extends VaultError {
330
+ /**
331
+ * The signing-key name that already exists (the caller-facing `--name`, not
332
+ * the internal namespaced identifier).
333
+ */
334
+ readonly keyName: string;
335
+ constructor(message: string, keyName: string);
336
+ }
337
+ /**
338
+ * Thrown when a signing operation (`key create`, `key export`, `sign`) is
339
+ * requested against a backend that does not implement the signing contract.
340
+ * Signing is never silently emulated on a backend that cannot perform it in a
341
+ * key-stays-backend-side manner; inspect {@link SigningNotSupportedError.builtInSigningBackends}
342
+ * for the built-in backend types that do.
343
+ *
344
+ * @public
345
+ */
346
+ declare class SigningNotSupportedError extends VaultError {
347
+ /** The type identifier of the active backend that cannot sign. */
348
+ readonly backendType: string;
349
+ /**
350
+ * The **built-in** backend type identifiers known to implement the signing
351
+ * contract (currently just the `file` backend). This is deliberately a
352
+ * static list of built-ins, not a live capability survey: a consumer that
353
+ * registers its own {@link SigningBackend} is not enumerated here, because
354
+ * discovering that would require instantiating every registered backend —
355
+ * a side effect that must not happen on an error path. A caller can still
356
+ * point a user at a working built-in, and custom backends may implement the
357
+ * contract independently.
358
+ */
359
+ readonly builtInSigningBackends: string[];
360
+ constructor(message: string, backendType: string, builtInSigningBackends: string[]);
361
+ }
362
+ /**
363
+ * Thrown when an encrypted-at-rest secret entry cannot be decrypted — e.g.
364
+ * the stored ciphertext is truncated/corrupted or the AES-GCM auth tag fails
365
+ * to verify. The message never echoes any part of the secret or key
366
+ * material.
367
+ *
368
+ * @public
369
+ */
370
+ declare class DecryptionError extends VaultError {
371
+ /**
372
+ * The path of the encrypted entry that failed to decrypt.
373
+ */
374
+ readonly path: string;
375
+ constructor(message: string, path: string);
376
+ }
377
+ /**
378
+ * Thrown when a delegated `fetch()` call fails before a `Response` can be
379
+ * produced — for example the URL is malformed or the underlying network
380
+ * request fails (DNS failure, connection refused, TLS error).
381
+ *
382
+ * @public
383
+ */
384
+ declare class FetchError extends VaultError {
385
+ /**
386
+ * The unresolved URL template that fetch failed to request, with
387
+ * `{{secret}}`/`{{secret:name}}` placeholders left intact. The
388
+ * placeholder-resolved URL is deliberately never stored here, so an
389
+ * injected secret is never exposed.
390
+ */
391
+ readonly url: string;
392
+ constructor(message: string, url: string);
393
+ }
394
+ /**
395
+ * Thrown when a config value fails structural or semantic validation (e.g. a
396
+ * whitespace-only `BackendConfig.path`).
397
+ */
398
+ declare class ConfigValidationError extends VaultError {
399
+ /**
400
+ * The dotted/bracketed path to the offending config field (e.g.
401
+ * `'backends[0].path'`).
402
+ */
403
+ readonly field: string;
404
+ /**
405
+ * The path of the config file that failed validation, when the error
406
+ * originated from loading a file on disk (via `loadConfig`) rather than
407
+ * from validating an in-memory value directly. This is `configDir` joined
408
+ * with `config.json` exactly as provided to `loadConfig` — it is not
409
+ * guaranteed to be absolute (`loadConfig` does not resolve a relative
410
+ * `configDir`).
411
+ */
412
+ readonly configFilePath: string | undefined;
413
+ constructor(message: string, field: string, configFilePath?: string);
414
+ }
415
+ /**
416
+ * Thrown when a config's `backends[].type` names a backend that is not
417
+ * registered with the {@link BackendRegistry}.
418
+ *
419
+ * A specialization of {@link ConfigValidationError}: an unknown backend type is
420
+ * a semantic schema failure (the config parses and is structurally valid, but
421
+ * names a backend that cannot be created), so it fails config validation the
422
+ * same way `version !== 1` does. It carries the offending `backendType` and the
423
+ * set of `knownTypes` so a consumer — notably `doctor` — can give the same
424
+ * "Available types: …" guidance the runtime {@link BackendUnavailableError}
425
+ * gives, without parsing the human-readable message. This closes the gap where
426
+ * a config with an unknown backend type parsed as valid JSON and passed
427
+ * `doctor` with a false "System ready.", only for the next real command to
428
+ * throw {@link BackendUnavailableError} (issue #215).
429
+ */
430
+ declare class UnknownBackendTypeError extends ConfigValidationError {
431
+ /** The unregistered backend type named in the config. */
432
+ readonly backendType: string;
433
+ /**
434
+ * The backend type identifiers that were registered when validation ran —
435
+ * the valid options to offer in remediation.
436
+ */
437
+ readonly knownTypes: string[];
438
+ constructor(message: string, field: string, backendType: string, knownTypes: string[], configFilePath?: string);
439
+ }
440
+ /**
441
+ * Thrown when a config file's contents cannot be parsed as JSON.
442
+ *
443
+ * The `message` already embeds the file path, the parse location (when the
444
+ * underlying `SyntaxError` exposes one), and a remediation hint that either
445
+ * points at `vaultkeeper config init` (via the separate `@vaultkeeper/cli`
446
+ * package — this library ships no CLI of its own) or at repairing/replacing
447
+ * the config directly through this library's JS API — see issues #68, #100.
448
+ * `path` and `location` are also exposed individually for callers (e.g.
449
+ * `doctor`) that want to report them as structured fields rather than
450
+ * re-parsing the message.
451
+ */
452
+ declare class ConfigParseError extends VaultError {
453
+ /**
454
+ * The path of the config file that failed to parse. This is `configDir`
455
+ * joined with `config.json` exactly as provided to `loadConfig` — it is
456
+ * not guaranteed to be absolute (`loadConfig` does not resolve a relative
457
+ * `configDir`).
458
+ */
459
+ readonly path: string;
460
+ /**
461
+ * A human-readable parse location (e.g. `'line 3, column 12'`), when one
462
+ * could be derived from the underlying `SyntaxError`.
463
+ */
464
+ readonly location: string | undefined;
465
+ constructor(message: string, path: string, location: string | undefined);
466
+ }
186
467
  /**
187
468
  * Thrown during initialization when a required system dependency (e.g. OpenSSL
188
469
  * or a native credential helper) is missing or incompatible.
@@ -195,20 +476,49 @@ declare class SetupError extends VaultError {
195
476
  constructor(message: string, dependency: string);
196
477
  }
197
478
  /**
198
- * Thrown when a filesystem operation fails due to a permission or access
199
- * problem (e.g. the config directory is not writable).
479
+ * Thrown when a filesystem operation fails. Common causes include a
480
+ * permission or access problem, for example the config directory is not
481
+ * writable, but the underlying failure may be any Node.js errno condition,
482
+ * for example the disk is full or a file was expected but a directory was
483
+ * found. Inspect the code property for the specific errno code when one is
484
+ * available.
200
485
  */
201
486
  declare class FilesystemError extends VaultError {
202
487
  /**
203
- * The absolute path of the file or directory that caused the error.
488
+ * The path of the file or directory that caused the error, as provided by
489
+ * the caller. Not guaranteed to be absolute — e.g. `loadConfig` throws this
490
+ * with `configDir` joined with `config.json` exactly as given, without
491
+ * resolving a relative `configDir`.
204
492
  */
205
493
  readonly path: string;
206
494
  /**
207
- * The permission level that was required but not available
208
- * (e.g. `'read'`, `'write'`, `'execute'`).
495
+ * The file operation or access mode that was being attempted when the
496
+ * failure occurred, for example 'read', 'write', 'delete', or 'rwx' for a
497
+ * directory create/access check. Despite the field name, this does not
498
+ * imply the failure was itself a permission problem — it names the
499
+ * attempted operation regardless of the underlying errno, which may be a
500
+ * non-permission code such as ENOSPC or EISDIR.
209
501
  */
210
502
  readonly permission: string;
211
- constructor(message: string, filePath: string, permission: string);
503
+ /**
504
+ * The Node.js errno code from the underlying filesystem failure, for
505
+ * example EACCES, EPERM, ENOSPC, or EISDIR. Undefined when the error was
506
+ * constructed without an underlying cause, or when that cause did not
507
+ * expose a string errno code. Prefer this over parsing the message text,
508
+ * which is not a contractual format.
509
+ */
510
+ readonly code: string | undefined;
511
+ /**
512
+ * @param message - Human-readable description of the failure.
513
+ * @param filePath - The path of the file or directory that caused the error.
514
+ * @param permission - The file operation or access mode being attempted,
515
+ * for example 'read', 'write', 'delete', or 'rwx'. See the `permission`
516
+ * property for why this need not indicate an actual permission problem.
517
+ * @param cause - The underlying error that was caught, if any. Recorded as
518
+ * the standard `Error.cause` and used to populate `code` when it exposes a
519
+ * string errno code.
520
+ */
521
+ constructor(message: string, filePath: string, permission: string, cause?: unknown);
212
522
  }
213
523
  /**
214
524
  * Thrown when a key rotation is requested while a previous rotation is still
@@ -221,12 +531,91 @@ declare class RotationInProgressError extends VaultError {
221
531
  /**
222
532
  * Shared types and interfaces for vaultkeeper.
223
533
  */
534
+
224
535
  /** Trust tier for executable identity verification. */
225
536
  type TrustTier = 1 | 2 | 3;
226
537
  /** Key status in the rotation lifecycle. */
227
538
  type KeyStatus = 'current' | 'previous' | 'deprecated';
228
- /** Status of a preflight check. */
229
- type PreflightCheckStatus = 'ok' | 'missing' | 'version-unsupported';
539
+ /**
540
+ * Status of a preflight check.
541
+ *
542
+ * `'invalid'` applies specifically to the `config` check: the config file
543
+ * exists but fails to parse or fails schema validation (see issue #68).
544
+ */
545
+ type PreflightCheckStatus = 'ok' | 'missing' | 'version-unsupported' | 'invalid';
546
+ /**
547
+ * The kind of error that made a preflight check fail, as a stable
548
+ * machine-readable discriminant. `'config-parse'` means the config file
549
+ * could not be parsed as JSON; `'config-validation'` means it parsed but
550
+ * failed schema validation; `'config-unknown-backend'` is a specific
551
+ * validation failure where `backends[].type` names a backend that is not
552
+ * registered, carrying the offending type and the valid options;
553
+ * `'config-read'` means the config file could not be read at all (for example
554
+ * a permission failure on the file or its parent directory) — a different
555
+ * remediation from parse/validation, since overwriting the file with
556
+ * `config init --force` cannot fix a read-permission problem.
557
+ */
558
+ type PreflightCheckErrorKind = 'config-parse' | 'config-validation' | 'config-unknown-backend' | 'config-read';
559
+ /**
560
+ * Structured, remediation-free error context for a failed preflight check.
561
+ *
562
+ * This carries the machine-readable facts a caller needs to build its own
563
+ * audience-appropriate remediation message, so a consumer never has to parse
564
+ * the human-readable `reason` prose. It is currently populated only for the
565
+ * `config` check when the config file is present but invalid.
566
+ *
567
+ * The `reason` field intentionally keeps the library's own remediation text
568
+ * (which points a library consumer at installing the CLI); a consumer that
569
+ * ships its own CLI should read this structured field instead and phrase the
570
+ * remediation itself.
571
+ */
572
+ interface PreflightCheckError {
573
+ /** The kind of failure, as a stable machine-readable discriminant. */
574
+ kind: PreflightCheckErrorKind;
575
+ /**
576
+ * Path to the config file that failed to parse or validate, as derived from
577
+ * the doctor call's `configDir`. Not guaranteed to be absolute — it is
578
+ * `configDir` joined with `config.json` exactly as given, so it is relative
579
+ * when `configDir` is relative.
580
+ */
581
+ configPath: string;
582
+ /**
583
+ * Human-readable parse location within the config file (for example
584
+ * `line 3, column 12`), present only for a `'config-parse'` failure.
585
+ */
586
+ location?: string | undefined;
587
+ /**
588
+ * The dotted/bracketed path to the offending config field (for example
589
+ * `backends` or `backends[0].path`), present for a `'config-validation'` or
590
+ * `'config-unknown-backend'` failure. This is the validation analogue of
591
+ * `location`: it lets a consumer point the user at exactly which field
592
+ * failed schema validation, the way `location` points at a parse position,
593
+ * without reusing the human-readable `reason` prose (which carries the
594
+ * library's own "install @vaultkeeper/cli" remediation).
595
+ */
596
+ field?: string | undefined;
597
+ /**
598
+ * The unregistered backend type named in `backends[].type`, present only for
599
+ * a `'config-unknown-backend'` failure. Lets a consumer echo the offending
600
+ * type in its remediation without parsing the `reason` prose.
601
+ */
602
+ backendType?: string | undefined;
603
+ /**
604
+ * The backend type identifiers that were registered when validation ran —
605
+ * the valid options — present only for a `'config-unknown-backend'` failure.
606
+ * Lets a consumer offer the same "Available types: …" guidance the runtime
607
+ * `BackendUnavailableError` gives.
608
+ */
609
+ knownBackendTypes?: readonly string[] | undefined;
610
+ /**
611
+ * The Node.js errno code (for example `EACCES`, `EPERM`, `EISDIR`) from the
612
+ * underlying filesystem failure, present only for a `'config-read'` failure
613
+ * and only when the cause exposed a string errno code. Lets a consumer
614
+ * distinguish a permission problem from another read failure when phrasing
615
+ * the remediation.
616
+ */
617
+ code?: string | undefined;
618
+ }
230
619
  /** Result of a preflight check for a single dependency. */
231
620
  interface PreflightCheck {
232
621
  /** Human-readable name of the dependency being checked. */
@@ -237,11 +626,30 @@ interface PreflightCheck {
237
626
  version?: string | undefined;
238
627
  /** Human-readable explanation of why the status is not `'ok'`. */
239
628
  reason?: string | undefined;
629
+ /**
630
+ * Structured, remediation-free error context when this check failed with a
631
+ * recognized error, so a caller can build its own remediation message
632
+ * instead of parsing the `reason` prose. Populated only for the `config`
633
+ * check when the config file is present but invalid.
634
+ */
635
+ error?: PreflightCheckError | undefined;
636
+ }
637
+ /**
638
+ * A {@link PreflightCheck} scoped by whether its dependency is required for
639
+ * the active/configured backend(s). Plugin-backend checks (`op`, `ykman`)
640
+ * are `required: false` when their backend isn't enabled — a non-`'ok'`
641
+ * status there is informational, not a system-readiness blocker (issue
642
+ * #116). They are promoted to `required: true` when their backend is
643
+ * explicitly enabled (e.g. `--backend yubikey` requires `ykman`).
644
+ */
645
+ interface ScopedPreflightCheck extends PreflightCheck {
646
+ /** Whether this dependency is required by the active/configured backend(s). */
647
+ required: boolean;
240
648
  }
241
649
  /** Aggregated result from all preflight checks. */
242
650
  interface PreflightResult {
243
651
  /** Individual check results, one per dependency inspected. */
244
- checks: PreflightCheck[];
652
+ checks: ScopedPreflightCheck[];
245
653
  /** `true` if all required checks passed and the system is ready. */
246
654
  ready: boolean;
247
655
  /** Non-fatal advisory messages about optional missing dependencies. */
@@ -260,51 +668,69 @@ interface VaultResponse {
260
668
  * Request for delegated HTTP fetch.
261
669
  *
262
670
  * String values in `url`, `headers`, and `body` may include the placeholder
263
- * `{{secret}}`, which is replaced with the actual secret value immediately
264
- * before the request is sent.
671
+ * `{{secret}}` (single-token mode) or `{{secret:name}}` (multi-token mode),
672
+ * which are replaced with actual secret values immediately before the request
673
+ * is sent.
265
674
  */
266
675
  interface FetchRequest {
267
676
  /**
268
- * The target URL. May contain `{{secret}}` which is replaced with the secret
269
- * value before the fetch is executed (e.g. for API-key-in-URL patterns).
677
+ * The target URL. May contain `{{secret}}` or `{{secret:name}}` which is
678
+ * replaced with the secret value before the fetch is executed.
270
679
  */
271
680
  url: string;
272
681
  /** HTTP method (defaults to `'GET'` when omitted). */
273
682
  method?: string | undefined;
274
683
  /**
275
- * Request headers. Any header value may contain `{{secret}}`, which is
276
- * replaced with the secret value before the request is sent.
684
+ * Request headers. Any header value may contain `{{secret}}` or
685
+ * `{{secret:name}}`, which is replaced with the secret value before
686
+ * the request is sent.
277
687
  */
278
688
  headers?: Record<string, string> | undefined;
279
689
  /**
280
- * Request body. May contain `{{secret}}`, which is replaced with the secret
281
- * value before the request is sent.
690
+ * Request body. May contain `{{secret}}` or `{{secret:name}}`, which is
691
+ * replaced with the secret value before the request is sent.
282
692
  */
283
693
  body?: string | undefined;
284
694
  }
285
695
  /**
286
696
  * Request for delegated command execution.
287
697
  *
288
- * String values in `args` and `env` may include the placeholder `{{secret}}`,
289
- * which is replaced with the actual secret value immediately before the command
290
- * is spawned.
698
+ * String values in `env` may include the placeholder `{{secret}}`
699
+ * (single-token mode) or `{{secret:name}}` (multi-token mode), which are
700
+ * replaced with actual secret values immediately before the command is
701
+ * spawned. Placeholders are **not** supported in `command` or `args` —
702
+ * `VaultKeeper.exec()` throws `ExecError` if one appears there.
291
703
  */
292
704
  interface ExecRequest {
293
705
  /** The command (binary) to execute. */
294
706
  command: string;
295
707
  /**
296
- * Command-line arguments. Any argument may contain `{{secret}}`, which is
297
- * replaced with the secret value before the command is spawned.
708
+ * Command-line arguments. Secret placeholders (`{{secret}}` or
709
+ * `{{secret:name}}`) are **not** supported here process arguments are
710
+ * visible to other processes via `ps` and often collected in logs and
711
+ * telemetry. `VaultKeeper.exec()` throws `ExecError` if a placeholder
712
+ * appears in any argument. Use `env` to inject secrets instead.
298
713
  */
299
714
  args?: string[] | undefined;
300
715
  /**
301
716
  * Additional environment variables to merge into the child process
302
- * environment. Any value may contain `{{secret}}`, which is replaced with
303
- * the secret value before the command is spawned.
717
+ * environment. Any value may contain `{{secret}}` or `{{secret:name}}`,
718
+ * which is replaced with the secret value before the command is spawned.
304
719
  */
305
720
  env?: Record<string, string> | undefined;
306
721
  /** Working directory for the spawned process. */
307
722
  cwd?: string | undefined;
723
+ /**
724
+ * Whether to redact injected secret values from the captured `stdout` and
725
+ * `stderr` before they are returned. Defaults to `true`: every occurrence of
726
+ * an injected secret value in the captured output is replaced with
727
+ * `[REDACTED]`, so the raw secret never appears in {@link ExecResult} even
728
+ * when the spawned command echoes it. Set to `false` to receive the raw,
729
+ * unredacted output — only for callers that genuinely need it (for example
730
+ * output that legitimately contains the secret and must be preserved), since
731
+ * doing so forfeits the redaction guarantee.
732
+ */
733
+ redact?: boolean | undefined;
308
734
  }
309
735
  /** Result from delegated command execution. */
310
736
  interface ExecResult {
@@ -318,10 +744,11 @@ interface ExecResult {
318
744
  /**
319
745
  * Callback-based secret accessor with auto-zeroing.
320
746
  *
321
- * The accessor is backed by a revocable Proxy. Calling `read()` passes a
322
- * `Buffer` containing the secret to the callback, then zeroes the buffer after
323
- * the callback returns. The accessor can only be read once; a second call
324
- * throws.
747
+ * The accessor is backed by a Proxy and is single-use via an internal
748
+ * consumed flag. Calling `read()` passes a `Buffer` containing the secret to
749
+ * the callback, then zeroes the buffer after the callback returns and passes
750
+ * the callback's return value through. The accessor can only be read once; a
751
+ * second call throws.
325
752
  */
326
753
  interface SecretAccessor {
327
754
  /**
@@ -331,51 +758,81 @@ interface SecretAccessor {
331
758
  * as UTF-8. The buffer is zeroed immediately after the callback returns, so
332
759
  * callers must not store a reference to it beyond the callback scope.
333
760
  *
334
- * @param callback - Function that receives the secret buffer.
761
+ * The callback's return value is passed through, so a caller-derived result
762
+ * (for example `buf.toString()` or a hash) flows out of `read()`:
763
+ * `const digest = accessor.read((buf) => sha256(buf))`. To preserve the
764
+ * zero-copy, auto-zeroing contract, derive a new value inside the callback —
765
+ * never return the raw `buf` itself, which is zeroed before `read()` returns.
766
+ *
767
+ * @param callback - Function that receives the secret buffer and returns a
768
+ * caller-derived value.
769
+ * @returns Whatever the callback returns.
335
770
  * @throws {Error} If the accessor has already been consumed.
336
771
  */
337
- read(callback: (buf: Buffer) => void): void;
772
+ read<T>(callback: (buf: Buffer) => T): T;
338
773
  }
339
774
  /**
340
- * Request for delegated signing.
775
+ * A signing algorithm identifier from the strict JOSE registry (RFC 7518).
341
776
  *
342
- * The `data` field is the payload to sign. Strings are UTF-8-encoded
343
- * before signing.
777
+ * Only `'EdDSA'` (Ed25519) is supported today; the identifier is intentionally
778
+ * a strict JOSE `alg` value so future algorithms (`'ES256'`, `'RS256'`, …) each
779
+ * bind to their proper curve/key type rather than an ambiguous label.
344
780
  */
345
- interface SignRequest {
346
- /** The data to sign. Strings are treated as UTF-8. */
347
- data: string | Buffer;
781
+ type SigningAlgorithm = 'EdDSA';
782
+ /**
783
+ * The public half of an enrolled signing key.
784
+ *
785
+ * Returned by `key create` / `key export` and used to verify detached
786
+ * signatures independently of vaultkeeper.
787
+ */
788
+ interface SigningPublicKey {
789
+ /** SPKI (SubjectPublicKeyInfo) PEM encoding of the public key. */
790
+ publicKeyPem: string;
791
+ /** The JOSE algorithm this key signs with. */
792
+ algorithm: SigningAlgorithm;
348
793
  /**
349
- * Override the hash algorithm (`'sha256'`, `'sha384'`, or `'sha512'`).
350
- * Ignored for Ed25519/Ed448 keys where the algorithm is implicit.
351
- * Non-Edwards keys (RSA, EC) default to `'sha256'` when omitted.
352
- * Weak algorithms (e.g. `'md5'`, `'sha1'`) are rejected.
794
+ * Stable key identifier: the base64url-encoded SHA-256 of the SPKI DER. Used
795
+ * as the JWS `kid` protected-header value so a verifier can select the key.
353
796
  */
354
- algorithm?: string | undefined;
797
+ kid: string;
798
+ }
799
+ /**
800
+ * Request to sign a caller-supplied payload with a named signing key.
801
+ *
802
+ * The `payload` is arbitrary bytes to be signed with detachment (RFC 7797) —
803
+ * it is never stored and never treated as a secret. Strings are UTF-8-encoded
804
+ * before signing.
805
+ */
806
+ interface SignRequest {
807
+ /** The payload bytes to sign. Strings are treated as UTF-8. */
808
+ payload: string | Buffer;
355
809
  }
356
- /** Result from a delegated signing operation. */
810
+ /**
811
+ * Result of a signing operation.
812
+ *
813
+ * The signature is a detached-payload Compact JWS (RFC 7515 §7.2.2 + RFC 7797
814
+ * `b64:false`, `crit:["b64"]`): `<protected>..<signature>`, with the payload
815
+ * omitted. Any standards-compliant JOSE library can verify it given the
816
+ * detached payload and the public key.
817
+ */
357
818
  interface SignResult {
358
- /** Base64-encoded signature. */
359
- signature: string;
360
- /**
361
- * Algorithm label describing how the signature was produced.
362
- * For Edwards keys this is the key type (e.g. `'ed25519'`).
363
- * For other keys this matches the `algorithm` field from the request
364
- * (or the default `'sha256'`).
365
- */
366
- algorithm: string;
819
+ /** The detached-payload compact JWS (`<protected>..<signature>`). */
820
+ jws: string;
367
821
  }
368
822
  /**
369
- * Request for signature verification.
823
+ * Request for detached-signature verification.
370
824
  *
371
- * This is a static operation that only requires public key material —
372
- * no VaultKeeper instance or capability token is needed.
825
+ * This is a fully offline operation that only requires public key material —
826
+ * no VaultKeeper instance, backend, config, or capability token is needed.
373
827
  */
374
828
  interface VerifyRequest {
375
- /** The original data that was signed. Strings are treated as UTF-8. */
376
- data: string | Buffer;
377
- /** Base64-encoded signature to verify. */
378
- signature: string;
829
+ /** The detached payload bytes that were signed. Strings are treated as UTF-8. */
830
+ payload: string | Buffer;
831
+ /**
832
+ * The detached-payload compact JWS produced by {@link SignResult.jws}
833
+ * (`<protected>..<signature>`).
834
+ */
835
+ jws: string;
379
836
  /**
380
837
  * PEM-encoded public key (SPKI format) as a string.
381
838
  *
@@ -383,11 +840,6 @@ interface VerifyRequest {
383
840
  * accepted by this interface.
384
841
  */
385
842
  publicKey: string;
386
- /**
387
- * Override the hash algorithm. Ignored for Ed25519/Ed448 keys.
388
- * Non-Edwards keys default to `'sha256'` when omitted.
389
- */
390
- algorithm?: string | undefined;
391
843
  }
392
844
  /** Vaultkeeper configuration file structure. */
393
845
  interface VaultConfig {
@@ -430,20 +882,19 @@ interface BackendConfig {
430
882
  options?: Record<string, string> | undefined;
431
883
  }
432
884
 
433
- /**
434
- * Backend abstraction layer types for vaultkeeper.
435
- */
436
885
  /**
437
886
  * Factory function for creating a SecretBackend instance.
438
887
  *
439
888
  * @remarks
440
889
  * Factories may optionally accept a {@link BackendConfig} to configure the
441
- * backend from the user's vaultkeeper config file. Factories that do not need
442
- * configuration can ignore the parameter.
890
+ * backend from the user's vaultkeeper config file, and the resolved config
891
+ * directory (the same directory config and key material are read from) so
892
+ * file-based backends can default their storage under it. Factories that do
893
+ * not need either can ignore the parameters.
443
894
  *
444
895
  * @public
445
896
  */
446
- type BackendFactory = (config?: BackendConfig) => SecretBackend;
897
+ type BackendFactory = (config?: BackendConfig, configDir?: string) => SecretBackend;
447
898
  /**
448
899
  * Abstraction interface for all secret storage backends.
449
900
  *
@@ -505,6 +956,170 @@ interface ListableBackend extends SecretBackend {
505
956
  * @public
506
957
  */
507
958
  declare function isListableBackend(backend: SecretBackend): backend is ListableBackend;
959
+ /**
960
+ * A keyed backend operation that a presence-per-use requirement can gate.
961
+ *
962
+ * @remarks
963
+ * Used by {@link BackendCapabilities.presenceEnforcedOperations} to express that
964
+ * an instance forces a fresh per-use action for only *some* operations. `'read'`
965
+ * covers the secret read behind `setup`/`exec`; `'store'`, `'delete'`, and
966
+ * `'sign'` are the write, removal, and signing paths.
967
+ *
968
+ * @public
969
+ */
970
+ type PresenceOperation = 'read' | 'store' | 'delete' | 'sign';
971
+ /**
972
+ * The set of security capabilities a configured backend instance advertises.
973
+ *
974
+ * @remarks
975
+ * Capabilities describe what a **specific configured instance** guarantees, not
976
+ * what its backend *type* is generally able to do. Two instances of the same
977
+ * backend type can report different capabilities depending on their
978
+ * configuration (e.g. a YubiKey slot with a touch policy vs. one without, or
979
+ * 1Password in `per-access` vs. `session` mode). Never derive a capability from
980
+ * the backend's `type` alone.
981
+ *
982
+ * The shape is intentionally open to extension: new capability flags may be
983
+ * added over time, so consumers should read only the fields they understand and
984
+ * treat a missing/unknown field as absent.
985
+ *
986
+ * @public
987
+ */
988
+ interface BackendCapabilities {
989
+ /**
990
+ * `true` when this configured instance can force a distinct, fresh physical
991
+ * human action (e.g. a YubiKey touch, a gpg-smartcard tap, or a 1Password
992
+ * per-use biometric approval) — a deliberate action taken *for this operation,
993
+ * right now*, not merely "a vault was unlocked at some point."
994
+ *
995
+ * The guarantee is **operation-scoped**, not blanket: when `presencePerUse` is
996
+ * `true`, that fresh action is available and **non-bypassably enforced** for
997
+ * exactly the operations listed in
998
+ * {@link BackendCapabilities.presenceEnforcedOperations} (all keyed operations
999
+ * when that field is omitted). For a covered operation, a
1000
+ * {@link https://github.com/mike-north/vaultkeeper/issues/122 | `--require-presence-per-use`}
1001
+ * request drives a fresh action that cannot be satisfied from a cached or
1002
+ * session-unlocked state. For an operation **outside** that set, the request is
1003
+ * **refused** with a `NotCapableError` — it is never silently satisfied from a
1004
+ * cached unlock. A backend that only caches an unlock, or that is
1005
+ * encryption-only with no per-use action, reports `false`.
1006
+ *
1007
+ * Backends that do not implement {@link PresenceCapableBackend} are treated as
1008
+ * `false` by {@link getBackendCapabilities} — an unknown backend never
1009
+ * silently claims presence.
1010
+ */
1011
+ readonly presencePerUse: boolean;
1012
+ /**
1013
+ * The keyed operations for which this instance actually forces a fresh per-use
1014
+ * human action. When **omitted**, a `presencePerUse: true` instance is taken
1015
+ * to force presence for **all** keyed operations — the default for a touch
1016
+ * device (e.g. a YubiKey whose challenge-response touch fires on every
1017
+ * `store`/`retrieve`/`delete`).
1018
+ *
1019
+ * A backend that can force presence for only *some* operations must list
1020
+ * exactly those, so a `--require-presence-per-use` request for an **uncovered**
1021
+ * operation fails closed with a `NotCapableError` rather than silently passing
1022
+ * without a fresh action. For example, 1Password `per-access` forces a fresh
1023
+ * biometric on reads (`setup`/`exec`) but routes `store`/`delete` through the
1024
+ * cached session client, so it reports `['read']` — a flagged `store`/`delete`
1025
+ * is then correctly refused.
1026
+ *
1027
+ * Ignored when {@link BackendCapabilities.presencePerUse} is `false`.
1028
+ */
1029
+ readonly presenceEnforcedOperations?: readonly PresenceOperation[];
1030
+ }
1031
+ /**
1032
+ * Backend that can report its security {@link BackendCapabilities} for its
1033
+ * configured instance.
1034
+ *
1035
+ * @remarks
1036
+ * This is an optional extension interface, mirroring {@link ListableBackend} and
1037
+ * {@link SigningBackend}: it is **not** a required member of
1038
+ * {@link SecretBackend}. Prefer {@link getBackendCapabilities} over calling
1039
+ * {@link PresenceCapableBackend.getCapabilities} directly, so a backend that
1040
+ * does not implement the interface safely defaults to no capabilities rather
1041
+ * than being assumed to have them.
1042
+ *
1043
+ * {@link PresenceCapableBackend.getCapabilities} is asynchronous and describes
1044
+ * the **current configured/live state** of the instance — it must reflect
1045
+ * configuration (or a live device/session probe) rather than a hardcoded
1046
+ * per-type answer, and must not itself trigger a human-presence prompt.
1047
+ *
1048
+ * @public
1049
+ */
1050
+ interface PresenceCapableBackend extends SecretBackend {
1051
+ /**
1052
+ * Report the capabilities of this configured instance.
1053
+ * @returns The instance's {@link BackendCapabilities}.
1054
+ */
1055
+ getCapabilities(): Promise<BackendCapabilities>;
1056
+ }
1057
+ /**
1058
+ * Type guard for backends that implement the capability-reporting contract.
1059
+ * @public
1060
+ */
1061
+ declare function isPresenceCapableBackend(backend: SecretBackend): backend is PresenceCapableBackend;
1062
+ /**
1063
+ * Resolve a backend's {@link BackendCapabilities}, defaulting safely for
1064
+ * backends that do not implement {@link PresenceCapableBackend}.
1065
+ *
1066
+ * @remarks
1067
+ * A backend without the capability interface reports `{ presencePerUse: false }`
1068
+ * — an unknown backend never silently claims a security guarantee it cannot
1069
+ * prove. This is the only supported way to query capabilities; callers must not
1070
+ * assume a capability from a backend's `type`.
1071
+ *
1072
+ * @param backend - The backend instance to query.
1073
+ * @returns The instance's capabilities, or the safe default for a backend that
1074
+ * does not implement {@link PresenceCapableBackend}.
1075
+ * @public
1076
+ */
1077
+ declare function getBackendCapabilities(backend: SecretBackend): Promise<BackendCapabilities>;
1078
+ /**
1079
+ * Backend that can enroll and use signing keys entirely on its own side.
1080
+ *
1081
+ * @remarks
1082
+ * Signing keys are a distinct resource from secrets: a private key must never
1083
+ * flow through {@link SecretBackend.store}/{@link SecretBackend.retrieve} or a
1084
+ * capability token's claims. A signing backend generates the keypair, exposes
1085
+ * only the public half, and performs the signature itself — the private key
1086
+ * never leaves the backend. This is what keeps a key out of any JWE claims
1087
+ * token and lets a presence-per-use backend enforce its guarantee for signing.
1088
+ *
1089
+ * Implementations must keep signing keys in a namespace that cannot collide
1090
+ * with or be read as ordinary secrets.
1091
+ *
1092
+ * @public
1093
+ */
1094
+ interface SigningBackend extends SecretBackend {
1095
+ /**
1096
+ * Enroll a new signing keypair under `id`.
1097
+ * @param id - Namespaced signing-key identifier.
1098
+ * @param algorithm - The JOSE algorithm to generate a key for.
1099
+ * @throws If a signing key already exists under `id`, or `algorithm` is not
1100
+ * supported by this backend.
1101
+ */
1102
+ generateSigningKey(id: string, algorithm: SigningAlgorithm): Promise<void>;
1103
+ /**
1104
+ * Return the public half of the signing key stored under `id`.
1105
+ * @param id - Namespaced signing-key identifier.
1106
+ * @throws A `SigningKeyNotFoundError` if no signing key exists under `id`.
1107
+ */
1108
+ getPublicKey(id: string): Promise<SigningPublicKey>;
1109
+ /**
1110
+ * Sign `data` with the private key stored under `id`, returning the raw
1111
+ * signature bytes. The private key never leaves the backend.
1112
+ * @param id - Namespaced signing-key identifier.
1113
+ * @param data - The exact bytes to sign (e.g. a JWS signing input).
1114
+ * @throws A `SigningKeyNotFoundError` if no signing key exists under `id`.
1115
+ */
1116
+ signWithKey(id: string, data: Buffer): Promise<Buffer>;
1117
+ }
1118
+ /**
1119
+ * Type guard for backends that implement the signing contract.
1120
+ * @public
1121
+ */
1122
+ declare function isSigningBackend(backend: SecretBackend): backend is SigningBackend;
508
1123
 
509
1124
  /**
510
1125
  * Types for the backend setup protocol.
@@ -570,10 +1185,12 @@ declare class BackendRegistry {
570
1185
  * Create a backend instance by type.
571
1186
  * @param type - Backend type identifier
572
1187
  * @param config - Optional backend configuration forwarded to the factory
1188
+ * @param configDir - Optional resolved config directory forwarded to the
1189
+ * factory, so file-based backends can default their storage under it
573
1190
  * @returns A SecretBackend instance
574
1191
  * @throws {@link BackendUnavailableError} if the backend type is not registered
575
1192
  */
576
- static create(type: string, config?: BackendConfig): SecretBackend;
1193
+ static create(type: string, config?: BackendConfig, configDir?: string): SecretBackend;
577
1194
  /**
578
1195
  * Get all registered backend type identifiers.
579
1196
  * @returns Array of backend type identifiers
@@ -632,7 +1249,23 @@ declare class BackendRegistry {
632
1249
  * class fields enforce that no property on the token object leaks data.
633
1250
  */
634
1251
 
635
- /** Opaque capability token. Claims are inaccessible without `validateCapabilityToken`. */
1252
+ /**
1253
+ * An opaque handle to authorized secret claims.
1254
+ *
1255
+ * A `CapabilityToken` is produced by {@link VaultKeeper.authorize} and
1256
+ * deliberately exposes no readable data. The underlying claims — including the
1257
+ * secret value — are held in a module-private `WeakMap` that this class does
1258
+ * not reference, and its private fields keep any property from leaking them
1259
+ * (`toString()` returns only a debug identifier). There is intentionally no
1260
+ * public API for reading the claims directly.
1261
+ *
1262
+ * To use the secret, pass the token to a {@link VaultKeeper} access method —
1263
+ * {@link VaultKeeper.getSecret}, {@link VaultKeeper.fetch},
1264
+ * {@link VaultKeeper.exec}, or {@link VaultKeeper.sign} — which resolve the
1265
+ * claims internally.
1266
+ *
1267
+ * @public
1268
+ */
636
1269
  declare class CapabilityToken {
637
1270
  #private;
638
1271
  constructor();
@@ -675,6 +1308,18 @@ interface RunDoctorOptions {
675
1308
  * (backward-compatible behavior).
676
1309
  */
677
1310
  backends?: BackendConfig[];
1311
+ /**
1312
+ * When provided (and `backends` is not explicitly given), doctor loads and
1313
+ * validates the config file under this directory, adding a `config`
1314
+ * preflight check to the result. A present-but-invalid config file (parse
1315
+ * or schema failure) becomes a failing, required check — with the
1316
+ * underlying error's message (file path, parse location, remediation hint)
1317
+ * as `reason` — so an invalid config is visible in `doctor`'s output and
1318
+ * fails the overall `ready` result (issue #68). A missing config file is
1319
+ * not an error: `loadConfig` resolves platform defaults and the check
1320
+ * reports `ok`.
1321
+ */
1322
+ configDir?: string;
678
1323
  }
679
1324
  /**
680
1325
  * Run all platform-appropriate preflight checks and aggregate the results.
@@ -686,28 +1331,201 @@ declare function runDoctor(options?: RunDoctorOptions): Promise<PreflightResult>
686
1331
  * VaultKeeper main class — wires together all vaultkeeper subsystems.
687
1332
  */
688
1333
 
689
- /** Options for initializing VaultKeeper. */
1334
+ /**
1335
+ * Map of named secrets to their capability tokens.
1336
+ *
1337
+ * Use with `exec()` or `fetch()` to inject multiple secrets into a single
1338
+ * request. Each key becomes the name referenced in `{{secret:name}}`
1339
+ * placeholders.
1340
+ *
1341
+ * @example
1342
+ * ```ts
1343
+ * const { token: apiToken } = await vault.authorize(apiJwe)
1344
+ * const { token: dbToken } = await vault.authorize(dbJwe)
1345
+ *
1346
+ * await vault.exec(
1347
+ * { apiKey: apiToken, dbPass: dbToken },
1348
+ * { command: 'deploy', env: { API_KEY: '{{secret:apiKey}}', DB: '{{secret:dbPass}}' } },
1349
+ * )
1350
+ * ```
1351
+ *
1352
+ * @public
1353
+ */
1354
+ type SecretTokenMap = Record<string, CapabilityToken>;
1355
+ /**
1356
+ * Options for initializing VaultKeeper.
1357
+ *
1358
+ * @remarks
1359
+ * When neither `config` nor a config file (at `configDir`) is present, and
1360
+ * {@link VaultKeeperOptions.backend} is not set, the active backend falls back
1361
+ * to the safe zero-config default resolved by {@link defaultBackendType} — the
1362
+ * `file` backend, on every platform, so a missing config never silently
1363
+ * targets the real OS credential store. Inspect
1364
+ * {@link VaultKeeper.activeBackendType} after `init()` to confirm which backend
1365
+ * a given instance resolved to. To use the OS-native store instead, opt in via
1366
+ * an explicit config, or `vaultkeeper config init --backend <type>` from the
1367
+ * separate `@vaultkeeper/cli` package (see {@link platformNativeBackendType}).
1368
+ * When `backend` is set instead, see that option's own JSDoc for the fallback
1369
+ * config used in its place.
1370
+ */
690
1371
  interface VaultKeeperOptions {
691
1372
  /** Override the config directory. */
692
1373
  configDir?: string | undefined;
693
1374
  /** Supply config directly, skipping file load. */
694
1375
  config?: VaultConfig | undefined;
1376
+ /**
1377
+ * Inject a {@link SecretBackend} instance directly, bypassing the global
1378
+ * {@link BackendRegistry} and `config.backends` resolution entirely.
1379
+ *
1380
+ * This is the primary hook for tests and embedders that want to store and
1381
+ * retrieve secrets without registering a backend globally or hand
1382
+ * assembling a full {@link VaultConfig}. When set, this backend instance is
1383
+ * used for every `store()`/`retrieve()`/`setup()` call.
1384
+ *
1385
+ * **Precedence with `config`/`configDir`:**
1386
+ * - `backend` always wins over the backend that `config.backends` (or the
1387
+ * config loaded from `configDir`) would otherwise resolve — the
1388
+ * `backends` array is never consulted when `backend` is set.
1389
+ * - Other config fields (`keyRotation`, `defaults`, `developmentMode`)
1390
+ * still come from `config`, or from the config loaded from `configDir`,
1391
+ * when either is provided.
1392
+ * - If `backend` is set and `config` is omitted, a minimal built-in
1393
+ * default config is used instead of loading one from `configDir` — so a
1394
+ * caller that only needs an injected backend never has to construct a
1395
+ * {@link VaultConfig} or touch `configDir` at all.
1396
+ */
1397
+ backend?: SecretBackend | undefined;
695
1398
  /** Skip the doctor preflight check. */
696
1399
  skipDoctor?: boolean | undefined;
697
1400
  }
698
- /** Options for the setup operation. */
699
- interface SetupOptions {
1401
+ /**
1402
+ * Options common to every backend-touching operation that can be gated on a
1403
+ * fresh, per-use human presence action.
1404
+ *
1405
+ * @remarks
1406
+ * When `requirePresencePerUse` is `true`, the operation refuses — with a
1407
+ * {@link NotCapableError}, before any credential, session, or device is touched
1408
+ * — unless the active backend's configured instance advertises
1409
+ * {@link BackendCapabilities.presencePerUse}. When the backend is capable, the
1410
+ * operation proceeds through the backend's ordinary path, which (by the meaning
1411
+ * of the capability) forces a distinct fresh human action for this specific
1412
+ * call; a cached or session-unlocked state can never satisfy it. Capabilities
1413
+ * are queried fresh on every call and never cached across operations.
1414
+ *
1415
+ * @public
1416
+ */
1417
+ interface PresenceRequirementOptions {
1418
+ /**
1419
+ * Require the active backend to force a fresh, per-use human presence action
1420
+ * for this operation. Defaults to `false` (no presence requirement).
1421
+ */
1422
+ requirePresencePerUse?: boolean | undefined;
1423
+ }
1424
+ /**
1425
+ * Options for {@link VaultKeeper.setup} that are independent of the mandatory
1426
+ * executable-trust choice. Intersected with that choice to form
1427
+ * {@link SetupOptions}.
1428
+ *
1429
+ * @public
1430
+ */
1431
+ interface SetupOptionsBase extends PresenceRequirementOptions {
700
1432
  /** TTL in minutes for the JWE. */
701
1433
  ttlMinutes?: number | undefined;
702
1434
  /** Usage limit (null for unlimited). */
703
1435
  useLimit?: number | null | undefined;
704
- /** Executable path for identity binding. Use "dev" for dev mode. */
705
- executablePath?: string | undefined;
706
1436
  /** Trust tier override. */
707
1437
  trustTier?: TrustTier | undefined;
708
1438
  /** Backend type to use. */
709
1439
  backendType?: string | undefined;
710
1440
  }
1441
+ /**
1442
+ * Options for the setup operation.
1443
+ *
1444
+ * @remarks
1445
+ * The executable-trust choice is **mandatory and mutually exclusive**, and the
1446
+ * type system enforces it: `SetupOptions` is {@link SetupOptionsBase}
1447
+ * intersected with a choice of **exactly one** of `executablePath` (run
1448
+ * trust-on-first-use verification — the production choice) or `skipTrust: true`
1449
+ * (deliberately skip verification — development only). An options object with
1450
+ * **neither** field, or with **both**, fails to typecheck; and because
1451
+ * {@link VaultKeeper.setup}'s options argument is required, `vault.setup('NAME')`
1452
+ * and `vault.setup('NAME', {})` are compile-time type errors rather than
1453
+ * runtime-only failures. {@link ExecutableTrustRequiredError} remains a runtime
1454
+ * backstop for callers without static typing (e.g. plain JavaScript), and is
1455
+ * still thrown if `executablePath` is the retired legacy `'dev'` sentinel.
1456
+ *
1457
+ * @public
1458
+ */
1459
+ type SetupOptions = SetupOptionsBase & ({
1460
+ /**
1461
+ * Path to the calling executable, used to bind the minted token to that
1462
+ * executable's identity. `setup()` runs trust-on-first-use (TOFU)
1463
+ * verification: the file is hashed (SHA-256) and checked against the
1464
+ * local trust manifest. This is the safe, production choice.
1465
+ *
1466
+ * A path registered via `setDevelopmentMode` is still exempted from
1467
+ * hashing (the established development-mode allowlist); any other path
1468
+ * is verified. Mutually exclusive with `skipTrust`.
1469
+ *
1470
+ * **Rebuild caveat:** for a compiled or bundled entry point the file's
1471
+ * hash changes on every rebuild, so binding to a dev build target (e.g.
1472
+ * `process.argv[1]`) makes the next `setup()` after a recompile throw
1473
+ * `IdentityMismatchError`. In production point this at a **stable**
1474
+ * artifact — a released binary, or `process.execPath` to trust the Node
1475
+ * runtime; for a frequently-rebuilt local caller you want to keep
1476
+ * verifying, use `setDevelopmentMode` instead.
1477
+ */
1478
+ executablePath: string;
1479
+ skipTrust?: never;
1480
+ } | {
1481
+ /**
1482
+ * Development-only escape hatch: skip executable-trust (TOFU)
1483
+ * verification. The minted token carries no executable identity binding.
1484
+ *
1485
+ * **Security warning:** a token minted with `skipTrust: true` is not
1486
+ * bound to any calling executable, so any process that obtains the JWE
1487
+ * can redeem it. Use this only in local development or tests — never in
1488
+ * production. Prefer `executablePath` so executable trust is actually
1489
+ * enforced. Mutually exclusive with `executablePath`.
1490
+ */
1491
+ skipTrust: true;
1492
+ executablePath?: never;
1493
+ });
1494
+ /**
1495
+ * Trust status of an executable, as recorded in the trust-on-first-use (TOFU)
1496
+ * trust manifest.
1497
+ *
1498
+ * Returned by {@link VaultKeeper.approveExecutable} and
1499
+ * {@link VaultKeeper.checkExecutableTrust}.
1500
+ *
1501
+ * @public
1502
+ */
1503
+ interface ExecutableTrustStatus {
1504
+ /**
1505
+ * Whether the executable's current hash is approved in the trust manifest.
1506
+ * When `false`, callers must obtain approval (e.g. an interactive prompt)
1507
+ * before granting secret access.
1508
+ */
1509
+ trusted: boolean;
1510
+ /** SHA-256 hex digest of the executable's current contents. */
1511
+ hash: string;
1512
+ /**
1513
+ * `true` when the executable is already known to the manifest but its
1514
+ * current hash does not match any approved value — a TOFU conflict. A
1515
+ * conflicting executable is never trusted; callers must re-approve or deny.
1516
+ */
1517
+ hashMismatch: boolean;
1518
+ /**
1519
+ * Hashes recorded as approved for this executable in the trust manifest, in
1520
+ * approval order (empty when the executable has never been approved). When
1521
+ * {@link ExecutableTrustStatus.hashMismatch} is `true`, these are the prior
1522
+ * approved values that {@link ExecutableTrustStatus.hash} no longer matches;
1523
+ * the last element is the most recently approved hash.
1524
+ */
1525
+ approvedHashes: readonly string[];
1526
+ /** Human-readable description of how trust was (or was not) established. */
1527
+ reason: string;
1528
+ }
711
1529
  /**
712
1530
  * Main entry point for vaultkeeper. Orchestrates backends, keys, JWE tokens,
713
1531
  * identity verification, and access patterns.
@@ -718,6 +1536,12 @@ declare class VaultKeeper {
718
1536
  /**
719
1537
  * Initialize a new VaultKeeper instance.
720
1538
  * Runs doctor checks (unless skipped), loads config, and sets up the key manager.
1539
+ *
1540
+ * The configured secret backend is resolved lazily on first use, not during
1541
+ * `init()`. Trust-only operations (e.g. {@link VaultKeeper.approveExecutable},
1542
+ * {@link VaultKeeper.checkExecutableTrust}) therefore succeed even when the
1543
+ * configured backend or plugin is unavailable or unregistered; a
1544
+ * misconfigured backend surfaces only when a secret operation is invoked.
721
1545
  */
722
1546
  static init(options?: VaultKeeperOptions): Promise<VaultKeeper>;
723
1547
  /**
@@ -730,6 +1554,59 @@ declare class VaultKeeper {
730
1554
  * @param options - Optional doctor options (e.g. `{ backends }` to scope checks).
731
1555
  */
732
1556
  static doctor(options?: RunDoctorOptions): Promise<PreflightResult>;
1557
+ /**
1558
+ * The type identifier of the active backend — the first enabled backend in
1559
+ * the resolved configuration (the one `store()`, `delete()`, and `setup()`
1560
+ * operate on).
1561
+ *
1562
+ * @remarks
1563
+ * This is a pure, side-effect-free view of the resolved configuration: it
1564
+ * reads the type of the first enabled backend without instantiating the
1565
+ * backend or requiring it to be registered or healthy. Reading it therefore
1566
+ * never throws for an unavailable or unregistered backend — unlike a secret
1567
+ * operation — so it is safe to call purely to introspect an instance.
1568
+ *
1569
+ * When a backend was injected via `init({ backend })`, config-based
1570
+ * resolution is bypassed entirely and this reports the injected instance's
1571
+ * declared `type` (see {@link SecretBackend}) — or the stable sentinel
1572
+ * `'custom'` if it declares an empty type. It never throws in the injected
1573
+ * path.
1574
+ *
1575
+ * Use it to confirm which backend an instance resolved to, especially when
1576
+ * no config file exists and the safe zero-config default applies (see
1577
+ * {@link defaultBackendType}). With no config this reads `file` on every
1578
+ * platform, so secret operations never silently target the real OS
1579
+ * credential store; opt into the native store (see
1580
+ * {@link platformNativeBackendType}) via explicit config to change this.
1581
+ *
1582
+ * @throws A {@link BackendUnavailableError} only when the configuration has
1583
+ * no enabled backend at all (a configuration error, not a backend fault).
1584
+ * This can only happen in the config-driven path; an injected backend never
1585
+ * throws here.
1586
+ *
1587
+ * @public
1588
+ */
1589
+ get activeBackendType(): string;
1590
+ /**
1591
+ * Report the {@link BackendCapabilities} of the active backend's configured
1592
+ * instance — the same backend {@link VaultKeeper.store}, {@link VaultKeeper.setup},
1593
+ * and {@link VaultKeeper.sign} operate on.
1594
+ *
1595
+ * @remarks
1596
+ * Unlike the pure {@link VaultKeeper.activeBackendType} getter, this resolves
1597
+ * (instantiates) the backend, because capabilities are a property of the
1598
+ * configured instance, not of the type. The answer reflects configured/live
1599
+ * state — e.g. a YubiKey slot's touch policy or 1Password's access mode — and
1600
+ * a backend that does not implement {@link PresenceCapableBackend} reports the
1601
+ * safe default (`{ presencePerUse: false }`) via {@link getBackendCapabilities}.
1602
+ * Reading this never triggers a human-presence prompt.
1603
+ *
1604
+ * @returns The active backend instance's capabilities.
1605
+ * @throws A {@link BackendUnavailableError} if no backend is enabled or the
1606
+ * configured backend cannot be built.
1607
+ * @public
1608
+ */
1609
+ getActiveBackendCapabilities(): Promise<BackendCapabilities>;
733
1610
  /**
734
1611
  * Store a secret in the configured backend.
735
1612
  *
@@ -737,11 +1614,20 @@ declare class VaultKeeper {
737
1614
  * `store()` method. If a secret with the same name already exists, it is
738
1615
  * overwritten.
739
1616
  *
740
- * @param name - Identifier for the secret.
1617
+ * @param name - Identifier for the secret. Must not contain `':'` — that
1618
+ * character is reserved for the internal `signing-key:` namespace, so a
1619
+ * secret name can never collide with a signing key.
741
1620
  * @param value - The secret value to store.
1621
+ * @param options - Optional {@link PresenceRequirementOptions}. When
1622
+ * `requirePresencePerUse` is set, the store is refused with a
1623
+ * {@link NotCapableError} before the backend is touched unless the active
1624
+ * backend forces a fresh per-use human action.
1625
+ * @throws {VaultError} If `name` is empty or contains `':'`.
1626
+ * @throws {@link NotCapableError} If `options.requirePresencePerUse` is set
1627
+ * and the active backend is not presence-per-use capable.
742
1628
  * @public
743
1629
  */
744
- store(name: string, value: string): Promise<void>;
1630
+ store(name: string, value: string, options?: PresenceRequirementOptions): Promise<void>;
745
1631
  /**
746
1632
  * Delete a secret from the configured backend.
747
1633
  *
@@ -749,17 +1635,80 @@ declare class VaultKeeper {
749
1635
  * `delete()` method.
750
1636
  *
751
1637
  * @param name - Identifier for the secret to delete.
1638
+ * @param options - Optional {@link PresenceRequirementOptions}. When
1639
+ * `requirePresencePerUse` is set, the delete is refused with a
1640
+ * {@link NotCapableError} before the backend is touched unless the active
1641
+ * backend forces a fresh per-use human action.
1642
+ * @throws {@link NotCapableError} If `options.requirePresencePerUse` is set
1643
+ * and the active backend is not presence-per-use capable.
1644
+ * @public
1645
+ */
1646
+ delete(name: string, options?: PresenceRequirementOptions): Promise<void>;
1647
+ /**
1648
+ * Check whether a secret exists in the active backend, without retrieving
1649
+ * its value, minting a token, or touching the TOFU trust manifest.
1650
+ *
1651
+ * @remarks
1652
+ * This is a lightweight precondition check intended to run before any
1653
+ * interactive or trust-gating logic (e.g. `exec`'s caller-approval prompt),
1654
+ * so a nonexistent secret is reported immediately and unambiguously instead
1655
+ * of being masked by an unrelated approval failure — see issue #69.
1656
+ *
1657
+ * @param name - Identifier for the secret.
1658
+ * @returns `true` if the secret exists, `false` otherwise.
752
1659
  * @public
753
1660
  */
754
- delete(name: string): Promise<void>;
1661
+ secretExists(name: string): Promise<boolean>;
755
1662
  /**
756
1663
  * Read a stored secret from the backend and mint a JWE token that encapsulates it.
757
1664
  *
758
- * @param secretName - Identifier for the secret
759
- * @param options - Setup options
1665
+ * @remarks
1666
+ * `setup()` requires an explicit executable-trust decision — it has no
1667
+ * default and never silently skips verification. Pass `executablePath` (the
1668
+ * calling executable's real path) to run trust-on-first-use verification, or
1669
+ * `skipTrust: true` to deliberately skip it in development. The {@link SetupOptions}
1670
+ * type enforces this choice at compile time (exactly one, and the options
1671
+ * argument is required); {@link ExecutableTrustRequiredError} is the runtime
1672
+ * backstop for untyped callers.
1673
+ *
1674
+ * **Seeing a compile error here?** A bare `vault.setup('NAME')` or
1675
+ * `vault.setup('NAME', {})` fails to typecheck (e.g. TS2554 "Expected 2
1676
+ * arguments, but got 1" or TS2345 "Argument … is not assignable") precisely
1677
+ * because the mandatory trust choice is missing. The fix is to add **exactly
1678
+ * one** of `executablePath: '<path>'` (verify the caller — production) or
1679
+ * `skipTrust: true` (skip verification — development only). Supplying both
1680
+ * fails to typecheck for the same reason.
1681
+ *
1682
+ * @example
1683
+ * ```ts
1684
+ * // Production: bind the token to a STABLE executable so a swapped binary is
1685
+ * // rejected. Point executablePath at a released binary, or process.execPath
1686
+ * // to trust the Node runtime. Do NOT use process.argv[1] for a compiled entry
1687
+ * // point — its hash changes on every rebuild, so the next setup() after a
1688
+ * // recompile throws IdentityMismatchError (use setDevelopmentMode or
1689
+ * // skipTrust for a frequently-rebuilt local caller).
1690
+ * const jwe = await vault.setup('MY_API_KEY', { executablePath: '/usr/local/bin/my-tool' })
1691
+ *
1692
+ * // Local development: skip verification so rebuilds don't reject the caller.
1693
+ * const devJwe = await vault.setup('MY_API_KEY', { skipTrust: true })
1694
+ * ```
1695
+ *
1696
+ * @param secretName - Identifier for the secret. Must not contain `':'` (the
1697
+ * reserved `signing-key:` namespace separator).
1698
+ * @param options - Setup options; must carry exactly one of `executablePath`
1699
+ * or `skipTrust: true`
760
1700
  * @returns Compact JWE string
1701
+ * @throws {VaultError} If `secretName` is empty or contains `':'`.
1702
+ * @throws {@link ExecutableTrustRequiredError} If neither `executablePath`
1703
+ * nor `skipTrust: true` is provided, if both are, or if `executablePath` is
1704
+ * the retired legacy `'dev'` opt-out sentinel (use `skipTrust: true`).
1705
+ * @throws {@link IdentityMismatchError} If `executablePath`'s current hash no
1706
+ * longer matches a previously approved value (TOFU conflict).
1707
+ * @throws {@link FilesystemError} If `executablePath` cannot be read or hashed
1708
+ * for verification, or the trust manifest cannot be read or written while
1709
+ * recording the executable.
761
1710
  */
762
- setup(secretName: string, options?: SetupOptions): Promise<string>;
1711
+ setup(secretName: string, options: SetupOptions): Promise<string>;
763
1712
  /**
764
1713
  * Decrypt a JWE, validate claims, verify executable identity, and return
765
1714
  * an opaque CapabilityToken.
@@ -775,40 +1724,65 @@ declare class VaultKeeper {
775
1724
  vaultResponse: VaultResponse;
776
1725
  }>;
777
1726
  /**
778
- * Execute a delegated HTTP fetch, injecting the secret from the token.
1727
+ * Execute a delegated HTTP fetch, injecting secrets from the token(s).
779
1728
  *
780
- * The secret value is substituted for every `{{secret}}` placeholder found
781
- * in `request.url`, `request.headers`, and `request.body` before the fetch
782
- * is executed. The raw secret is never exposed in the return value.
1729
+ * **Single token:** every `{{secret}}` placeholder in `request.url`,
1730
+ * `request.headers`, and `request.body` is replaced with the secret value.
783
1731
  *
784
- * @param token - A `CapabilityToken` obtained from `authorize()`.
785
- * @param request - The fetch request template. Use `{{secret}}` as a
786
- * placeholder wherever the secret value should be injected.
1732
+ * **Token map ({@link SecretTokenMap}):** every `{{secret:name}}` placeholder
1733
+ * is replaced with the secret from the corresponding named token.
1734
+ *
1735
+ * The raw secret is never exposed in the return value.
1736
+ *
1737
+ * @param token - A single `CapabilityToken` or a `SecretTokenMap` mapping
1738
+ * names to tokens obtained from `authorize()`.
1739
+ * @param request - The fetch request template with placeholders.
787
1740
  * @returns The `Response` from the underlying `fetch()` call, together with
788
1741
  * the vault metadata (`vaultResponse`).
789
- * @throws {Error} If `token` is invalid or was not created by this vault
790
- * instance.
1742
+ * @throws {AuthorizationDeniedError} If any token is invalid or was not
1743
+ * created by this vault instance.
1744
+ * @throws {VaultError} If a named placeholder references an unknown
1745
+ * secret name.
1746
+ * @throws {FetchError} If the URL is malformed or the underlying network
1747
+ * request fails.
791
1748
  */
792
- fetch(token: CapabilityToken, request: FetchRequest): Promise<{
1749
+ fetch(token: CapabilityToken | SecretTokenMap, request: FetchRequest): Promise<{
793
1750
  response: Response;
794
1751
  vaultResponse: VaultResponse;
795
1752
  }>;
796
1753
  /**
797
- * Execute a delegated command, injecting the secret from the token.
1754
+ * Execute a delegated command, injecting secrets from the token(s).
798
1755
  *
799
- * The secret value is substituted for every `{{secret}}` placeholder found
800
- * in `request.args` and `request.env` values before the process is spawned.
801
- * The raw secret is never exposed in the return value.
1756
+ * **Single token:** every `{{secret}}` placeholder in `request.env` values
1757
+ * is replaced with the secret value.
802
1758
  *
803
- * @param token - A `CapabilityToken` obtained from `authorize()`.
804
- * @param request - The exec request template. Use `{{secret}}` as a
805
- * placeholder wherever the secret value should be injected.
1759
+ * **Token map ({@link SecretTokenMap}):** every `{{secret:name}}` placeholder
1760
+ * is replaced with the secret from the corresponding named token.
1761
+ *
1762
+ * Secret placeholders are not supported in `request.command` or
1763
+ * `request.args` — process arguments are visible to other processes via
1764
+ * `ps` and often collected in logs and telemetry.
1765
+ *
1766
+ * The raw secret is never exposed in the return value: by default the
1767
+ * captured `stdout`/`stderr` is scrubbed of every injected secret value
1768
+ * (replaced with `[REDACTED]`), so a command that echoes the secret does not
1769
+ * leak it back. Pass `request.redact = false` to receive raw, unredacted
1770
+ * output — only when a caller genuinely needs it, since that forfeits the
1771
+ * guarantee.
1772
+ *
1773
+ * @param token - A single `CapabilityToken` or a `SecretTokenMap` mapping
1774
+ * names to tokens obtained from `authorize()`.
1775
+ * @param request - The exec request template with placeholders. Set
1776
+ * `redact: false` to opt out of output redaction.
806
1777
  * @returns The command result (`stdout`, `stderr`, `exitCode`) together with
807
1778
  * the vault metadata (`vaultResponse`).
808
- * @throws {Error} If `token` is invalid or was not created by this vault
809
- * instance.
1779
+ * @throws {AuthorizationDeniedError} If any token is invalid or was not
1780
+ * created by this vault instance.
1781
+ * @throws {ExecError} If the command cannot be started (e.g. ENOENT),
1782
+ * a placeholder references an unknown secret name, or a secret
1783
+ * placeholder appears in the `command` or `args` field.
810
1784
  */
811
- exec(token: CapabilityToken, request: ExecRequest): Promise<{
1785
+ exec(token: CapabilityToken | SecretTokenMap, request: ExecRequest): Promise<{
812
1786
  result: ExecResult;
813
1787
  vaultResponse: VaultResponse;
814
1788
  }>;
@@ -821,49 +1795,108 @@ declare class VaultKeeper {
821
1795
  *
822
1796
  * @param token - A `CapabilityToken` obtained from `authorize()`.
823
1797
  * @returns A `SecretAccessor` that can be read exactly once.
824
- * @throws {Error} If `token` is invalid or was not created by this vault
825
- * instance.
1798
+ * @throws {AuthorizationDeniedError} If `token` is invalid or was not created
1799
+ * by this vault instance, or if it is a signing-key token — a signing key
1800
+ * carries no secret and cannot be read through `getSecret()` (use `sign()`).
826
1801
  */
827
1802
  getSecret(token: CapabilityToken): SecretAccessor;
828
1803
  /**
829
- * Sign data using the private key embedded in a capability token.
1804
+ * Enroll a new signing keypair under `name` in the active backend.
830
1805
  *
831
- * The signing key is extracted from the token's encrypted claims, used
832
- * for a single `crypto.sign()` call, and never exposed to the caller.
833
- * The algorithm is auto-detected from the key type unless overridden
834
- * in the request.
1806
+ * The keypair is generated and stored entirely backend-side (see
1807
+ * {@link SigningBackend}); the private key never enters vault claims, a
1808
+ * capability token, or the caller's process. Signing keys occupy a namespace
1809
+ * distinct from secrets, so a signing key and a secret can share a name
1810
+ * without colliding, and a signing key can never be read as a secret.
835
1811
  *
836
- * @param token - A `CapabilityToken` obtained from `authorize()`.
837
- * @param request - The data to sign and optional algorithm override.
838
- * @returns The base64-encoded signature and algorithm label, together
839
- * with the vault metadata (`vaultResponse`).
840
- * @throws {VaultError} If `token` is invalid or was not created by this
841
- * vault instance.
842
- * @throws {InvalidAlgorithmError} If `request.algorithm` is not in the
843
- * allowed set (e.g. `'md5'`).
844
- */
845
- sign(token: CapabilityToken, request: SignRequest): Promise<{
1812
+ * @param name - Caller-facing signing key name. Must not contain `':'` (the
1813
+ * `signing-key:` namespace separator).
1814
+ * @param algorithm - The JOSE signing algorithm (currently only `'EdDSA'`).
1815
+ * @returns The public half of the newly enrolled key.
1816
+ * @throws {SigningNotSupportedError} If the active backend cannot sign.
1817
+ * @throws {InvalidAlgorithmError} If `algorithm` is not supported.
1818
+ * @throws {SigningKeyAlreadyExistsError} If a signing key already exists under `name`.
1819
+ * @throws {VaultError} If `name` is empty or contains `':'`.
1820
+ * @public
1821
+ */
1822
+ createSigningKey(name: string, algorithm: SigningAlgorithm): Promise<SigningPublicKey>;
1823
+ /**
1824
+ * Export the SPKI PEM public key for the signing key named `name`.
1825
+ *
1826
+ * @param name - Caller-facing signing key name. Must not contain `':'`.
1827
+ * @returns The public key material (SPKI PEM, algorithm, kid).
1828
+ * @throws {SigningNotSupportedError} If the active backend cannot sign.
1829
+ * @throws {SigningKeyNotFoundError} If no signing key exists under `name`.
1830
+ * @throws {VaultError} If `name` is empty or contains `':'`.
1831
+ * @public
1832
+ */
1833
+ exportPublicKey(name: string): Promise<SigningPublicKey>;
1834
+ /**
1835
+ * Mint a signing-key capability token for the key named `name`.
1836
+ *
1837
+ * The returned token carries only `{ kid, backendRef, keyType: 'signing-key' }`
1838
+ * — never any key material — and is accepted only by {@link VaultKeeper.sign}.
1839
+ * Passing it to `getSecret`/`fetch`/`exec` is rejected.
1840
+ *
1841
+ * @param name - Caller-facing signing key name. Must not contain `':'`.
1842
+ * @returns An opaque {@link CapabilityToken} usable with `sign()`.
1843
+ * @throws {SigningNotSupportedError} If the active backend cannot sign.
1844
+ * @throws {SigningKeyNotFoundError} If no signing key exists under `name`.
1845
+ * @throws {VaultError} If `name` is empty or contains `':'`.
1846
+ * @public
1847
+ */
1848
+ authorizeSigningKey(name: string): Promise<CapabilityToken>;
1849
+ /**
1850
+ * Sign a caller-supplied payload with a signing-key capability token.
1851
+ *
1852
+ * The signature is produced backend-side via {@link SigningBackend.signWithKey}
1853
+ * — the private key never leaves the backend and never appears in the token,
1854
+ * the claims, or this process. The result is a detached-payload Compact JWS
1855
+ * (RFC 7515 §7.2.2 + RFC 7797 `b64:false`, `crit:["b64"]`, `alg:EdDSA`) that
1856
+ * any standards-compliant JOSE library can verify given the payload and the
1857
+ * public key.
1858
+ *
1859
+ * @param token - A signing-key `CapabilityToken` from {@link VaultKeeper.authorizeSigningKey}.
1860
+ * @param request - The payload to sign.
1861
+ * @param options - Optional {@link PresenceRequirementOptions}. When
1862
+ * `requirePresencePerUse` is set, the signature is refused with a
1863
+ * {@link NotCapableError} before the backend is touched unless the active
1864
+ * backend forces a fresh per-use human action. When capable, a fresh
1865
+ * backend `signWithKey` round-trip is performed for this call — no cached
1866
+ * key material can satisfy it (the private key never leaves the backend).
1867
+ * @returns The detached compact JWS and vault metadata.
1868
+ * @throws {AuthorizationDeniedError} If `token` is invalid or is not a
1869
+ * signing-key token (e.g. an ordinary secret token).
1870
+ * @throws {SigningNotSupportedError} If the active backend cannot sign.
1871
+ * @throws {SigningKeyNotFoundError} If the referenced key no longer exists.
1872
+ * @throws {@link NotCapableError} If `options.requirePresencePerUse` is set
1873
+ * and the active backend is not presence-per-use capable.
1874
+ * @public
1875
+ */
1876
+ sign(token: CapabilityToken, request: SignRequest, options?: PresenceRequirementOptions): Promise<{
846
1877
  result: SignResult;
847
1878
  vaultResponse: VaultResponse;
848
1879
  }>;
849
1880
  /**
850
- * Verify a signature using a public key.
851
- *
852
- * This is a static method — no VaultKeeper instance, secrets, or
853
- * capability tokens are required. It is safe to call from CI or any
854
- * context that has access to public key material.
1881
+ * Verify a detached-payload Compact JWS against a public key — fully offline.
855
1882
  *
856
- * Returns `false` for invalid key material, malformed signatures, or
857
- * any verification failure (except disallowed algorithms, which throw).
1883
+ * This is a static, asynchronous method: no VaultKeeper instance, backend,
1884
+ * config, or capability token is required, so it is safe to call in CI or any
1885
+ * context holding only public material.
858
1886
  *
859
- * @throws {InvalidAlgorithmError} If `request.algorithm` is not in the
860
- * allowed set (e.g. `'md5'`).
1887
+ * Returns `false` for a signature that does not verify — a tampered payload,
1888
+ * the wrong key, or a structurally malformed JWS. It throws
1889
+ * {@link InvalidKeyMaterialError} only when the public key itself is not
1890
+ * parseable (or a private key was supplied) — an operational fault distinct
1891
+ * from a bad signature.
861
1892
  *
862
- * @param request - The data, signature, public key, and optional
863
- * algorithm override.
1893
+ * @param request - The detached payload, the JWS, and the SPKI PEM public key.
864
1894
  * @returns `true` if the signature is valid, `false` otherwise.
1895
+ * @throws {InvalidKeyMaterialError} If `request.publicKey` is not parseable
1896
+ * SPKI public key material.
1897
+ * @public
865
1898
  */
866
- static verify(request: VerifyRequest): boolean;
1899
+ static verify(request: VerifyRequest): Promise<boolean>;
867
1900
  /**
868
1901
  * Rotate the current encryption key.
869
1902
  *
@@ -897,6 +1930,202 @@ declare class VaultKeeper {
897
1930
  * to remove it.
898
1931
  */
899
1932
  setDevelopmentMode(executablePath: string, enabled: boolean): Promise<void>;
1933
+ /**
1934
+ * Approve an executable for trust-on-first-use by recording its current
1935
+ * SHA-256 hash in the trust manifest.
1936
+ *
1937
+ * After approval, {@link VaultKeeper.setup} and
1938
+ * {@link VaultKeeper.checkExecutableTrust} recognize the executable (matched
1939
+ * by its resolved absolute path and content hash) as trusted, so callers can
1940
+ * skip an interactive approval prompt.
1941
+ *
1942
+ * The operation is idempotent: approving the same, unchanged executable more
1943
+ * than once leaves a single manifest entry.
1944
+ *
1945
+ * @param executablePath - Path to the executable to approve. Resolved to an
1946
+ * absolute path before hashing and recording.
1947
+ * @returns The recorded trust status (always `trusted: true`).
1948
+ * @throws {FilesystemError} If the executable does not exist or cannot be read.
1949
+ * @public
1950
+ */
1951
+ approveExecutable(executablePath: string): Promise<ExecutableTrustStatus>;
1952
+ /**
1953
+ * Check whether an executable is trusted according to the trust manifest,
1954
+ * without modifying the manifest.
1955
+ *
1956
+ * This is a read-only probe. Unlike {@link VaultKeeper.setup}, it never
1957
+ * records a hash, so it can be used to decide whether an interactive approval
1958
+ * prompt is required before proceeding.
1959
+ *
1960
+ * @param executablePath - Path to the executable to check. Resolved to an
1961
+ * absolute path before hashing and lookup.
1962
+ * @returns The current trust status. `trusted` is `true` only when the
1963
+ * executable's current hash matches an approved manifest entry.
1964
+ * @throws {FilesystemError} If the executable does not exist or cannot be read.
1965
+ * @public
1966
+ */
1967
+ checkExecutableTrust(executablePath: string): Promise<ExecutableTrustStatus>;
900
1968
  }
901
1969
 
902
- export { AccessorConsumedError, AuthorizationDeniedError, type BackendConfig, type BackendFactory, BackendLockedError, BackendRegistry, type BackendSetupFactory, BackendUnavailableError, CapabilityToken, DeviceNotPresentError, ExecError, type ExecRequest, type ExecResult, type FetchRequest, FilesystemError, IdentityMismatchError, InvalidAlgorithmError, InvalidTokenError, KeyRevokedError, KeyRotatedError, type KeyStatus, type ListableBackend, type Platform, PluginNotFoundError, type PreflightCheck, type PreflightCheckStatus, type PreflightResult, RotationInProgressError, type RunDoctorOptions, type SecretAccessor, type SecretBackend, SecretNotFoundError, type SetupChoice, SetupError, type SetupOptions, type SetupQuestion, type SetupResult, type SignRequest, type SignResult, TokenExpiredError, TokenRevokedError, type TrustTier, UsageLimitExceededError, type VaultConfig, VaultError, VaultKeeper, type VaultKeeperOptions, type VaultResponse, type VerifyRequest, isListableBackend, runDoctor };
1970
+ /**
1971
+ * Secret redaction for captured process output.
1972
+ *
1973
+ * The single source of truth for how an injected secret value is scrubbed from
1974
+ * text. Both the library's delegated `exec()` (which buffers full output) and
1975
+ * the CLI's streaming `RedactingStream` (which redacts live) route their
1976
+ * substitution through {@link redactSecrets}, so the redaction behavior can
1977
+ * never drift between the two surfaces.
1978
+ */
1979
+ /**
1980
+ * The token substituted for a redacted secret value.
1981
+ *
1982
+ * @public
1983
+ */
1984
+ declare const REDACTED = "[REDACTED]";
1985
+ /**
1986
+ * Replace every occurrence of each secret value in `text` with `replacement`.
1987
+ *
1988
+ * This is used to scrub captured child-process `stdout`/`stderr` so a secret
1989
+ * injected into a delegated command never surfaces in the returned output.
1990
+ *
1991
+ * Before redacting, the secret set is normalized so masking is complete and
1992
+ * order-independent:
1993
+ *
1994
+ * - **Empty/whitespace-only values are dropped.** An empty string matches
1995
+ * between every character, and a whitespace-only value matches ubiquitous
1996
+ * whitespace, so redacting either would blank the text rather than a secret.
1997
+ * - **Duplicates are removed** so each distinct value is processed once.
1998
+ * - **Longer values are redacted first.** If one secret is a substring or
1999
+ * prefix of another — common for keys that share a prefix — redacting the
2000
+ * shorter one first would leave the longer secret's remaining suffix visible
2001
+ * (redacting `"abc"` before `"abc123"` yields `"[REDACTED]123"`, leaking
2002
+ * `"123"`). Sorting by length descending guarantees each longer value is
2003
+ * fully masked before any shorter substring pass runs.
2004
+ *
2005
+ * Both arguments are treated fully literally: the secret is matched as a
2006
+ * plain substring (never as a regular expression), and `replacement` is
2007
+ * inserted verbatim — `String.prototype.replaceAll`'s `$`-substitution
2008
+ * patterns are disabled, since a replacement containing `$&` would otherwise
2009
+ * re-expand to the matched secret and silently defeat the redaction.
2010
+ *
2011
+ * @param text - The text to scrub.
2012
+ * @param secrets - The secret values to redact. Every non-empty, non-whitespace
2013
+ * value has all of its occurrences replaced.
2014
+ * @param replacement - The token to substitute for each occurrence, inserted
2015
+ * literally. Defaults to {@link REDACTED}.
2016
+ * @returns `text` with every occurrence of each redactable secret replaced.
2017
+ *
2018
+ * @public
2019
+ */
2020
+ declare function redactSecrets(text: string, secrets: readonly string[], replacement?: string): string;
2021
+
2022
+ /**
2023
+ * Configuration loading, validation, and defaults for vaultkeeper.
2024
+ */
2025
+
2026
+ /**
2027
+ * Return the platform-appropriate default config directory, ignoring the
2028
+ * `VAULTKEEPER_CONFIG_DIR` environment variable.
2029
+ *
2030
+ * This is the location vaultkeeper falls back to when neither a CLI
2031
+ * `--config-dir` flag nor the `VAULTKEEPER_CONFIG_DIR` environment variable
2032
+ * is set: `%APPDATA%/vaultkeeper` on Windows, `~/.config/vaultkeeper`
2033
+ * elsewhere.
2034
+ *
2035
+ * Unlike `getDefaultConfigDir`, this deliberately does not consult
2036
+ * `VAULTKEEPER_CONFIG_DIR`, so a caller can tell whether an active config
2037
+ * directory differs from the machine default even when that difference came
2038
+ * from the environment variable — for example, to decide whether a printed
2039
+ * remediation command must carry an explicit `--config-dir` so it still
2040
+ * targets the right file in a fresh shell that does not have the environment
2041
+ * variable set.
2042
+ *
2043
+ * @public
2044
+ */
2045
+ declare function getPlatformDefaultConfigDir(): string;
2046
+ /**
2047
+ * Return the platform-appropriate default config directory.
2048
+ *
2049
+ * Resolution order: the `VAULTKEEPER_CONFIG_DIR` environment variable, then
2050
+ * the platform default (see `getPlatformDefaultConfigDir`). Consumers that
2051
+ * also support a higher-precedence override (e.g. a CLI flag) should check
2052
+ * that first and only fall back to this function when no override was
2053
+ * supplied.
2054
+ *
2055
+ * @public
2056
+ */
2057
+ declare function getDefaultConfigDir(): string;
2058
+ /**
2059
+ * The backend type vaultkeeper uses by default when no backend is explicitly
2060
+ * configured — the **`file`** backend, on every platform.
2061
+ *
2062
+ * @remarks
2063
+ * The zero-config default is deliberately the portable, self-contained
2064
+ * AES-256-GCM encrypted file backend rather than the platform-native OS
2065
+ * credential store. This guarantees that a bare {@link VaultKeeper.init} — or a
2066
+ * `vaultkeeper config init` (run via the separate `@vaultkeeper/cli` package)
2067
+ * with no `--backend` flag — can never silently write a secret into the
2068
+ * user's real login keychain (or Windows DPAPI store) before they have
2069
+ * chosen to. It also matches the WASM SDK, which always uses the file
2070
+ * backend.
2071
+ *
2072
+ * The OS-native store is still available as an explicit opt-in: pass
2073
+ * `--backend keychain` (macOS) / `--backend dpapi` (Windows) to
2074
+ * `vaultkeeper config init` (via `@vaultkeeper/cli`), or set
2075
+ * `{ type: 'keychain' | 'dpapi' }` directly in a config object/file. Use
2076
+ * {@link platformNativeBackendType} to discover which native store the current
2077
+ * platform offers.
2078
+ *
2079
+ * @returns The zero-config default backend type identifier (`'file'`).
2080
+ * @public
2081
+ */
2082
+ declare function defaultBackendType(): string;
2083
+ /**
2084
+ * Resolve the OS-native credential store type for the current platform.
2085
+ *
2086
+ * @remarks
2087
+ * This is **not** the zero-config default — {@link defaultBackendType} (always
2088
+ * `'file'`) is. This function reports which platform-native store a user can
2089
+ * explicitly opt into (e.g. via `vaultkeeper config init --backend keychain`
2090
+ * from the separate `@vaultkeeper/cli` package, or `{ type: 'keychain' }` in
2091
+ * a config object/file passed directly to this library):
2092
+ *
2093
+ * - **macOS** → `keychain` (macOS Keychain)
2094
+ * - **Windows** → `dpapi` (Windows DPAPI)
2095
+ * - **Linux** → `secret-tool` (Secret Service via `libsecret`; opting in
2096
+ * requires the `libsecret-tools` package)
2097
+ * - **any other platform** → `file` (no built-in native store integration, so
2098
+ * the portable AES-256-GCM encrypted file backend is the only option)
2099
+ *
2100
+ * Use it to tell the user which native store is available on their platform, or
2101
+ * to label the opt-in. It never affects what an unconfigured vault resolves to
2102
+ * — that is always {@link defaultBackendType} (`file`).
2103
+ *
2104
+ * @returns The OS-native backend type identifier for the current platform.
2105
+ * @public
2106
+ */
2107
+ declare function platformNativeBackendType(): string;
2108
+ /**
2109
+ * Load the vaultkeeper config from disk, falling back to platform defaults
2110
+ * only when the config file is missing (`ENOENT`).
2111
+ *
2112
+ * Any other read failure (e.g. `EACCES`, `EISDIR`) is a genuinely broken or
2113
+ * unreadable config and is rethrown as a {@link FilesystemError} rather than
2114
+ * silently defaulted — silently defaulting on a permissions error would hide
2115
+ * the problem from `doctor` and `config show` (issue #68). A present file
2116
+ * that fails to parse as JSON throws {@link ConfigParseError}; a present file
2117
+ * that parses but fails schema validation throws {@link ConfigValidationError}.
2118
+ * All three error messages include the config file path and a remediation
2119
+ * hint naming `vaultkeeper config init --force` (via the separate
2120
+ * `@vaultkeeper/cli` package) as well as the JS-API alternative of repairing
2121
+ * or replacing the config directly — the supported recovery paths for an
2122
+ * existing-but-broken config (issues #97, #100).
2123
+ *
2124
+ * @param configDir - Directory containing config.json. Defaults to
2125
+ * `getDefaultConfigDir()`, which itself honors `VAULTKEEPER_CONFIG_DIR`
2126
+ * before falling back to the platform-appropriate path.
2127
+ * @public
2128
+ */
2129
+ declare function loadConfig(configDir?: string): Promise<VaultConfig>;
2130
+
2131
+ export { AccessorConsumedError, AuthorizationDeniedError, type BackendCapabilities, type BackendConfig, type BackendFactory, BackendLockedError, BackendRegistry, type BackendSetupFactory, BackendUnavailableError, CapabilityToken, ConfigParseError, ConfigValidationError, DecryptionError, DeviceNotPresentError, ExecError, type ExecRequest, type ExecResult, ExecutableTrustRequiredError, type ExecutableTrustStatus, FetchError, type FetchRequest, FilesystemError, IdentityMismatchError, InvalidAlgorithmError, InvalidKeyMaterialError, InvalidTokenError, KeyRevokedError, KeyRotatedError, type KeyStatus, type ListableBackend, NotCapableError, type Platform, PluginNotFoundError, type PreflightCheck, type PreflightCheckError, type PreflightCheckErrorKind, type PreflightCheckStatus, type PreflightResult, type PresenceCapableBackend, PresenceDeclinedError, type PresenceOperation, type PresenceRequirementOptions, PresenceTimeoutError, REDACTED, RotationInProgressError, type RunDoctorOptions, type ScopedPreflightCheck, type SecretAccessor, type SecretBackend, SecretNotFoundError, type SecretTokenMap, type SetupChoice, SetupError, type SetupOptions, type SetupOptionsBase, type SetupQuestion, type SetupResult, type SignRequest, type SignResult, type SigningAlgorithm, type SigningBackend, SigningKeyAlreadyExistsError, SigningKeyNotFoundError, SigningNotSupportedError, type SigningPublicKey, TokenExpiredError, TokenRevokedError, type TrustTier, UnknownBackendTypeError, UsageLimitExceededError, type VaultConfig, VaultError, VaultKeeper, type VaultKeeperOptions, type VaultResponse, type VerifyRequest, defaultBackendType, getBackendCapabilities, getDefaultConfigDir, getPlatformDefaultConfigDir, isListableBackend, isPresenceCapableBackend, isSigningBackend, loadConfig, platformNativeBackendType, redactSecrets, runDoctor };