vaultkeeper 0.5.3 → 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.ts 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,80 @@ declare class IdentityMismatchError extends VaultError {
135
201
  constructor(message: string, previousHash: string, currentHash: string);
136
202
  }
137
203
  /**
138
- * Thrown when a caller requests a signing/verification algorithm that is not
139
- * in the allowed set (e.g. `'md5'`).
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.
241
+ *
242
+ * @public
243
+ */
244
+ declare class ExecError extends VaultError {
245
+ /**
246
+ * The command that failed to execute.
247
+ */
248
+ readonly command: string;
249
+ constructor(message: string, command: string);
250
+ }
251
+ /**
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.
256
+ *
257
+ * @public
258
+ */
259
+ declare class InvalidTokenError extends VaultError {
260
+ constructor(message: string);
261
+ }
262
+ /**
263
+ * Thrown when `SecretAccessor.read()` is called after the accessor has
264
+ * already been consumed.
265
+ *
266
+ * @public
267
+ */
268
+ declare class AccessorConsumedError extends VaultError {
269
+ constructor(message: string);
270
+ }
271
+ /**
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.
276
+ *
277
+ * @public
140
278
  */
141
279
  declare class InvalidAlgorithmError extends VaultError {
142
280
  /**
@@ -149,6 +287,183 @@ declare class InvalidAlgorithmError extends VaultError {
149
287
  readonly allowed: string[];
150
288
  constructor(message: string, algorithm: string, allowed: string[]);
151
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
+ }
152
467
  /**
153
468
  * Thrown during initialization when a required system dependency (e.g. OpenSSL
154
469
  * or a native credential helper) is missing or incompatible.
@@ -161,20 +476,49 @@ declare class SetupError extends VaultError {
161
476
  constructor(message: string, dependency: string);
162
477
  }
163
478
  /**
164
- * Thrown when a filesystem operation fails due to a permission or access
165
- * 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.
166
485
  */
167
486
  declare class FilesystemError extends VaultError {
168
487
  /**
169
- * 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`.
170
492
  */
171
493
  readonly path: string;
172
494
  /**
173
- * The permission level that was required but not available
174
- * (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.
175
501
  */
176
502
  readonly permission: string;
177
- 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);
178
522
  }
179
523
  /**
180
524
  * Thrown when a key rotation is requested while a previous rotation is still
@@ -187,12 +531,91 @@ declare class RotationInProgressError extends VaultError {
187
531
  /**
188
532
  * Shared types and interfaces for vaultkeeper.
189
533
  */
534
+
190
535
  /** Trust tier for executable identity verification. */
191
536
  type TrustTier = 1 | 2 | 3;
192
537
  /** Key status in the rotation lifecycle. */
193
538
  type KeyStatus = 'current' | 'previous' | 'deprecated';
194
- /** Status of a preflight check. */
195
- 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
+ }
196
619
  /** Result of a preflight check for a single dependency. */
