vaultkeeper 0.0.0 → 0.3.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.js ADDED
@@ -0,0 +1,1468 @@
1
+ import { spawn } from 'child_process';
2
+ import * as fs5 from 'fs/promises';
3
+ import * as path5 from 'path';
4
+ import * as os4 from 'os';
5
+ import * as crypto4 from 'crypto';
6
+ import * as fs4 from 'fs';
7
+ import { CompactEncrypt, compactDecrypt } from 'jose';
8
+
9
+ // src/errors.ts
10
+ var VaultError = class extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "VaultError";
14
+ }
15
+ };
16
+ var BackendLockedError = class extends VaultError {
17
+ /**
18
+ * Whether the lock can be resolved through an interactive user prompt.
19
+ * When `true`, callers may retry after prompting the user.
20
+ */
21
+ interactive;
22
+ constructor(message, interactive) {
23
+ super(message);
24
+ this.name = "BackendLockedError";
25
+ this.interactive = interactive;
26
+ }
27
+ };
28
+ var DeviceNotPresentError = class extends VaultError {
29
+ /**
30
+ * How long (in milliseconds) the operation waited for the device before
31
+ * giving up.
32
+ */
33
+ timeoutMs;
34
+ constructor(message, timeoutMs) {
35
+ super(message);
36
+ this.name = "DeviceNotPresentError";
37
+ this.timeoutMs = timeoutMs;
38
+ }
39
+ };
40
+ var AuthorizationDeniedError = class extends VaultError {
41
+ constructor(message) {
42
+ super(message);
43
+ this.name = "AuthorizationDeniedError";
44
+ }
45
+ };
46
+ var BackendUnavailableError = class extends VaultError {
47
+ /**
48
+ * Machine-readable reason code describing why the backend is unavailable
49
+ * (e.g. `'none-enabled'`, `'all-failed'`).
50
+ */
51
+ reason;
52
+ /**
53
+ * The backend type identifiers that were attempted before this error was
54
+ * thrown.
55
+ */
56
+ attempted;
57
+ constructor(message, reason, attempted) {
58
+ super(message);
59
+ this.name = "BackendUnavailableError";
60
+ this.reason = reason;
61
+ this.attempted = attempted;
62
+ }
63
+ };
64
+ var PluginNotFoundError = class extends VaultError {
65
+ /**
66
+ * The plugin package or binary name that was not found.
67
+ */
68
+ plugin;
69
+ /**
70
+ * A URL pointing to installation instructions for the missing plugin.
71
+ */
72
+ installUrl;
73
+ constructor(message, plugin, installUrl) {
74
+ super(message);
75
+ this.name = "PluginNotFoundError";
76
+ this.plugin = plugin;
77
+ this.installUrl = installUrl;
78
+ }
79
+ };
80
+ var SecretNotFoundError = class extends VaultError {
81
+ constructor(message) {
82
+ super(message);
83
+ this.name = "SecretNotFoundError";
84
+ }
85
+ };
86
+ var TokenExpiredError = class extends VaultError {
87
+ /**
88
+ * Whether the token can be refreshed by calling `setup()` again.
89
+ * When `true`, the secret still exists in the backend and a new token can be
90
+ * issued.
91
+ */
92
+ canRefresh;
93
+ constructor(message, canRefresh) {
94
+ super(message);
95
+ this.name = "TokenExpiredError";
96
+ this.canRefresh = canRefresh;
97
+ }
98
+ };
99
+ var KeyRotatedError = class extends VaultError {
100
+ constructor(message) {
101
+ super(message);
102
+ this.name = "KeyRotatedError";
103
+ }
104
+ };
105
+ var KeyRevokedError = class extends VaultError {
106
+ constructor(message) {
107
+ super(message);
108
+ this.name = "KeyRevokedError";
109
+ }
110
+ };
111
+ var TokenRevokedError = class extends VaultError {
112
+ constructor(message) {
113
+ super(message);
114
+ this.name = "TokenRevokedError";
115
+ }
116
+ };
117
+ var UsageLimitExceededError = class extends VaultError {
118
+ constructor(message) {
119
+ super(message);
120
+ this.name = "UsageLimitExceededError";
121
+ }
122
+ };
123
+ var IdentityMismatchError = class extends VaultError {
124
+ /**
125
+ * The hash that was recorded in the trust manifest at approval time.
126
+ */
127
+ previousHash;
128
+ /**
129
+ * The hash computed from the executable at the current moment.
130
+ */
131
+ currentHash;
132
+ constructor(message, previousHash, currentHash) {
133
+ super(message);
134
+ this.name = "IdentityMismatchError";
135
+ this.previousHash = previousHash;
136
+ this.currentHash = currentHash;
137
+ }
138
+ };
139
+ var SetupError = class extends VaultError {
140
+ /**
141
+ * The name of the dependency that caused the setup failure.
142
+ */
143
+ dependency;
144
+ constructor(message, dependency) {
145
+ super(message);
146
+ this.name = "SetupError";
147
+ this.dependency = dependency;
148
+ }
149
+ };
150
+ var FilesystemError = class extends VaultError {
151
+ /**
152
+ * The absolute path of the file or directory that caused the error.
153
+ */
154
+ path;
155
+ /**
156
+ * The permission level that was required but not available
157
+ * (e.g. `'read'`, `'write'`, `'execute'`).
158
+ */
159
+ permission;
160
+ constructor(message, filePath, permission) {
161
+ super(message);
162
+ this.name = "FilesystemError";
163
+ this.path = filePath;
164
+ this.permission = permission;
165
+ }
166
+ };
167
+ var RotationInProgressError = class extends VaultError {
168
+ constructor(message) {
169
+ super(message);
170
+ this.name = "RotationInProgressError";
171
+ }
172
+ };
173
+
174
+ // src/backend/types.ts
175
+ function isListableBackend(backend) {
176
+ return "list" in backend && typeof backend.list === "function";
177
+ }
178
+
179
+ // src/backend/registry.ts
180
+ var BackendRegistry = class {
181
+ static backends = /* @__PURE__ */ new Map();
182
+ /**
183
+ * Register a backend factory.
184
+ * @param type - Backend type identifier
185
+ * @param factory - Factory function to create backend instances
186
+ */
187
+ static register(type, factory) {
188
+ this.backends.set(type, factory);
189
+ }
190
+ /**
191
+ * Create a backend instance by type.
192
+ * @param type - Backend type identifier
193
+ * @returns A SecretBackend instance
194
+ * @throws Error if the backend type is not registered
195
+ */
196
+ static create(type) {
197
+ const factory = this.backends.get(type);
198
+ if (factory === void 0) {
199
+ throw new BackendUnavailableError(
200
+ `Unknown backend type: ${type}. Available types: ${Array.from(this.backends.keys()).join(", ")}`,
201
+ "unknown-type",
202
+ Array.from(this.backends.keys())
203
+ );
204
+ }
205
+ return factory();
206
+ }
207
+ /**
208
+ * Get all registered backend type identifiers.
209
+ * @returns Array of backend type identifiers
210
+ */
211
+ static getTypes() {
212
+ return Array.from(this.backends.keys());
213
+ }
214
+ /**
215
+ * Returns backend types that are available on the current system.
216
+ *
217
+ * @remarks
218
+ * Creates each registered backend via its factory, calls `isAvailable()`,
219
+ * and returns only the type identifiers whose backend reports availability.
220
+ * If a backend's `isAvailable()` call throws, that backend is excluded from
221
+ * the result rather than propagating the error.
222
+ *
223
+ * @returns Promise resolving to an array of available backend type identifiers
224
+ * @public
225
+ */
226
+ static async getAvailableTypes() {
227
+ const entries = Array.from(this.backends.entries());
228
+ const results = await Promise.all(
229
+ entries.map(async ([type, factory]) => {
230
+ try {
231
+ const backend = factory();
232
+ const available = await backend.isAvailable();
233
+ return available ? type : null;
234
+ } catch {
235
+ return null;
236
+ }
237
+ })
238
+ );
239
+ return results.filter((type) => type !== null);
240
+ }
241
+ };
242
+ async function execCommand(command, args, options) {
243
+ const result = await execCommandFull(command, args);
244
+ if (result.exitCode !== 0) {
245
+ throw new Error(`Command failed with exit code ${String(result.exitCode)}: ${result.stderr}`);
246
+ }
247
+ return result.stdout.trim();
248
+ }
249
+ function execCommandFull(command, args, options) {
250
+ return new Promise((resolve, reject) => {
251
+ const proc = spawn(command, args, {
252
+ stdio: ["ignore", "pipe", "pipe"]
253
+ });
254
+ let stdout = "";
255
+ let stderr = "";
256
+ proc.stdout?.on("data", (data) => {
257
+ stdout += data.toString();
258
+ });
259
+ proc.stderr?.on("data", (data) => {
260
+ stderr += data.toString();
261
+ });
262
+ proc.on("close", (code) => {
263
+ resolve({ stdout, stderr, exitCode: code ?? 1 });
264
+ });
265
+ proc.on("error", (error) => {
266
+ reject(error);
267
+ });
268
+ });
269
+ }
270
+ path5.join(".vaultkeeper", "file");
271
+ path5.join(".vaultkeeper", "yubikey");
272
+ function hashExecutable(filePath) {
273
+ return new Promise((resolve, reject) => {
274
+ const hash = crypto4.createHash("sha256");
275
+ const stream = fs4.createReadStream(filePath);
276
+ stream.on("data", (chunk) => {
277
+ hash.update(chunk);
278
+ });
279
+ stream.on("end", () => {
280
+ resolve(hash.digest("hex"));
281
+ });
282
+ stream.on("error", (err) => {
283
+ reject(err);
284
+ });
285
+ });
286
+ }
287
+ var MANIFEST_FILENAME = "trust-manifest.json";
288
+ function isRawManifest(value) {
289
+ if (typeof value !== "object" || value === null) return false;
290
+ if (!("version" in value) || typeof value.version !== "number") return false;
291
+ if (!("entries" in value) || typeof value.entries !== "object" || value.entries === null) return false;
292
+ return true;
293
+ }
294
+ function isTrustManifestEntry(value) {
295
+ if (typeof value !== "object" || value === null) return false;
296
+ if (!("hashes" in value) || !Array.isArray(value.hashes)) return false;
297
+ if (!("trustTier" in value)) return false;
298
+ const { trustTier } = value;
299
+ if (trustTier !== 1 && trustTier !== 2 && trustTier !== 3) return false;
300
+ return true;
301
+ }
302
+ async function loadManifest(configDir) {
303
+ const manifestPath = path5.join(configDir, MANIFEST_FILENAME);
304
+ let rawText;
305
+ try {
306
+ rawText = await fs5.readFile(manifestPath, "utf8");
307
+ } catch (err) {
308
+ if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
309
+ return /* @__PURE__ */ new Map();
310
+ }
311
+ throw err;
312
+ }
313
+ const parsed = JSON.parse(rawText);
314
+ if (!isRawManifest(parsed)) {
315
+ return /* @__PURE__ */ new Map();
316
+ }
317
+ const manifest = /* @__PURE__ */ new Map();
318
+ for (const [namespace, entry] of Object.entries(parsed.entries)) {
319
+ if (isTrustManifestEntry(entry)) {
320
+ manifest.set(namespace, { hashes: [...entry.hashes], trustTier: entry.trustTier });
321
+ }
322
+ }
323
+ return manifest;
324
+ }
325
+ async function saveManifest(configDir, manifest) {
326
+ await fs5.mkdir(configDir, { recursive: true });
327
+ const entries = {};
328
+ for (const [namespace, entry] of manifest) {
329
+ entries[namespace] = entry;
330
+ }
331
+ const raw = { version: 1, entries };
332
+ const manifestPath = path5.join(configDir, MANIFEST_FILENAME);
333
+ await fs5.writeFile(manifestPath, JSON.stringify(raw, null, 2), "utf8");
334
+ }
335
+ function addTrustedHash(manifest, namespace, hash) {
336
+ const next = new Map(manifest);
337
+ const existing = next.get(namespace);
338
+ if (existing === void 0) {
339
+ next.set(namespace, { hashes: [hash], trustTier: 3 });
340
+ } else if (!existing.hashes.includes(hash)) {
341
+ next.set(namespace, { hashes: [...existing.hashes, hash], trustTier: existing.trustTier });
342
+ }
343
+ return next;
344
+ }
345
+ function isTrusted(manifest, namespace, hash) {
346
+ const entry = manifest.get(namespace);
347
+ if (entry === void 0) return false;
348
+ return entry.hashes.includes(hash);
349
+ }
350
+
351
+ // src/identity/trust.ts
352
+ async function trySigstore(execPath) {
353
+ try {
354
+ const sigstore = await import('sigstore');
355
+ if (typeof sigstore !== "object" || sigstore === null) {
356
+ return false;
357
+ }
358
+ if (!("verify" in sigstore) || typeof sigstore.verify !== "function") {
359
+ return false;
360
+ }
361
+ void execPath;
362
+ return false;
363
+ } catch {
364
+ return false;
365
+ }
366
+ }
367
+ async function verifyTrust(execPath, options) {
368
+ if (execPath === "dev") {
369
+ return {
370
+ identity: { hash: "dev", trustTier: 3, verified: false },
371
+ tofuConflict: false,
372
+ reason: "Dev mode \u2014 hash verification skipped"
373
+ };
374
+ }
375
+ const configDir = options?.configDir ?? ".vaultkeeper";
376
+ const namespace = options?.namespace ?? execPath;
377
+ const currentHash = await hashExecutable(execPath);
378
+ const manifest = await loadManifest(configDir);
379
+ if (options?.skipSigstore !== true) {
380
+ const sigstoreVerified = await trySigstore(execPath);
381
+ if (sigstoreVerified) {
382
+ const updated2 = addTrustedHash(manifest, namespace, currentHash);
383
+ await saveManifest(configDir, updated2);
384
+ return {
385
+ identity: { hash: currentHash, trustTier: 1, verified: true },
386
+ tofuConflict: false,
387
+ reason: "Sigstore bundle verified"
388
+ };
389
+ }
390
+ }
391
+ if (isTrusted(manifest, namespace, currentHash)) {
392
+ return {
393
+ identity: { hash: currentHash, trustTier: 2, verified: true },
394
+ tofuConflict: false,
395
+ reason: "Hash found in trust manifest"
396
+ };
397
+ }
398
+ const existing = manifest.get(namespace);
399
+ if (existing !== void 0 && existing.hashes.length > 0) {
400
+ return {
401
+ identity: { hash: currentHash, trustTier: 3, verified: false },
402
+ tofuConflict: true,
403
+ reason: `Hash changed from a previously approved value \u2014 re-approval required`
404
+ };
405
+ }
406
+ const updated = addTrustedHash(manifest, namespace, currentHash);
407
+ await saveManifest(configDir, updated);
408
+ return {
409
+ identity: { hash: currentHash, trustTier: 3, verified: false },
410
+ tofuConflict: false,
411
+ reason: "First encounter \u2014 hash recorded via TOFU"
412
+ };
413
+ }
414
+
415
+ // src/identity/session.ts
416
+ var CapabilityToken = class {
417
+ // Private field ensures no public surface leaks claims.
418
+ #brand;
419
+ constructor() {
420
+ this.#brand = /* @__PURE__ */ Symbol("CapabilityToken");
421
+ }
422
+ /**
423
+ * Return a non-enumerable identifier for debugging purposes only.
424
+ * Does NOT expose claims.
425
+ */
426
+ toString() {
427
+ return `[CapabilityToken ${this.#brand.toString()}]`;
428
+ }
429
+ };
430
+ var claimsStore = /* @__PURE__ */ new WeakMap();
431
+ function createCapabilityToken(claims) {
432
+ const token = new CapabilityToken();
433
+ claimsStore.set(token, claims);
434
+ return token;
435
+ }
436
+ function validateCapabilityToken(token) {
437
+ const claims = claimsStore.get(token);
438
+ if (claims === void 0) {
439
+ throw new AuthorizationDeniedError("Invalid or unrecognized capability token");
440
+ }
441
+ return claims;
442
+ }
443
+ function getDefaultConfigDir() {
444
+ if (process.platform === "win32") {
445
+ const appData = process.env.APPDATA;
446
+ if (appData !== void 0) {
447
+ return path5.join(appData, "vaultkeeper");
448
+ }
449
+ return path5.join(os4.homedir(), "AppData", "Roaming", "vaultkeeper");
450
+ }
451
+ return path5.join(os4.homedir(), ".config", "vaultkeeper");
452
+ }
453
+ function defaultConfig() {
454
+ return {
455
+ version: 1,
456
+ backends: [{ type: "file", enabled: true }],
457
+ keyRotation: { gracePeriodDays: 7 },
458
+ defaults: { ttlMinutes: 60, trustTier: 3 }
459
+ };
460
+ }
461
+ function isObject(value) {
462
+ return typeof value === "object" && value !== null && !Array.isArray(value);
463
+ }
464
+ function validateBackendEntry(entry, index) {
465
+ if (!isObject(entry)) {
466
+ throw new Error(`backends[${String(index)}] must be an object`);
467
+ }
468
+ if (typeof entry.type !== "string" || entry.type.trim() === "") {
469
+ throw new Error(`backends[${String(index)}].type must be a non-empty string`);
470
+ }
471
+ if (typeof entry.enabled !== "boolean") {
472
+ throw new Error(`backends[${String(index)}].enabled must be a boolean`);
473
+ }
474
+ const result = {
475
+ type: entry.type,
476
+ enabled: entry.enabled
477
+ };
478
+ if (entry.plugin !== void 0) {
479
+ if (typeof entry.plugin !== "boolean") {
480
+ throw new Error(`backends[${String(index)}].plugin must be a boolean`);
481
+ }
482
+ result.plugin = entry.plugin;
483
+ }
484
+ if (entry.path !== void 0) {
485
+ if (typeof entry.path !== "string") {
486
+ throw new Error(`backends[${String(index)}].path must be a string`);
487
+ }
488
+ result.path = entry.path;
489
+ }
490
+ return result;
491
+ }
492
+ function validateConfig(config) {
493
+ if (!isObject(config)) {
494
+ throw new Error("Config must be an object");
495
+ }
496
+ if (typeof config.version !== "number" || config.version !== 1) {
497
+ throw new Error("Config version must be 1");
498
+ }
499
+ if (!Array.isArray(config.backends) || config.backends.length === 0) {
500
+ throw new Error("Config must have at least one backend");
501
+ }
502
+ const backends = config.backends.map(
503
+ (entry, i) => validateBackendEntry(entry, i)
504
+ );
505
+ if (!isObject(config.keyRotation)) {
506
+ throw new Error("Config keyRotation must be an object");
507
+ }
508
+ if (typeof config.keyRotation.gracePeriodDays !== "number" || config.keyRotation.gracePeriodDays <= 0) {
509
+ throw new Error("Config keyRotation.gracePeriodDays must be a positive number");
510
+ }
511
+ if (!isObject(config.defaults)) {
512
+ throw new Error("Config defaults must be an object");
513
+ }
514
+ if (typeof config.defaults.ttlMinutes !== "number" || config.defaults.ttlMinutes <= 0) {
515
+ throw new Error("Config defaults.ttlMinutes must be a positive number");
516
+ }
517
+ const tier = config.defaults.trustTier;
518
+ if (tier !== 1 && tier !== 2 && tier !== 3) {
519
+ throw new Error("Config defaults.trustTier must be 1, 2, or 3");
520
+ }
521
+ const result = {
522
+ version: 1,
523
+ backends,
524
+ keyRotation: {
525
+ gracePeriodDays: config.keyRotation.gracePeriodDays
526
+ },
527
+ defaults: {
528
+ ttlMinutes: config.defaults.ttlMinutes,
529
+ trustTier: tier
530
+ }
531
+ };
532
+ if (config.developmentMode !== void 0) {
533
+ if (!isObject(config.developmentMode)) {
534
+ throw new Error("Config developmentMode must be an object");
535
+ }
536
+ if (!Array.isArray(config.developmentMode.executables)) {
537
+ throw new Error("Config developmentMode.executables must be an array");
538
+ }
539
+ const executables = [];
540
+ for (const [i, exe] of Array.from(config.developmentMode.executables).entries()) {
541
+ if (typeof exe !== "string") {
542
+ throw new Error(`Config developmentMode.executables[${String(i)}] must be a string`);
543
+ }
544
+ executables.push(exe);
545
+ }
546
+ result.developmentMode = { executables };
547
+ }
548
+ return result;
549
+ }
550
+ async function loadConfig(configDir) {
551
+ const dir = configDir ?? getDefaultConfigDir();
552
+ const configPath = path5.join(dir, "config.json");
553
+ let raw;
554
+ try {
555
+ raw = await fs5.readFile(configPath, "utf-8");
556
+ } catch {
557
+ return defaultConfig();
558
+ }
559
+ let parsed;
560
+ try {
561
+ parsed = JSON.parse(raw);
562
+ } catch {
563
+ throw new Error(`Failed to parse config file at ${configPath}`);
564
+ }
565
+ return validateConfig(parsed);
566
+ }
567
+ var KeyManager = class {
568
+ #state = void 0;
569
+ #gracePeriodTimer = void 0;
570
+ #gracePeriodExpiresAt = void 0;
571
+ #rotating = false;
572
+ /** Generate a new 32-byte key with a timestamp-based id. */
573
+ generateKey() {
574
+ const randomSuffix = crypto4.randomBytes(4).toString("hex");
575
+ return {
576
+ id: `k-${String(Date.now())}-${randomSuffix}`,
577
+ key: new Uint8Array(crypto4.randomBytes(32)),
578
+ createdAt: /* @__PURE__ */ new Date()
579
+ };
580
+ }
581
+ /**
582
+ * Initialize the manager with a freshly generated key.
583
+ * Safe to call multiple times; subsequent calls are no-ops.
584
+ */
585
+ init() {
586
+ if (this.#state === void 0) {
587
+ this.#state = { current: this.generateKey() };
588
+ }
589
+ return Promise.resolve();
590
+ }
591
+ /** Return the current (encryption) key. Throws if not initialized. */
592
+ getCurrentKey() {
593
+ const state = this.#requireState();
594
+ return state.current;
595
+ }
596
+ /**
597
+ * Return the previous key if we are still inside a grace period,
598
+ * otherwise `undefined`.
599
+ */
600
+ getPreviousKey() {
601
+ const state = this.#requireState();
602
+ return state.previous;
603
+ }
604
+ /**
605
+ * Find a key by its id, searching current then previous.
606
+ * Returns `undefined` if the key is not found (or the previous key's
607
+ * grace period has expired).
608
+ */
609
+ findKeyById(kid) {
610
+ const state = this.#requireState();
611
+ if (state.current.id === kid) {
612
+ return state.current;
613
+ }
614
+ const { previous } = state;
615
+ if (previous?.id === kid) {
616
+ return previous;
617
+ }
618
+ return void 0;
619
+ }
620
+ /**
621
+ * Rotate the current key: the current key becomes previous, a new key
622
+ * becomes current. A grace-period timer is started; when it fires the
623
+ * previous key is cleared automatically.
624
+ *
625
+ * @throws {RotationInProgressError} if a rotation is already underway.
626
+ */
627
+ rotateKey(gracePeriodMs) {
628
+ if (this.#rotating) {
629
+ throw new RotationInProgressError("A key rotation is already in progress");
630
+ }
631
+ const state = this.#requireState();
632
+ this.#rotating = true;
633
+ this.#clearGracePeriodTimer();
634
+ const newKey = this.generateKey();
635
+ this.#state = { current: newKey, previous: state.current };
636
+ this.#gracePeriodExpiresAt = Date.now() + gracePeriodMs;
637
+ this.#gracePeriodTimer = setTimeout(() => {
638
+ if (this.#state !== void 0) {
639
+ this.#state = { current: this.#state.current };
640
+ }
641
+ this.#gracePeriodExpiresAt = void 0;
642
+ this.#gracePeriodTimer = void 0;
643
+ this.#rotating = false;
644
+ }, gracePeriodMs);
645
+ if (typeof this.#gracePeriodTimer.unref === "function") {
646
+ this.#gracePeriodTimer.unref();
647
+ }
648
+ }
649
+ /**
650
+ * Emergency revocation: immediately clear the previous key and generate
651
+ * a brand-new current key. Any in-flight grace period is cancelled.
652
+ */
653
+ revokeKey() {
654
+ this.#clearGracePeriodTimer();
655
+ this.#rotating = false;
656
+ this.#gracePeriodExpiresAt = void 0;
657
+ const newKey = this.generateKey();
658
+ this.#state = { current: newKey };
659
+ }
660
+ /**
661
+ * Return `true` while a rotation grace period is active (i.e. the previous
662
+ * key is still accessible).
663
+ */
664
+ isInGracePeriod() {
665
+ if (this.#gracePeriodExpiresAt === void 0) {
666
+ return false;
667
+ }
668
+ return Date.now() < this.#gracePeriodExpiresAt;
669
+ }
670
+ // ---------------------------------------------------------------------------
671
+ // Private helpers
672
+ // ---------------------------------------------------------------------------
673
+ #requireState() {
674
+ if (this.#state === void 0) {
675
+ throw new SetupError(
676
+ "KeyManager has not been initialized \u2014 call init() first",
677
+ "KeyManager"
678
+ );
679
+ }
680
+ return this.#state;
681
+ }
682
+ #clearGracePeriodTimer() {
683
+ if (this.#gracePeriodTimer !== void 0) {
684
+ clearTimeout(this.#gracePeriodTimer);
685
+ this.#gracePeriodTimer = void 0;
686
+ }
687
+ }
688
+ };
689
+ var ALGORITHM = "dir";
690
+ var ENCRYPTION = "A256GCM";
691
+ async function createToken(key, claims, options) {
692
+ const plaintext = new TextEncoder().encode(JSON.stringify(claims));
693
+ const header = {
694
+ alg: ALGORITHM,
695
+ enc: ENCRYPTION
696
+ };
697
+ if (options?.kid !== void 0) {
698
+ header.kid = options.kid;
699
+ }
700
+ return new CompactEncrypt(plaintext).setProtectedHeader(header).encrypt(key);
701
+ }
702
+ function isObject2(value) {
703
+ return typeof value === "object" && value !== null && !Array.isArray(value);
704
+ }
705
+ function parseVaultClaims(raw) {
706
+ if (!isObject2(raw)) {
707
+ return void 0;
708
+ }
709
+ const { jti, exp, iat, sub, exe, use, tid, bkd, val, ref } = raw;
710
+ if (typeof jti !== "string") return void 0;
711
+ if (typeof exp !== "number") return void 0;
712
+ if (typeof iat !== "number") return void 0;
713
+ if (typeof sub !== "string") return void 0;
714
+ if (typeof exe !== "string") return void 0;
715
+ if (use !== null && typeof use !== "number") return void 0;
716
+ if (tid !== 1 && tid !== 2 && tid !== 3) return void 0;
717
+ if (typeof bkd !== "string") return void 0;
718
+ if (typeof val !== "string") return void 0;
719
+ if (typeof ref !== "string") return void 0;
720
+ return { jti, exp, iat, sub, exe, use: use ?? null, tid, bkd, val, ref };
721
+ }
722
+ async function decryptToken(key, jwe) {
723
+ let plaintext;
724
+ try {
725
+ const result = await compactDecrypt(jwe, key);
726
+ plaintext = result.plaintext;
727
+ } catch (err) {
728
+ const message = err instanceof Error ? err.message : String(err);
729
+ throw new VaultError(`JWE decryption failed: ${message}`);
730
+ }
731
+ let parsed;
732
+ try {
733
+ parsed = JSON.parse(new TextDecoder().decode(plaintext));
734
+ } catch {
735
+ throw new VaultError("JWE payload is not valid JSON");
736
+ }
737
+ const claims = parseVaultClaims(parsed);
738
+ if (claims === void 0) {
739
+ throw new VaultError("JWE payload does not match VaultClaims schema");
740
+ }
741
+ return claims;
742
+ }
743
+ function extractKid(jwe) {
744
+ const parts = jwe.split(".");
745
+ if (parts.length !== 5) {
746
+ throw new VaultError("Invalid JWE compact serialization: expected 5 parts");
747
+ }
748
+ const headerSegment = parts[0];
749
+ if (headerSegment === void 0 || headerSegment === "") {
750
+ throw new VaultError("Invalid JWE compact serialization: missing header segment");
751
+ }
752
+ let headerJson;
753
+ try {
754
+ headerJson = Buffer.from(headerSegment, "base64url").toString("utf-8");
755
+ } catch {
756
+ throw new VaultError("Invalid JWE compact serialization: header is not valid Base64URL");
757
+ }
758
+ let header;
759
+ try {
760
+ header = JSON.parse(headerJson);
761
+ } catch {
762
+ throw new VaultError("Invalid JWE compact serialization: header is not valid JSON");
763
+ }
764
+ if (!isObject2(header)) {
765
+ return void 0;
766
+ }
767
+ const kid = header.kid;
768
+ if (typeof kid !== "string") {
769
+ return void 0;
770
+ }
771
+ return kid;
772
+ }
773
+
774
+ // src/jwe/claims.ts
775
+ var BLOCKLIST_MAX_SIZE = 1e4;
776
+ var blocklist = /* @__PURE__ */ new Map();
777
+ function blockToken(jti) {
778
+ if (blocklist.has(jti)) {
779
+ blocklist.delete(jti);
780
+ } else if (blocklist.size >= BLOCKLIST_MAX_SIZE) {
781
+ const oldestKey = blocklist.keys().next().value;
782
+ if (oldestKey !== void 0) {
783
+ blocklist.delete(oldestKey);
784
+ }
785
+ }
786
+ blocklist.set(jti, true);
787
+ }
788
+ function isBlocked(jti) {
789
+ return blocklist.has(jti);
790
+ }
791
+ function validateClaims(claims, usedCount = 0) {
792
+ if (claims.jti.trim() === "") {
793
+ throw new VaultError("Invalid token: jti must not be empty");
794
+ }
795
+ if (claims.sub.trim() === "") {
796
+ throw new VaultError("Invalid token: sub must not be empty");
797
+ }
798
+ if (claims.exe.trim() === "") {
799
+ throw new VaultError("Invalid token: exe must not be empty");
800
+ }
801
+ if (claims.bkd.trim() === "") {
802
+ throw new VaultError("Invalid token: bkd must not be empty");
803
+ }
804
+ if (claims.val.trim() === "") {
805
+ throw new VaultError("Invalid token: val must not be empty");
806
+ }
807
+ if (claims.ref.trim() === "") {
808
+ throw new VaultError("Invalid token: ref must not be empty");
809
+ }
810
+ if (claims.iat > claims.exp) {
811
+ throw new VaultError("Invalid token: iat must not be after exp");
812
+ }
813
+ const nowSec = Math.floor(Date.now() / 1e3);
814
+ if (nowSec >= claims.exp) {
815
+ throw new TokenExpiredError(
816
+ `Token expired at ${String(claims.exp)} (now: ${String(nowSec)})`,
817
+ false
818
+ );
819
+ }
820
+ if (isBlocked(claims.jti)) {
821
+ throw new TokenRevokedError(`Token ${claims.jti} has been revoked`);
822
+ }
823
+ if (claims.use !== null) {
824
+ if (claims.use <= 0) {
825
+ throw new UsageLimitExceededError(
826
+ `Token ${claims.jti} has a non-positive usage limit: ${String(claims.use)}`
827
+ );
828
+ }
829
+ if (usedCount >= claims.use) {
830
+ throw new UsageLimitExceededError(
831
+ `Token ${claims.jti} usage limit of ${String(claims.use)} exceeded (used: ${String(usedCount)})`
832
+ );
833
+ }
834
+ }
835
+ }
836
+
837
+ // src/access/delegated-fetch.ts
838
+ var PLACEHOLDER = "{{secret}}";
839
+ function replacePlaceholder(value, secret) {
840
+ return value.replaceAll(PLACEHOLDER, secret);
841
+ }
842
+ function replaceInRecord(record, secret) {
843
+ const result = {};
844
+ for (const [key, value] of Object.entries(record)) {
845
+ result[key] = replacePlaceholder(value, secret);
846
+ }
847
+ return result;
848
+ }
849
+ async function delegatedFetch(secret, request) {
850
+ const url = replacePlaceholder(request.url, secret);
851
+ const headers = request.headers !== void 0 ? replaceInRecord(request.headers, secret) : void 0;
852
+ const body = request.body !== void 0 ? replacePlaceholder(request.body, secret) : void 0;
853
+ const init = {};
854
+ if (request.method !== void 0) {
855
+ init.method = request.method;
856
+ }
857
+ if (headers !== void 0) {
858
+ init.headers = headers;
859
+ }
860
+ if (body !== void 0) {
861
+ init.body = body;
862
+ }
863
+ return fetch(url, init);
864
+ }
865
+ var PLACEHOLDER2 = "{{secret}}";
866
+ function replacePlaceholder2(value, secret) {
867
+ return value.replaceAll(PLACEHOLDER2, secret);
868
+ }
869
+ function replaceInRecord2(record, secret) {
870
+ const result = {};
871
+ for (const [key, value] of Object.entries(record)) {
872
+ result[key] = replacePlaceholder2(value, secret);
873
+ }
874
+ return result;
875
+ }
876
+ function delegatedExec(secret, request) {
877
+ const args = (request.args ?? []).map((arg) => replacePlaceholder2(arg, secret));
878
+ const env = request.env !== void 0 ? replaceInRecord2(request.env, secret) : void 0;
879
+ return new Promise((resolve, reject) => {
880
+ const spawnOptions = {
881
+ stdio: ["ignore", "pipe", "pipe"]
882
+ };
883
+ if (env !== void 0) {
884
+ spawnOptions.env = { ...process.env, ...env };
885
+ }
886
+ if (request.cwd !== void 0) {
887
+ spawnOptions.cwd = request.cwd;
888
+ }
889
+ const proc = spawn(request.command, args, spawnOptions);
890
+ let stdout = "";
891
+ let stderr = "";
892
+ proc.stdout?.on("data", (data) => {
893
+ stdout += data.toString();
894
+ });
895
+ proc.stderr?.on("data", (data) => {
896
+ stderr += data.toString();
897
+ });
898
+ proc.on("close", (code) => {
899
+ resolve({ stdout, stderr, exitCode: code ?? 1 });
900
+ });
901
+ proc.on("error", (error) => {
902
+ reject(error);
903
+ });
904
+ });
905
+ }
906
+
907
+ // src/access/controlled-direct.ts
908
+ var INSPECT_CUSTOM = /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom");
909
+ var SecretAccessorTarget = class {
910
+ read;
911
+ [INSPECT_CUSTOM];
912
+ constructor(readImpl, inspectImpl) {
913
+ this.read = readImpl;
914
+ this[INSPECT_CUSTOM] = inspectImpl;
915
+ }
916
+ };
917
+ function createSecretAccessor(secretValue) {
918
+ let consumed = false;
919
+ const revokeHolder = { fn: void 0 };
920
+ function readImpl(callback) {
921
+ if (consumed) {
922
+ throw new Error("SecretAccessor has already been consumed \u2014 call getSecret() again to obtain a new accessor");
923
+ }
924
+ consumed = true;
925
+ const buf = Buffer.from(secretValue, "utf8");
926
+ try {
927
+ callback(buf);
928
+ } finally {
929
+ buf.fill(0);
930
+ revokeHolder.fn?.();
931
+ }
932
+ }
933
+ function inspectImpl() {
934
+ return "[SecretAccessor]";
935
+ }
936
+ const target = new SecretAccessorTarget(readImpl, inspectImpl);
937
+ Object.setPrototypeOf(target, null);
938
+ Object.preventExtensions(target);
939
+ const handler = {
940
+ get(_t, prop, _receiver) {
941
+ if (prop === "read") return readImpl;
942
+ if (prop === INSPECT_CUSTOM) return inspectImpl;
943
+ return void 0;
944
+ },
945
+ set(_t, _prop, _value, _receiver) {
946
+ return false;
947
+ },
948
+ has(_t, prop) {
949
+ return prop === "read" || prop === INSPECT_CUSTOM;
950
+ },
951
+ deleteProperty(_t, _prop) {
952
+ return false;
953
+ },
954
+ apply(_t, _thisArg, _args) {
955
+ throw new TypeError("SecretAccessor is not a function");
956
+ },
957
+ construct(_t, _args, _newTarget) {
958
+ throw new TypeError("SecretAccessor is not a constructor");
959
+ },
960
+ defineProperty(_t, _prop, _descriptor) {
961
+ return false;
962
+ },
963
+ getOwnPropertyDescriptor(_t, prop) {
964
+ if (prop === "read") {
965
+ return { configurable: true, enumerable: true, writable: false, value: readImpl };
966
+ }
967
+ if (prop === INSPECT_CUSTOM) {
968
+ return { configurable: true, enumerable: false, writable: false, value: inspectImpl };
969
+ }
970
+ return void 0;
971
+ },
972
+ getPrototypeOf(_t) {
973
+ return null;
974
+ },
975
+ setPrototypeOf(_t, _proto) {
976
+ return false;
977
+ },
978
+ isExtensible(_t) {
979
+ return false;
980
+ },
981
+ preventExtensions(_t) {
982
+ return true;
983
+ },
984
+ ownKeys(_t) {
985
+ return ["read", INSPECT_CUSTOM];
986
+ }
987
+ };
988
+ const { proxy, revoke } = Proxy.revocable(target, handler);
989
+ revokeHolder.fn = revoke;
990
+ return proxy;
991
+ }
992
+
993
+ // src/doctor/checks.ts
994
+ function parseVersion(raw) {
995
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(raw);
996
+ if (!match) return null;
997
+ const major = parseInt(match[1] ?? "0", 10);
998
+ const minor = parseInt(match[2] ?? "0", 10);
999
+ const patch = parseInt(match[3] ?? "0", 10);
1000
+ return [major, minor, patch];
1001
+ }
1002
+ function versionGte(a, b) {
1003
+ if (a[0] !== b[0]) return a[0] > b[0];
1004
+ if (a[1] !== b[1]) return a[1] > b[1];
1005
+ return a[2] >= b[2];
1006
+ }
1007
+ async function checkOpenssl() {
1008
+ const name = "openssl";
1009
+ try {
1010
+ const output = await execCommand("openssl", ["version"]);
1011
+ const parsed = parseVersion(output);
1012
+ if (!parsed) {
1013
+ return {
1014
+ name,
1015
+ status: "version-unsupported",
1016
+ version: output,
1017
+ reason: "Could not parse openssl version"
1018
+ };
1019
+ }
1020
+ if (!versionGte(parsed, [1, 1, 1])) {
1021
+ return {
1022
+ name,
1023
+ status: "version-unsupported",
1024
+ version: output,
1025
+ reason: "openssl >= 1.1.1 is required"
1026
+ };
1027
+ }
1028
+ return { name, status: "ok", version: output };
1029
+ } catch {
1030
+ return { name, status: "missing", reason: "openssl not found in PATH" };
1031
+ }
1032
+ }
1033
+ async function checkBash() {
1034
+ const name = "bash";
1035
+ try {
1036
+ const output = await execCommand("bash", ["--version"]);
1037
+ const firstLine = output.split("\n")[0] ?? output;
1038
+ return { name, status: "ok", version: firstLine };
1039
+ } catch {
1040
+ return { name, status: "missing", reason: "bash not found in PATH" };
1041
+ }
1042
+ }
1043
+ async function checkPowershell() {
1044
+ const name = "powershell";
1045
+ try {
1046
+ const output = await execCommand("powershell", [
1047
+ "-Command",
1048
+ "$PSVersionTable.PSVersion"
1049
+ ]);
1050
+ const version = output.trim();
1051
+ return { name, status: "ok", version };
1052
+ } catch {
1053
+ return { name, status: "missing", reason: "powershell not found in PATH" };
1054
+ }
1055
+ }
1056
+ async function checkSecurity() {
1057
+ const name = "security";
1058
+ try {
1059
+ await execCommand("security", ["help"]);
1060
+ return { name, status: "ok" };
1061
+ } catch (err) {
1062
+ const message = err instanceof Error ? err.message : String(err);
1063
+ if (message.includes("security")) {
1064
+ return { name, status: "ok" };
1065
+ }
1066
+ return {
1067
+ name,
1068
+ status: "missing",
1069
+ reason: "security command not found in PATH"
1070
+ };
1071
+ }
1072
+ }
1073
+ async function checkSecretTool() {
1074
+ const name = "secret-tool";
1075
+ try {
1076
+ const output = await execCommand("secret-tool", ["--version"]);
1077
+ return { name, status: "ok", version: output.trim() };
1078
+ } catch {
1079
+ return {
1080
+ name,
1081
+ status: "missing",
1082
+ reason: "secret-tool not found in PATH (install libsecret-tools)"
1083
+ };
1084
+ }
1085
+ }
1086
+ async function checkOp() {
1087
+ const name = "op";
1088
+ try {
1089
+ const output = await execCommand("op", ["--version"]);
1090
+ return { name, status: "ok", version: output.trim() };
1091
+ } catch {
1092
+ return {
1093
+ name,
1094
+ status: "missing",
1095
+ reason: "op (1Password CLI) not found in PATH"
1096
+ };
1097
+ }
1098
+ }
1099
+ async function checkYkman() {
1100
+ const name = "ykman";
1101
+ try {
1102
+ const output = await execCommand("ykman", ["--version"]);
1103
+ return { name, status: "ok", version: output.trim() };
1104
+ } catch {
1105
+ return {
1106
+ name,
1107
+ status: "missing",
1108
+ reason: "ykman (YubiKey Manager) not found in PATH"
1109
+ };
1110
+ }
1111
+ }
1112
+
1113
+ // src/util/platform.ts
1114
+ function currentPlatform() {
1115
+ const p = process.platform;
1116
+ if (p === "darwin" || p === "win32" || p === "linux") {
1117
+ return p;
1118
+ }
1119
+ throw new Error(`Unsupported platform: ${p}`);
1120
+ }
1121
+
1122
+ // src/doctor/runner.ts
1123
+ async function runDoctor(options) {
1124
+ const platform = currentPlatform();
1125
+ const entries = buildCheckList(platform);
1126
+ const resolved = await Promise.all(
1127
+ entries.map(async ({ check, required }) => {
1128
+ const result = await check();
1129
+ return { required, result };
1130
+ })
1131
+ );
1132
+ const ready = resolved.every(({ required, result }) => {
1133
+ if (!required) return true;
1134
+ return result.status === "ok";
1135
+ });
1136
+ const warnings = [];
1137
+ const nextSteps = [];
1138
+ for (const { required, result } of resolved) {
1139
+ if (result.status === "missing") {
1140
+ if (required) {
1141
+ nextSteps.push(`Install missing required dependency: ${result.name}`);
1142
+ } else {
1143
+ warnings.push(
1144
+ `Optional dependency not found: ${result.name}${result.reason !== void 0 ? ` \u2014 ${result.reason}` : ""}`
1145
+ );
1146
+ }
1147
+ } else if (result.status === "version-unsupported") {
1148
+ const msg = `${result.name} version is unsupported${result.reason !== void 0 ? `: ${result.reason}` : ""}`;
1149
+ if (required) {
1150
+ nextSteps.push(`Upgrade required dependency: ${msg}`);
1151
+ } else {
1152
+ warnings.push(`Optional dependency version unsupported: ${msg}`);
1153
+ }
1154
+ }
1155
+ }
1156
+ const checks = resolved.map(({ result }) => result);
1157
+ return { checks, ready, warnings, nextSteps };
1158
+ }
1159
+ function buildCheckList(platform) {
1160
+ const entries = [{ check: checkOpenssl, required: true }];
1161
+ if (platform === "darwin") {
1162
+ entries.push({ check: checkSecurity, required: true });
1163
+ entries.push({ check: checkBash, required: false });
1164
+ } else if (platform === "win32") {
1165
+ entries.push({ check: checkPowershell, required: true });
1166
+ } else {
1167
+ entries.push({ check: checkBash, required: true });
1168
+ entries.push({ check: checkSecretTool, required: true });
1169
+ }
1170
+ entries.push({ check: checkOp, required: false });
1171
+ entries.push({ check: checkYkman, required: false });
1172
+ return entries;
1173
+ }
1174
+
1175
+ // src/vault.ts
1176
+ var usageCounts = /* @__PURE__ */ new Map();
1177
+ var VaultKeeper = class _VaultKeeper {
1178
+ #config;
1179
+ #keyManager;
1180
+ #configDir;
1181
+ #backend;
1182
+ constructor(config, keyManager, configDir) {
1183
+ this.#config = config;
1184
+ this.#keyManager = keyManager;
1185
+ this.#configDir = configDir;
1186
+ }
1187
+ /**
1188
+ * Initialize a new VaultKeeper instance.
1189
+ * Runs doctor checks (unless skipped), loads config, and sets up the key manager.
1190
+ */
1191
+ static async init(options) {
1192
+ if (options?.skipDoctor !== true) {
1193
+ const doctorResult = await runDoctor();
1194
+ if (!doctorResult.ready) {
1195
+ throw new VaultError(
1196
+ `System not ready: ${doctorResult.nextSteps.join("; ")}`
1197
+ );
1198
+ }
1199
+ }
1200
+ const configDir = options?.configDir ?? getDefaultConfigDir();
1201
+ const config = options?.config ?? await loadConfig(configDir);
1202
+ const keyManager = new KeyManager();
1203
+ await keyManager.init();
1204
+ const vault = new _VaultKeeper(config, keyManager, configDir);
1205
+ vault.#backend = vault.#resolveBackend();
1206
+ return vault;
1207
+ }
1208
+ /** Run doctor checks without full initialization. */
1209
+ static async doctor() {
1210
+ return runDoctor();
1211
+ }
1212
+ /**
1213
+ * Store a secret and return a JWE token that encapsulates it.
1214
+ *
1215
+ * @param secretName - Identifier for the secret
1216
+ * @param options - Setup options
1217
+ * @returns Compact JWE string
1218
+ */
1219
+ async setup(secretName, options) {
1220
+ const backend = this.#requireBackend();
1221
+ const backendType = options?.backendType ?? backend.type;
1222
+ const ttlMinutes = options?.ttlMinutes ?? this.#config.defaults.ttlMinutes;
1223
+ const trustTier = options?.trustTier ?? this.#config.defaults.trustTier;
1224
+ const useLimit = options?.useLimit ?? null;
1225
+ const executablePath = options?.executablePath ?? "dev";
1226
+ const secretValue = await backend.retrieve(secretName);
1227
+ let exeIdentity;
1228
+ if (executablePath === "dev" || this.#isDevModeExecutable(executablePath)) {
1229
+ exeIdentity = "dev";
1230
+ } else {
1231
+ const trustResult = await verifyTrust(executablePath, {
1232
+ configDir: this.#configDir
1233
+ });
1234
+ if (trustResult.tofuConflict) {
1235
+ throw new IdentityMismatchError(
1236
+ "Executable hash changed \u2014 re-approval required",
1237
+ "previously-approved",
1238
+ trustResult.identity.hash
1239
+ );
1240
+ }
1241
+ exeIdentity = trustResult.identity.hash;
1242
+ }
1243
+ const now = Math.floor(Date.now() / 1e3);
1244
+ const claims = {
1245
+ jti: crypto4.randomUUID(),
1246
+ exp: now + ttlMinutes * 60,
1247
+ iat: now,
1248
+ sub: secretName,
1249
+ exe: exeIdentity,
1250
+ use: useLimit,
1251
+ tid: trustTier,
1252
+ bkd: backendType,
1253
+ val: secretValue,
1254
+ ref: secretName
1255
+ };
1256
+ const currentKey = this.#keyManager.getCurrentKey();
1257
+ return createToken(currentKey.key, claims, { kid: currentKey.id });
1258
+ }
1259
+ /**
1260
+ * Decrypt a JWE, validate claims, verify executable identity, and return
1261
+ * an opaque CapabilityToken.
1262
+ *
1263
+ * @param jwe - Compact JWE string from setup()
1264
+ * @returns Opaque capability token for use with fetch/exec/getSecret
1265
+ */
1266
+ async authorize(jwe) {
1267
+ const kid = extractKid(jwe);
1268
+ const { claims, keyStatus } = await this.#decryptWithKeyResolution(jwe, kid);
1269
+ const jti = claims.jti;
1270
+ const currentCount = usageCounts.get(jti) ?? 0;
1271
+ validateClaims(claims, currentCount);
1272
+ const newCount = currentCount + 1;
1273
+ if (claims.use !== null && newCount >= claims.use) {
1274
+ usageCounts.delete(jti);
1275
+ blockToken(jti);
1276
+ } else {
1277
+ usageCounts.set(jti, newCount);
1278
+ }
1279
+ const token = createCapabilityToken(claims);
1280
+ const response = { keyStatus };
1281
+ if (keyStatus === "previous") {
1282
+ const currentKey = this.#keyManager.getCurrentKey();
1283
+ const rotatedJwt = await createToken(currentKey.key, claims, { kid: currentKey.id });
1284
+ response.rotatedJwt = rotatedJwt;
1285
+ }
1286
+ return { token, response };
1287
+ }
1288
+ /**
1289
+ * Execute a delegated HTTP fetch, injecting the secret from the token.
1290
+ *
1291
+ * The secret value is substituted for every `{{secret}}` placeholder found
1292
+ * in `request.url`, `request.headers`, and `request.body` before the fetch
1293
+ * is executed. The raw secret is never exposed in the return value.
1294
+ *
1295
+ * @param token - A `CapabilityToken` obtained from `authorize()`.
1296
+ * @param request - The fetch request template. Use `{{secret}}` as a
1297
+ * placeholder wherever the secret value should be injected.
1298
+ * @returns The `Response` from the underlying `fetch()` call, together with
1299
+ * the vault metadata (`vaultResponse`).
1300
+ * @throws {Error} If `token` is invalid or was not created by this vault
1301
+ * instance.
1302
+ */
1303
+ async fetch(token, request) {
1304
+ const claims = validateCapabilityToken(token);
1305
+ const response = await delegatedFetch(claims.val, request);
1306
+ return {
1307
+ response,
1308
+ vaultResponse: { keyStatus: "current" }
1309
+ };
1310
+ }
1311
+ /**
1312
+ * Execute a delegated command, injecting the secret from the token.
1313
+ *
1314
+ * The secret value is substituted for every `{{secret}}` placeholder found
1315
+ * in `request.args` and `request.env` values before the process is spawned.
1316
+ * The raw secret is never exposed in the return value.
1317
+ *
1318
+ * @param token - A `CapabilityToken` obtained from `authorize()`.
1319
+ * @param request - The exec request template. Use `{{secret}}` as a
1320
+ * placeholder wherever the secret value should be injected.
1321
+ * @returns The command result (`stdout`, `stderr`, `exitCode`) together with
1322
+ * the vault metadata (`vaultResponse`).
1323
+ * @throws {Error} If `token` is invalid or was not created by this vault
1324
+ * instance.
1325
+ */
1326
+ async exec(token, request) {
1327
+ const claims = validateCapabilityToken(token);
1328
+ const result = await delegatedExec(claims.val, request);
1329
+ return {
1330
+ result,
1331
+ vaultResponse: { keyStatus: "current" }
1332
+ };
1333
+ }
1334
+ /**
1335
+ * Create a controlled-direct `SecretAccessor` from a capability token.
1336
+ *
1337
+ * The accessor wraps the secret in a single-use, auto-zeroing `Buffer`. The
1338
+ * secret is accessible only through the `read()` callback and is zeroed
1339
+ * immediately after the callback returns.
1340
+ *
1341
+ * @param token - A `CapabilityToken` obtained from `authorize()`.
1342
+ * @returns A `SecretAccessor` that can be read exactly once.
1343
+ * @throws {Error} If `token` is invalid or was not created by this vault
1344
+ * instance.
1345
+ */
1346
+ getSecret(token) {
1347
+ const claims = validateCapabilityToken(token);
1348
+ return createSecretAccessor(claims.val);
1349
+ }
1350
+ /**
1351
+ * Rotate the current encryption key.
1352
+ *
1353
+ * The previous key remains valid for decryption during the grace period
1354
+ * configured in `keyRotation.gracePeriodDays`. JWEs presented during the
1355
+ * grace period return a `rotatedJwt` in the `VaultResponse` so callers can
1356
+ * persist the updated token.
1357
+ *
1358
+ * @throws {RotationInProgressError} If a rotation is already in progress
1359
+ * (i.e. a previous key is still within its grace period).
1360
+ */
1361
+ async rotateKey() {
1362
+ const gracePeriodMs = this.#config.keyRotation.gracePeriodDays * 24 * 60 * 60 * 1e3;
1363
+ this.#keyManager.rotateKey(gracePeriodMs);
1364
+ await Promise.resolve();
1365
+ }
1366
+ /**
1367
+ * Emergency key revocation — invalidates the previous key immediately.
1368
+ *
1369
+ * After revocation, any JWE that was encrypted with the revoked key will
1370
+ * be permanently unreadable. A new encryption key is generated automatically
1371
+ * so that `setup()` can be called immediately after revocation.
1372
+ */
1373
+ async revokeKey() {
1374
+ this.#keyManager.revokeKey();
1375
+ await Promise.resolve();
1376
+ }
1377
+ /**
1378
+ * Add or remove an executable from the development-mode whitelist.
1379
+ *
1380
+ * When an executable is in the development-mode list, identity verification
1381
+ * (TOFU hash checking) is skipped for that executable during `setup()`. This
1382
+ * is intended for local development workflows where the binary changes
1383
+ * frequently.
1384
+ *
1385
+ * @param executablePath - Absolute path to the executable to add or remove.
1386
+ * @param enabled - Pass `true` to add the executable to the list, `false`
1387
+ * to remove it.
1388
+ */
1389
+ async setDevelopmentMode(executablePath, enabled) {
1390
+ if (this.#config.developmentMode === void 0) {
1391
+ if (enabled) {
1392
+ this.#config.developmentMode = { executables: [executablePath] };
1393
+ }
1394
+ return;
1395
+ }
1396
+ const exes = this.#config.developmentMode.executables;
1397
+ const idx = exes.indexOf(executablePath);
1398
+ if (enabled && idx === -1) {
1399
+ exes.push(executablePath);
1400
+ } else if (!enabled && idx !== -1) {
1401
+ exes.splice(idx, 1);
1402
+ }
1403
+ await Promise.resolve();
1404
+ }
1405
+ // ---------------------------------------------------------------------------
1406
+ // Private helpers
1407
+ // ---------------------------------------------------------------------------
1408
+ #resolveBackend() {
1409
+ const enabledBackends = this.#config.backends.filter((b) => b.enabled);
1410
+ if (enabledBackends.length === 0) {
1411
+ throw new BackendUnavailableError(
1412
+ "No enabled backends configured",
1413
+ "none-enabled",
1414
+ this.#config.backends.map((b) => b.type)
1415
+ );
1416
+ }
1417
+ const firstEnabled = enabledBackends[0];
1418
+ if (firstEnabled === void 0) {
1419
+ throw new BackendUnavailableError(
1420
+ "No enabled backends configured",
1421
+ "none-enabled",
1422
+ []
1423
+ );
1424
+ }
1425
+ return BackendRegistry.create(firstEnabled.type);
1426
+ }
1427
+ #requireBackend() {
1428
+ if (this.#backend === void 0) {
1429
+ throw new VaultError("VaultKeeper backend not initialized");
1430
+ }
1431
+ return this.#backend;
1432
+ }
1433
+ #isDevModeExecutable(executablePath) {
1434
+ if (this.#config.developmentMode === void 0) {
1435
+ return false;
1436
+ }
1437
+ return this.#config.developmentMode.executables.includes(executablePath);
1438
+ }
1439
+ async #decryptWithKeyResolution(jwe, kid) {
1440
+ if (kid !== void 0) {
1441
+ const key = this.#keyManager.findKeyById(kid);
1442
+ if (key !== void 0) {
1443
+ const claims = await decryptToken(key.key, jwe);
1444
+ const isCurrent = key.id === this.#keyManager.getCurrentKey().id;
1445
+ return {
1446
+ claims,
1447
+ keyStatus: isCurrent ? "current" : "previous"
1448
+ };
1449
+ }
1450
+ throw new KeyRevokedError(`Key ${kid} not found \u2014 may have been revoked`);
1451
+ }
1452
+ try {
1453
+ const claims = await decryptToken(this.#keyManager.getCurrentKey().key, jwe);
1454
+ return { claims, keyStatus: "current" };
1455
+ } catch {
1456
+ const previousKey = this.#keyManager.getPreviousKey();
1457
+ if (previousKey !== void 0) {
1458
+ const claims = await decryptToken(previousKey.key, jwe);
1459
+ return { claims, keyStatus: "previous" };
1460
+ }
1461
+ throw new VaultError("Failed to decrypt JWE with any available key");
1462
+ }
1463
+ }
1464
+ };
1465
+
1466
+ export { AuthorizationDeniedError, BackendLockedError, BackendRegistry, BackendUnavailableError, CapabilityToken, DeviceNotPresentError, FilesystemError, IdentityMismatchError, KeyRevokedError, KeyRotatedError, PluginNotFoundError, RotationInProgressError, SecretNotFoundError, SetupError, TokenExpiredError, TokenRevokedError, UsageLimitExceededError, VaultError, VaultKeeper, isListableBackend };
1467
+ //# sourceMappingURL=index.js.map
1468
+ //# sourceMappingURL=index.js.map