197
620
  interface PreflightCheck {
198
621
  /** Human-readable name of the dependency being checked. */
@@ -203,11 +626,30 @@ interface PreflightCheck {
203
626
  version?: string | undefined;
204
627
  /** Human-readable explanation of why the status is not `'ok'`. */
205
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;
206
648
  }
207
649
  /** Aggregated result from all preflight checks. */
208
650
  interface PreflightResult {
209
651
  /** Individual check results, one per dependency inspected. */
210
- checks: PreflightCheck[];
652
+ checks: ScopedPreflightCheck[];
211
653
  /** `true` if all required checks passed and the system is ready. */
212
654
  ready: boolean;
213
655
  /** Non-fatal advisory messages about optional missing dependencies. */
@@ -226,51 +668,69 @@ interface VaultResponse {
226
668
  * Request for delegated HTTP fetch.
227
669
  *
228
670
  * String values in `url`, `headers`, and `body` may include the placeholder
229
- * `{{secret}}`, which is replaced with the actual secret value immediately
230
- * 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.
231
674
  */
232
675
  interface FetchRequest {
233
676
  /**
234
- * The target URL. May contain `{{secret}}` which is replaced with the secret
235
- * 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.
236
679
  */
237
680
  url: string;
238
681
  /** HTTP method (defaults to `'GET'` when omitted). */
239
682
  method?: string | undefined;
240
683
  /**
241
- * Request headers. Any header value may contain `{{secret}}`, which is
242
- * 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.
243
687
  */
244
688
  headers?: Record<string, string> | undefined;
245
689
  /**
246
- * Request body. May contain `{{secret}}`, which is replaced with the secret
247
- * 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.
248
692
  */
249
693
  body?: string | undefined;
250
694
  }
251
695
  /**
252
696
  * Request for delegated command execution.
253
697
  *
254
- * String values in `args` and `env` may include the placeholder `{{secret}}`,
255
- * which is replaced with the actual secret value immediately before the command
256
- * 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.
257
703
  */
258
704
  interface ExecRequest {
259
705
  /** The command (binary) to execute. */
260
706
  command: string;
261
707
  /**
262
- * Command-line arguments. Any argument may contain `{{secret}}`, which is
263
- * 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.
264
713
  */
265
714
  args?: string[] | undefined;
266
715
  /**
267
716
  * Additional environment variables to merge into the child process
268
- * environment. Any value may contain `{{secret}}`, which is replaced with
269
- * 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.
270
719
  */
271
720
  env?: Record<string, string> | undefined;
272
721
  /** Working directory for the spawned process. */
273
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;
274
734
  }
275
735
  /** Result from delegated command execution. */
276
736
  interface ExecResult {
@@ -284,10 +744,11 @@ interface ExecResult {
284
744
  /**
285
745
  * Callback-based secret accessor with auto-zeroing.
286
746
  *
287
- * The accessor is backed by a revocable Proxy. Calling `read()` passes a
288
- * `Buffer` containing the secret to the callback, then zeroes the buffer after
289
- * the callback returns. The accessor can only be read once; a second call
290
- * 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.
291
752
  */
292
753
  interface SecretAccessor {
293
754
  /**
@@ -297,51 +758,81 @@ interface SecretAccessor {
297
758
  * as UTF-8. The buffer is zeroed immediately after the callback returns, so
298
759
  * callers must not store a reference to it beyond the callback scope.
299
760
  *
300
- * @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.
301
770
  * @throws {Error} If the accessor has already been consumed.
302
771
  */
303
- read(callback: (buf: Buffer) => void): void;
772
+ read<T>(callback: (buf: Buffer) => T): T;
304
773
  }
305
774
  /**
306
- * Request for delegated signing.
775
+ * A signing algorithm identifier from the strict JOSE registry (RFC 7518).
307
776
  *
308
- * The `data` field is the payload to sign. Strings are UTF-8-encoded
309
- * 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.
310
780
  */
311
- interface SignRequest {
312
- /** The data to sign. Strings are treated as UTF-8. */
313
- 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;
314
793
  /**
315
- * Override the hash algorithm (`'sha256'`, `'sha384'`, or `'sha512'`).
316
- * Ignored for Ed25519/Ed448 keys where the algorithm is implicit.
317
- * Non-Edwards keys (RSA, EC) default to `'sha256'` when omitted.
318
- * 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.
319
796
  */
320
- algorithm?: string | undefined;
797
+ kid: string;
321
798
  }
322
- /** Result from a delegated signing operation. */
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;
809
+ }
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
+ */
323
818
  interface SignResult {
324
- /** Base64-encoded signature. */
325
- signature: string;
326
- /**
327
- * Algorithm label describing how the signature was produced.
328
- * For Edwards keys this is the key type (e.g. `'ed25519'`).
329
- * For other keys this matches the `algorithm` field from the request
330
- * (or the default `'sha256'`).
331
- */
332
- algorithm: string;
819
+ /** The detached-payload compact JWS (`<protected>..<signature>`). */
820
+ jws: string;
333
821
  }
334
822
  /**
335
- * Request for signature verification.
823
+ * Request for detached-signature verification.
336
824
  *
337
- * This is a static operation that only requires public key material —
338
- * 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.
339
827
  */
340
828
  interface VerifyRequest {
341
- /** The original data that was signed. Strings are treated as UTF-8. */
342
- data: string | Buffer;
343
- /** Base64-encoded signature to verify. */
344
- 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;
345
836
  /**
346
837
  * PEM-encoded public key (SPKI format) as a string.
347
838
  *
@@ -349,11 +840,6 @@ interface VerifyRequest {
349
840
  * accepted by this interface.
350
841
  */
351
842
  publicKey: string;
352
- /**
353
- * Override the hash algorithm. Ignored for Ed25519/Ed448 keys.
354
- * Non-Edwards keys default to `'sha256'` when omitted.
355
- */
356
- algorithm?: string | undefined;
357
843
  }
358
844
  /** Vaultkeeper configuration file structure. */
359
845
  interface VaultConfig {
@@ -396,20 +882,19 @@ interface BackendConfig {
396
882
  options?: Record<string, string> | undefined;
397
883
  }
398
884
 
399
- /**
400
- * Backend abstraction layer types for vaultkeeper.
401
- */
402
885
  /**
403
886
  * Factory function for creating a SecretBackend instance.
404
887
  *
405
888
  * @remarks
406
889
  * Factories may optionally accept a {@link BackendConfig} to configure the
407
- * backend from the user's vaultkeeper config file. Factories that do not need
408
- * 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.
409
894
  *
410
895
  * @public
411
896
  */
412
- type BackendFactory = (config?: BackendConfig) => SecretBackend;
897
+ type BackendFactory = (config?: BackendConfig, configDir?: string) => SecretBackend;
413
898
  /**
414
899
  * Abstraction interface for all secret storage backends.
415
900
  *
@@ -471,6 +956,170 @@ interface ListableBackend extends SecretBackend {
471
956
  * @public
472
957
  */
473
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;
474
1123
 
475
1124
  /**
476
1125
  * Types for the backend setup protocol.
@@ -536,10 +1185,12 @@ declare class BackendRegistry {
536
1185
  * Create a backend instance by type.
537
1186
  * @param type - Backend type identifier
538
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
539
1190
  * @returns A SecretBackend instance
540
1191
  * @throws {@link BackendUnavailableError} if the backend type is not registered
541
1192
  */
542
- static create(type: string, config?: BackendConfig): SecretBackend;
1193
+ static create(type: string, config?: BackendConfig, configDir?: string): SecretBackend;
543
1194
  /**
544
1195
  * Get all registered backend type identifiers.
545
1196
  * @returns Array of backend type identifiers
@@ -598,7 +1249,23 @@ declare class BackendRegistry {
598
1249
  * class fields enforce that no property on the token object leaks data.
599
1250
  */
600
1251
 
601
- /** 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
+ */
602
1269
  declare class CapabilityToken {
603
1270
  #private;
604
1271
  constructor();
@@ -609,32 +1276,256 @@ declare class CapabilityToken {
609
1276
  toString(): string;
610
1277
  }
611
1278
 
1279
+ /**
1280
+ * Platform detection utilities.
1281
+ */
1282
+ /**
1283
+ * The OS platform identifier used for platform-specific behavior.
1284
+ * @public
1285
+ */
1286
+ type Platform = 'darwin' | 'win32' | 'linux';
1287
+
1288
+ /**
1289
+ * Doctor runner: orchestrates platform-appropriate checks and aggregates results.
1290
+ */
1291
+
1292
+ /**
1293
+ * Options for running the doctor.
1294
+ * @public
1295
+ */
1296
+ interface RunDoctorOptions {
1297
+ /** Override the platform detection (useful for testing). */
1298
+ platform?: Platform;
1299
+ /**
1300
+ * When provided, doctor checks are scoped to the given backends.
1301
+ * Platform-native dependency checks (e.g. `secret-tool`, `security`,
1302
+ * `powershell`) are demoted from required to optional when the
1303
+ * corresponding backend is not enabled. Plugin tool checks (`op`,
1304
+ * `ykman`) are promoted from optional to required when their backend
1305
+ * (`1password`, `yubikey`) is explicitly enabled.
1306
+ *
1307
+ * When omitted, all platform-default checks are treated as required
1308
+ * (backward-compatible behavior).
1309
+ */
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;
1323
+ }
1324
+ /**
1325
+ * Run all platform-appropriate preflight checks and aggregate the results.
1326
+ * @public
1327
+ */
1328
+ declare function runDoctor(options?: RunDoctorOptions): Promise<PreflightResult>;
1329
+
612
1330
  /**
613
1331
  * VaultKeeper main class — wires together all vaultkeeper subsystems.
614
1332
  */
615
1333
 
616
- /** 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
+ */
617
1371
  interface VaultKeeperOptions {
618
1372
  /** Override the config directory. */
619
1373
  configDir?: string | undefined;
620
1374
  /** Supply config directly, skipping file load. */
621
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;
622
1398
  /** Skip the doctor preflight check. */
623
1399
  skipDoctor?: boolean | undefined;
624
1400
  }
625
- /** Options for the setup operation. */
626
- 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 {
627
1432
  /** TTL in minutes for the JWE. */
628
1433
  ttlMinutes?: number | undefined;
629
1434
  /** Usage limit (null for unlimited). */
630
1435
  useLimit?: number | null | undefined;
631
- /** Executable path for identity binding. Use "dev" for dev mode. */
632
- executablePath?: string | undefined;
633
1436
  /** Trust tier override. */
634
1437
  trustTier?: TrustTier | undefined;
635
1438
  /** Backend type to use. */
636
1439
  backendType?: string | undefined;
637
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
+ }
638
1529
  /**
639
1530
  * Main entry point for vaultkeeper. Orchestrates backends, keys, JWE tokens,
640
1531
  * identity verification, and access patterns.
@@ -645,70 +1536,253 @@ declare class VaultKeeper {
645
1536
  /**
646
1537
  * Initialize a new VaultKeeper instance.
647
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.
648
1545
  */
649
1546
  static init(options?: VaultKeeperOptions): Promise<VaultKeeper>;
650
1547
  /**
651
1548
  * Run doctor checks without full initialization.
652
1549
  *
653
- * Uses conservative platform defaults — all platform-native dependency
654
- * checks are treated as required regardless of any backend configuration.
655
- * For config-aware scoping, call `runDoctor({ backends })` directly.
1550
+ * When called without arguments, uses conservative platform defaults —
1551
+ * all platform-native dependency checks are treated as required. Pass
1552
+ * `{ backends }` to scope checks to only the backends you plan to use.
1553
+ *
1554
+ * @param options - Optional doctor options (e.g. `{ backends }` to scope checks).
656
1555
  */
657
- static doctor(): Promise<PreflightResult>;
1556
+ static doctor(options?: RunDoctorOptions): Promise<PreflightResult>;
658
1557
  /**
659
- * Retrieve a secret from the backend and return a JWE token that encapsulates it.
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.
660
1574
  *
661
- * @param secretName - Identifier for the secret
662
- * @param options - Setup options
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>;
1610
+ /**
1611
+ * Store a secret in the configured backend.
1612
+ *
1613
+ * This is a convenience method that delegates to the active backend's
1614
+ * `store()` method. If a secret with the same name already exists, it is
1615
+ * overwritten.
1616
+ *
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.
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.
1628
+ * @public
1629
+ */
1630
+ store(name: string, value: string, options?: PresenceRequirementOptions): Promise<void>;
1631
+ /**
1632
+ * Delete a secret from the configured backend.
1633
+ *
1634
+ * This is a convenience method that delegates to the active backend's
1635
+ * `delete()` method.
1636
+ *
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.
1659
+ * @public
1660
+ */
1661
+ secretExists(name: string): Promise<boolean>;
1662
+ /**
1663
+ * Read a stored secret from the backend and mint a JWE token that encapsulates it.
1664
+ *
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`
663
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.
664
1710
  */
665
- setup(secretName: string, options?: SetupOptions): Promise<string>;
1711
+ setup(secretName: string, options: SetupOptions): Promise<string>;
666
1712
  /**
667
1713
  * Decrypt a JWE, validate claims, verify executable identity, and return
668
1714
  * an opaque CapabilityToken.
669
1715
  *
670
1716
  * @param jwe - Compact JWE string from setup()
671
- * @returns Opaque capability token for use with fetch/exec/getSecret
1717
+ * @returns Object containing an opaque {@link CapabilityToken} for use with
1718
+ * fetch/exec/getSecret, and a {@link VaultResponse} describing key status.
1719
+ * When the JWE was decrypted with a non-current key,
1720
+ * `vaultResponse.rotatedJwt` contains a re-encrypted JWE for the current key.
672
1721
  */
673
1722
  authorize(jwe: string): Promise<{
674
1723
  token: CapabilityToken;
675
- response: VaultResponse;
1724
+ vaultResponse: VaultResponse;
676
1725
  }>;
677
1726
  /**
678
- * Execute a delegated HTTP fetch, injecting the secret from the token.
1727
+ * Execute a delegated HTTP fetch, injecting secrets from the token(s).
679
1728
  *
680
- * The secret value is substituted for every `{{secret}}` placeholder found
681
- * in `request.url`, `request.headers`, and `request.body` before the fetch
682
- * 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.
683
1731
  *
684
- * @param token - A `CapabilityToken` obtained from `authorize()`.
685
- * @param request - The fetch request template. Use `{{secret}}` as a
686
- * 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.
687
1740
  * @returns The `Response` from the underlying `fetch()` call, together with
688
1741
  * the vault metadata (`vaultResponse`).
689
- * @throws {Error} If `token` is invalid or was not created by this vault
690
- * 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.
691
1748
  */
692
- fetch(token: CapabilityToken, request: FetchRequest): Promise<{
1749
+ fetch(token: CapabilityToken | SecretTokenMap, request: FetchRequest): Promise<{
693
1750
  response: Response;
694
1751
  vaultResponse: VaultResponse;
695
1752
  }>;
696
1753
  /**
697
- * Execute a delegated command, injecting the secret from the token.
1754
+ * Execute a delegated command, injecting secrets from the token(s).
698
1755
  *
699
- * The secret value is substituted for every `{{secret}}` placeholder found
700
- * in `request.args` and `request.env` values before the process is spawned.
701
- * 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.
702
1758
  *
703
- * @param token - A `CapabilityToken` obtained from `authorize()`.
704
- * @param request - The exec request template. Use `{{secret}}` as a
705
- * 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.
706
1777
  * @returns The command result (`stdout`, `stderr`, `exitCode`) together with
707
1778
  * the vault metadata (`vaultResponse`).
708
- * @throws {Error} If `token` is invalid or was not created by this vault
709
- * 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.
710
1784
  */
711
- exec(token: CapabilityToken, request: ExecRequest): Promise<{
1785
+ exec(token: CapabilityToken | SecretTokenMap, request: ExecRequest): Promise<{
712
1786
  result: ExecResult;
713
1787
  vaultResponse: VaultResponse;
714
1788
  }>;
@@ -721,49 +1795,108 @@ declare class VaultKeeper {
721
1795
  *
722
1796
  * @param token - A `CapabilityToken` obtained from `authorize()`.
723
1797
  * @returns A `SecretAccessor` that can be read exactly once.
724
- * @throws {Error} If `token` is invalid or was not created by this vault
725
- * 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()`).
726
1801
  */
727
1802
  getSecret(token: CapabilityToken): SecretAccessor;
728
1803
  /**
729
- * Sign data using the private key embedded in a capability token.
1804
+ * Enroll a new signing keypair under `name` in the active backend.
730
1805
  *
731
- * The signing key is extracted from the token's encrypted claims, used
732
- * for a single `crypto.sign()` call, and never exposed to the caller.
733
- * The algorithm is auto-detected from the key type unless overridden
734
- * 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.
735
1811
  *
736
- * @param token - A `CapabilityToken` obtained from `authorize()`.
737
- * @param request - The data to sign and optional algorithm override.
738
- * @returns The base64-encoded signature and algorithm label, together
739
- * with the vault metadata (`vaultResponse`).
740
- * @throws {VaultError} If `token` is invalid or was not created by this
741
- * vault instance.
742
- * @throws {InvalidAlgorithmError} If `request.algorithm` is not in the
743
- * allowed set (e.g. `'md5'`).
744
- */
745
- 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<{
746
1877
  result: SignResult;
747
1878
  vaultResponse: VaultResponse;
748
1879
  }>;
749
1880
  /**
750
- * Verify a signature using a public key.
751
- *
752
- * This is a static method — no VaultKeeper instance, secrets, or
753
- * capability tokens are required. It is safe to call from CI or any
754
- * context that has access to public key material.
1881
+ * Verify a detached-payload Compact JWS against a public key — fully offline.
755
1882
  *
756
- * Returns `false` for invalid key material, malformed signatures, or
757
- * 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.
758
1886
  *
759
- * @throws {InvalidAlgorithmError} If `request.algorithm` is not in the
760
- * 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.
761
1892
  *
762
- * @param request - The data, signature, public key, and optional
763
- * algorithm override.
1893
+ * @param request - The detached payload, the JWS, and the SPKI PEM public key.
764
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
765
1898
  */
766
- static verify(request: VerifyRequest): boolean;
1899
+ static verify(request: VerifyRequest): Promise<boolean>;
767
1900
  /**
768
1901
  * Rotate the current encryption key.
769
1902
  *
@@ -797,45 +1930,202 @@ declare class VaultKeeper {
797
1930
  * to remove it.
798
1931
  */
799
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>;
800
1968
  }
801
1969
 
802
1970
  /**
803
- * Platform detection utilities.
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.
804
1978
  */
805
1979
  /**
806
- * The OS platform identifier used for platform-specific behavior.
1980
+ * The token substituted for a redacted secret value.
1981
+ *
807
1982
  * @public
808
1983
  */
809
- type Platform = 'darwin' | 'win32' | 'linux';
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;
810
2021
 
811
2022
  /**
812
- * Doctor runner: orchestrates platform-appropriate checks and aggregates results.
2023
+ * Configuration loading, validation, and defaults for vaultkeeper.
813
2024
  */
814
2025
 
815
2026
  /**
816
- * Options for running the doctor.
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
+ *
817
2043
  * @public
818
2044
  */
819
- interface RunDoctorOptions {
820
- /** Override the platform detection (useful for testing). */
821
- platform?: Platform;
822
- /**
823
- * When provided, doctor checks are scoped to the given backends.
824
- * Platform-native dependency checks (e.g. `secret-tool`, `security`,
825
- * `powershell`) are demoted from required to optional when the
826
- * corresponding backend is not enabled. Plugin tool checks (`op`,
827
- * `ykman`) are promoted from optional to required when their backend
828
- * (`1password`, `yubikey`) is explicitly enabled.
829
- *
830
- * When omitted, all platform-default checks are treated as required
831
- * (backward-compatible behavior).
832
- */
833
- backends?: BackendConfig[];
834
- }
2045
+ declare function getPlatformDefaultConfigDir(): string;
835
2046
  /**
836
- * Run all platform-appropriate preflight checks and aggregate the results.
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
+ *
837
2055
  * @public
838
2056
  */
839
- declare function runDoctor(options?: RunDoctorOptions): Promise<PreflightResult>;
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>;
840
2130
 
841
- export { AuthorizationDeniedError, type BackendConfig, type BackendFactory, BackendLockedError, BackendRegistry, type BackendSetupFactory, BackendUnavailableError, CapabilityToken, DeviceNotPresentError, type ExecRequest, type ExecResult, type FetchRequest, FilesystemError, IdentityMismatchError, InvalidAlgorithmError, 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 };
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 };