vaultkeeper 0.3.0 → 0.5.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.cjs CHANGED
@@ -1,13 +1,15 @@
1
1
  'use strict';
2
2
 
3
- var child_process = require('child_process');
4
- var fs5 = require('fs/promises');
5
- var path5 = require('path');
3
+ var fs = require('fs/promises');
4
+ var path3 = require('path');
6
5
  var os4 = require('os');
7
- var crypto4 = require('crypto');
6
+ var crypto = require('crypto');
7
+ var child_process = require('child_process');
8
+ var url = require('url');
8
9
  var fs4 = require('fs');
9
10
  var jose = require('jose');
10
11
 
12
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
11
13
  function _interopNamespace(e) {
12
14
  if (e && e.__esModule) return e;
13
15
  var n = Object.create(null);
@@ -26,10 +28,10 @@ function _interopNamespace(e) {
26
28
  return Object.freeze(n);
27
29
  }
28
30
 
29
- var fs5__namespace = /*#__PURE__*/_interopNamespace(fs5);
30
- var path5__namespace = /*#__PURE__*/_interopNamespace(path5);
31
+ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
32
+ var path3__namespace = /*#__PURE__*/_interopNamespace(path3);
31
33
  var os4__namespace = /*#__PURE__*/_interopNamespace(os4);
32
- var crypto4__namespace = /*#__PURE__*/_interopNamespace(crypto4);
34
+ var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
33
35
  var fs4__namespace = /*#__PURE__*/_interopNamespace(fs4);
34
36
 
35
37
  // src/errors.ts
@@ -162,6 +164,22 @@ var IdentityMismatchError = class extends VaultError {
162
164
  this.currentHash = currentHash;
163
165
  }
164
166
  };
167
+ var InvalidAlgorithmError = class extends VaultError {
168
+ /**
169
+ * The algorithm that was requested.
170
+ */
171
+ algorithm;
172
+ /**
173
+ * The set of algorithms that are allowed.
174
+ */
175
+ allowed;
176
+ constructor(message, algorithm, allowed) {
177
+ super(message);
178
+ this.name = "InvalidAlgorithmError";
179
+ this.algorithm = algorithm;
180
+ this.allowed = allowed;
181
+ }
182
+ };
165
183
  var SetupError = class extends VaultError {
166
184
  /**
167
185
  * The name of the dependency that caused the setup failure.
@@ -197,14 +215,10 @@ var RotationInProgressError = class extends VaultError {
197
215
  }
198
216
  };
199
217
 
200
- // src/backend/types.ts
201
- function isListableBackend(backend) {
202
- return "list" in backend && typeof backend.list === "function";
203
- }
204
-
205
218
  // src/backend/registry.ts
206
219
  var BackendRegistry = class {
207
220
  static backends = /* @__PURE__ */ new Map();
221
+ static setups = /* @__PURE__ */ new Map();
208
222
  /**
209
223
  * Register a backend factory.
210
224
  * @param type - Backend type identifier
@@ -216,10 +230,11 @@ var BackendRegistry = class {
216
230
  /**
217
231
  * Create a backend instance by type.
218
232
  * @param type - Backend type identifier
233
+ * @param config - Optional backend configuration forwarded to the factory
219
234
  * @returns A SecretBackend instance
220
- * @throws Error if the backend type is not registered
235
+ * @throws {@link BackendUnavailableError} if the backend type is not registered
221
236
  */
222
- static create(type) {
237
+ static create(type, config) {
223
238
  const factory = this.backends.get(type);
224
239
  if (factory === void 0) {
225
240
  throw new BackendUnavailableError(
@@ -228,7 +243,7 @@ var BackendRegistry = class {
228
243
  Array.from(this.backends.keys())
229
244
  );
230
245
  }
231
- return factory();
246
+ return factory(config);
232
247
  }
233
248
  /**
234
249
  * Get all registered backend type identifiers.
@@ -264,18 +279,199 @@ var BackendRegistry = class {
264
279
  );
265
280
  return results.filter((type) => type !== null);
266
281
  }
282
+ /**
283
+ * Register a setup factory for a backend type.
284
+ * @param type - Backend type identifier
285
+ * @param factory - Factory function that creates a setup generator
286
+ */
287
+ static registerSetup(type, factory) {
288
+ this.setups.set(type, factory);
289
+ }
290
+ /**
291
+ * Get the setup factory for a backend type, if one is registered.
292
+ * @param type - Backend type identifier
293
+ * @returns The setup factory, or `undefined` if none is registered
294
+ */
295
+ static getSetup(type) {
296
+ return this.setups.get(type);
297
+ }
298
+ /**
299
+ * Check whether a setup factory is registered for the given backend type.
300
+ * @param type - Backend type identifier
301
+ * @returns `true` if a setup factory is registered
302
+ */
303
+ static hasSetup(type) {
304
+ return this.setups.has(type);
305
+ }
306
+ /**
307
+ * Clear all registered backend factories.
308
+ * Intended for use in tests only.
309
+ * @internal
310
+ */
311
+ static clearBackends() {
312
+ this.backends.clear();
313
+ }
314
+ /**
315
+ * Clear all registered setup factories.
316
+ * Intended for use in tests only.
317
+ * @internal
318
+ */
319
+ static clearSetups() {
320
+ this.setups.clear();
321
+ }
322
+ };
323
+ var STORAGE_DIR_NAME = path3__namespace.join(".vaultkeeper", "file");
324
+ var KEY_FILE = ".key";
325
+ var GCM_IV_BYTES = 12;
326
+ var GCM_KEY_BYTES = 32;
327
+ var GCM_TAG_LENGTH = 128;
328
+ function getStorageDir() {
329
+ return path3__namespace.join(os4__namespace.homedir(), STORAGE_DIR_NAME);
330
+ }
331
+ function getEntryPath(storageDir, id) {
332
+ const safeId = Buffer.from(id, "utf8").toString("hex");
333
+ return path3__namespace.join(storageDir, `${safeId}.enc`);
334
+ }
335
+ async function ensureStorageDir(storageDir) {
336
+ try {
337
+ await fs__namespace.mkdir(storageDir, { recursive: true, mode: 448 });
338
+ } catch (err) {
339
+ if (err instanceof Error && "code" in err && err.code !== "EEXIST") {
340
+ throw new FilesystemError(
341
+ `Failed to create storage directory: ${storageDir}`,
342
+ storageDir,
343
+ "rwx"
344
+ );
345
+ }
346
+ }
347
+ }
348
+ async function getOrCreateKey(storageDir) {
349
+ const keyPath = path3__namespace.join(storageDir, KEY_FILE);
350
+ try {
351
+ const data = await fs__namespace.readFile(keyPath);
352
+ return data;
353
+ } catch (err) {
354
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
355
+ const key = crypto__namespace.randomBytes(GCM_KEY_BYTES);
356
+ await fs__namespace.writeFile(keyPath, key, { mode: 384 });
357
+ return key;
358
+ }
359
+ throw err;
360
+ }
361
+ }
362
+ function encryptGcm(key, plaintext) {
363
+ const iv = crypto__namespace.randomBytes(GCM_IV_BYTES);
364
+ const cipher = crypto__namespace.createCipheriv("aes-256-gcm", key, iv, {
365
+ authTagLength: GCM_TAG_LENGTH / 8
366
+ });
367
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
368
+ const authTag = cipher.getAuthTag();
369
+ return [iv.toString("base64"), authTag.toString("base64"), encrypted.toString("base64")].join(":");
370
+ }
371
+ function decryptGcm(key, encoded) {
372
+ const parts = encoded.split(":");
373
+ if (parts.length !== 3) {
374
+ throw new Error("Invalid encrypted file format: expected iv:authTag:ciphertext");
375
+ }
376
+ const [ivB64, authTagB64, ciphertextB64] = parts;
377
+ if (ivB64 === void 0 || authTagB64 === void 0 || ciphertextB64 === void 0) {
378
+ throw new Error("Invalid encrypted file format: missing part");
379
+ }
380
+ const iv = Buffer.from(ivB64, "base64");
381
+ const authTag = Buffer.from(authTagB64, "base64");
382
+ const ciphertext = Buffer.from(ciphertextB64, "base64");
383
+ const decipher = crypto__namespace.createDecipheriv("aes-256-gcm", key, iv, {
384
+ authTagLength: GCM_TAG_LENGTH / 8
385
+ });
386
+ decipher.setAuthTag(authTag);
387
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
388
+ return decrypted.toString("utf8");
389
+ }
390
+ var FileBackend = class {
391
+ type = "file";
392
+ displayName = "Encrypted File Store";
393
+ async isAvailable() {
394
+ try {
395
+ const storageDir = getStorageDir();
396
+ await ensureStorageDir(storageDir);
397
+ return true;
398
+ } catch {
399
+ return false;
400
+ }
401
+ }
402
+ async store(id, secret) {
403
+ const storageDir = getStorageDir();
404
+ await ensureStorageDir(storageDir);
405
+ const key = await getOrCreateKey(storageDir);
406
+ const entryPath = getEntryPath(storageDir, id);
407
+ const encrypted = encryptGcm(key, secret);
408
+ await fs__namespace.writeFile(entryPath, encrypted, { mode: 384 });
409
+ }
410
+ async retrieve(id) {
411
+ const storageDir = getStorageDir();
412
+ const entryPath = getEntryPath(storageDir, id);
413
+ let encoded;
414
+ try {
415
+ encoded = await fs__namespace.readFile(entryPath, "utf8");
416
+ } catch (err) {
417
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
418
+ throw new SecretNotFoundError(`Secret not found in file store: ${id}`);
419
+ }
420
+ throw err;
421
+ }
422
+ const key = await getOrCreateKey(storageDir);
423
+ try {
424
+ return decryptGcm(key, encoded);
425
+ } catch (err) {
426
+ throw new Error(
427
+ `Failed to decrypt secret: ${err instanceof Error ? err.message : String(err)}`
428
+ );
429
+ }
430
+ }
431
+ async delete(id) {
432
+ const storageDir = getStorageDir();
433
+ const entryPath = getEntryPath(storageDir, id);
434
+ try {
435
+ await fs__namespace.unlink(entryPath);
436
+ } catch (err) {
437
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
438
+ throw new SecretNotFoundError(`Secret not found in file store: ${id}`);
439
+ }
440
+ throw err;
441
+ }
442
+ }
443
+ async exists(id) {
444
+ const storageDir = getStorageDir();
445
+ const entryPath = getEntryPath(storageDir, id);
446
+ try {
447
+ await fs__namespace.access(entryPath);
448
+ return true;
449
+ } catch {
450
+ return false;
451
+ }
452
+ }
453
+ async list() {
454
+ const storageDir = getStorageDir();
455
+ let entries;
456
+ try {
457
+ entries = await fs__namespace.readdir(storageDir);
458
+ } catch {
459
+ return [];
460
+ }
461
+ return entries.filter((f) => f.endsWith(".enc")).map((f) => Buffer.from(f.slice(0, -4), "hex").toString("utf8"));
462
+ }
267
463
  };
268
464
  async function execCommand(command, args, options) {
269
- const result = await execCommandFull(command, args);
465
+ const result = await execCommandFull(command, args, options);
270
466
  if (result.exitCode !== 0) {
271
467
  throw new Error(`Command failed with exit code ${String(result.exitCode)}: ${result.stderr}`);
272
468
  }
273
469
  return result.stdout.trim();
274
470
  }
275
471
  function execCommandFull(command, args, options) {
276
- return new Promise((resolve, reject) => {
472
+ return new Promise((resolve2, reject) => {
277
473
  const proc = child_process.spawn(command, args, {
278
- stdio: ["ignore", "pipe", "pipe"]
474
+ stdio: [options?.stdin !== void 0 ? "pipe" : "ignore", "pipe", "pipe"]
279
475
  });
280
476
  let stdout = "";
281
477
  let stderr = "";
@@ -285,25 +481,887 @@ function execCommandFull(command, args, options) {
285
481
  proc.stderr?.on("data", (data) => {
286
482
  stderr += data.toString();
287
483
  });
484
+ if (options?.stdin !== void 0 && proc.stdin) {
485
+ proc.stdin.write(options.stdin);
486
+ proc.stdin.end();
487
+ }
488
+ if (options?.timeoutMs !== void 0) {
489
+ setTimeout(() => {
490
+ proc.kill("SIGTERM");
491
+ reject(new Error(`Command timed out after ${String(options.timeoutMs)}ms`));
492
+ }, options.timeoutMs);
493
+ }
288
494
  proc.on("close", (code) => {
289
- resolve({ stdout, stderr, exitCode: code ?? 1 });
495
+ resolve2({ stdout, stderr, exitCode: code ?? 1 });
290
496
  });
291
497
  proc.on("error", (error) => {
292
498
  reject(error);
293
499
  });
294
500
  });
295
501
  }
296
- path5__namespace.join(".vaultkeeper", "file");
297
- path5__namespace.join(".vaultkeeper", "yubikey");
502
+
503
+ // src/backend/keychain-backend.ts
504
+ var ACCOUNT = "vaultkeeper";
505
+ var SERVICE_PREFIX = "vaultkeeper:";
506
+ var KeychainBackend = class {
507
+ type = "keychain";
508
+ displayName = "macOS Keychain";
509
+ async isAvailable() {
510
+ if (process.platform !== "darwin") {
511
+ return false;
512
+ }
513
+ try {
514
+ const result = await execCommandFull("security", ["version"]);
515
+ return result.exitCode === 0;
516
+ } catch {
517
+ return false;
518
+ }
519
+ }
520
+ async store(id, secret) {
521
+ const service = `${SERVICE_PREFIX}${id}`;
522
+ const encoded = Buffer.from(secret, "utf8").toString("base64");
523
+ await execCommandFull("security", [
524
+ "delete-generic-password",
525
+ "-a",
526
+ ACCOUNT,
527
+ "-s",
528
+ service
529
+ ]);
530
+ await execCommand("security", [
531
+ "add-generic-password",
532
+ "-a",
533
+ ACCOUNT,
534
+ "-s",
535
+ service,
536
+ "-w",
537
+ encoded
538
+ ]);
539
+ }
540
+ async retrieve(id) {
541
+ const service = `${SERVICE_PREFIX}${id}`;
542
+ const result = await execCommandFull("security", [
543
+ "find-generic-password",
544
+ "-a",
545
+ ACCOUNT,
546
+ "-s",
547
+ service,
548
+ "-w"
549
+ ]);
550
+ if (result.exitCode !== 0) {
551
+ throw new SecretNotFoundError(`Secret not found in macOS Keychain: ${id}`);
552
+ }
553
+ const encoded = result.stdout.trim();
554
+ return Buffer.from(encoded, "base64").toString("utf8");
555
+ }
556
+ async delete(id) {
557
+ const service = `${SERVICE_PREFIX}${id}`;
558
+ const result = await execCommandFull("security", [
559
+ "delete-generic-password",
560
+ "-a",
561
+ ACCOUNT,
562
+ "-s",
563
+ service
564
+ ]);
565
+ if (result.exitCode !== 0) {
566
+ throw new SecretNotFoundError(`Secret not found in macOS Keychain: ${id}`);
567
+ }
568
+ }
569
+ async exists(id) {
570
+ const service = `${SERVICE_PREFIX}${id}`;
571
+ const result = await execCommandFull("security", [
572
+ "find-generic-password",
573
+ "-a",
574
+ ACCOUNT,
575
+ "-s",
576
+ service
577
+ ]);
578
+ return result.exitCode === 0;
579
+ }
580
+ async list() {
581
+ const result = await execCommandFull("security", [
582
+ "dump-keychain"
583
+ ]);
584
+ if (result.exitCode !== 0) {
585
+ return [];
586
+ }
587
+ const ids = [];
588
+ const servicePattern = /0x00000007 <blob>="vaultkeeper:([^"]+)"/g;
589
+ let match = servicePattern.exec(result.stdout);
590
+ while (match !== null) {
591
+ const id = match[1];
592
+ if (id !== void 0) {
593
+ ids.push(id);
594
+ }
595
+ match = servicePattern.exec(result.stdout);
596
+ }
597
+ return ids;
598
+ }
599
+ };
600
+ function getStoragePath() {
601
+ return path3__namespace.join(os4__namespace.homedir(), ".vaultkeeper", "dpapi");
602
+ }
603
+ function getEntryPath2(storageDir, id) {
604
+ const safeId = Buffer.from(id, "utf8").toString("hex");
605
+ return path3__namespace.join(storageDir, `${safeId}.enc`);
606
+ }
607
+ var DpapiBackend = class {
608
+ type = "dpapi";
609
+ displayName = "Windows DPAPI";
610
+ async isAvailable() {
611
+ if (process.platform !== "win32") {
612
+ return false;
613
+ }
614
+ try {
615
+ const result = await execCommandFull("powershell", [
616
+ "-NoProfile",
617
+ "-Command",
618
+ "[System.Security.Cryptography.ProtectedData] | Out-Null; exit 0"
619
+ ]);
620
+ return result.exitCode === 0;
621
+ } catch {
622
+ return false;
623
+ }
624
+ }
625
+ async store(id, secret) {
626
+ const storageDir = getStoragePath();
627
+ await fs__namespace.mkdir(storageDir, { recursive: true });
628
+ const entryPath = getEntryPath2(storageDir, id);
629
+ const script = [
630
+ "Add-Type -AssemblyName System.Security",
631
+ `$bytes = [System.Text.Encoding]::UTF8.GetBytes(${JSON.stringify(secret)})`,
632
+ "$entropy = $null",
633
+ "$scope = [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
634
+ "$encrypted = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $entropy, $scope)",
635
+ `[System.IO.File]::WriteAllBytes(${JSON.stringify(entryPath)}, $encrypted)`
636
+ ].join("; ");
637
+ await execCommand("powershell", ["-NoProfile", "-Command", script]);
638
+ }
639
+ async retrieve(id) {
640
+ const storageDir = getStoragePath();
641
+ const entryPath = getEntryPath2(storageDir, id);
642
+ try {
643
+ await fs__namespace.access(entryPath);
644
+ } catch {
645
+ throw new SecretNotFoundError(`Secret not found in Windows DPAPI store: ${id}`);
646
+ }
647
+ const script = [
648
+ "Add-Type -AssemblyName System.Security",
649
+ `$encrypted = [System.IO.File]::ReadAllBytes(${JSON.stringify(entryPath)})`,
650
+ "$entropy = $null",
651
+ "$scope = [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
652
+ "$bytes = [System.Security.Cryptography.ProtectedData]::Unprotect($encrypted, $entropy, $scope)",
653
+ "Write-Output ([System.Text.Encoding]::UTF8.GetString($bytes))"
654
+ ].join("; ");
655
+ return execCommand("powershell", ["-NoProfile", "-Command", script]);
656
+ }
657
+ async delete(id) {
658
+ const storageDir = getStoragePath();
659
+ const entryPath = getEntryPath2(storageDir, id);
660
+ try {
661
+ await fs__namespace.unlink(entryPath);
662
+ } catch (err) {
663
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
664
+ throw new SecretNotFoundError(`Secret not found in Windows DPAPI store: ${id}`);
665
+ }
666
+ throw err;
667
+ }
668
+ }
669
+ async exists(id) {
670
+ const storageDir = getStoragePath();
671
+ const entryPath = getEntryPath2(storageDir, id);
672
+ try {
673
+ await fs__namespace.access(entryPath);
674
+ return true;
675
+ } catch {
676
+ return false;
677
+ }
678
+ }
679
+ async list() {
680
+ const storageDir = getStoragePath();
681
+ let entries;
682
+ try {
683
+ entries = await fs__namespace.readdir(storageDir);
684
+ } catch {
685
+ return [];
686
+ }
687
+ return entries.filter((f) => f.endsWith(".enc")).map((f) => Buffer.from(f.slice(0, -4), "hex").toString("utf8"));
688
+ }
689
+ };
690
+
691
+ // src/backend/secret-tool-backend.ts
692
+ var ATTRIBUTE_KEY = "vaultkeeper-id";
693
+ var LABEL_PREFIX = "vaultkeeper: ";
694
+ var SecretToolBackend = class {
695
+ type = "secret-tool";
696
+ displayName = "Linux Secret Service (secret-tool)";
697
+ async isAvailable() {
698
+ if (process.platform !== "linux") {
699
+ return false;
700
+ }
701
+ try {
702
+ const result = await execCommandFull("secret-tool", ["--version"]);
703
+ return result.exitCode === 0;
704
+ } catch {
705
+ return false;
706
+ }
707
+ }
708
+ async store(id, secret) {
709
+ const label = `${LABEL_PREFIX}${id}`;
710
+ await execCommand(
711
+ "secret-tool",
712
+ ["store", "--label", label, ATTRIBUTE_KEY, id],
713
+ { stdin: secret }
714
+ );
715
+ }
716
+ async retrieve(id) {
717
+ const result = await execCommandFull("secret-tool", ["lookup", ATTRIBUTE_KEY, id]);
718
+ if (result.exitCode !== 0 || result.stdout.trim() === "") {
719
+ throw new SecretNotFoundError(`Secret not found in Secret Service: ${id}`);
720
+ }
721
+ return result.stdout.trim();
722
+ }
723
+ async delete(id) {
724
+ const result = await execCommandFull("secret-tool", ["clear", ATTRIBUTE_KEY, id]);
725
+ if (result.exitCode !== 0) {
726
+ throw new SecretNotFoundError(`Secret not found in Secret Service: ${id}`);
727
+ }
728
+ }
729
+ async exists(id) {
730
+ const result = await execCommandFull("secret-tool", ["lookup", ATTRIBUTE_KEY, id]);
731
+ return result.exitCode === 0 && result.stdout.trim() !== "";
732
+ }
733
+ async list() {
734
+ const result = await execCommandFull("secret-tool", [
735
+ "search",
736
+ ATTRIBUTE_KEY,
737
+ ""
738
+ ]);
739
+ if (result.exitCode !== 0) {
740
+ return [];
741
+ }
742
+ const ids = [];
743
+ const attrPattern = new RegExp(`attribute\\.${ATTRIBUTE_KEY} = (.+)`, "g");
744
+ let match = attrPattern.exec(result.stdout);
745
+ while (match !== null) {
746
+ const id = match[1];
747
+ if (id !== void 0) {
748
+ ids.push(id);
749
+ }
750
+ match = attrPattern.exec(result.stdout);
751
+ }
752
+ return ids;
753
+ }
754
+ };
755
+ var INTEGRATION_NAME = "vaultkeeper";
756
+ var cachedVersion;
757
+ function getIntegrationVersion() {
758
+ if (cachedVersion !== void 0) return cachedVersion;
759
+ const dir = path3.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
760
+ const candidates = [
761
+ path3.resolve(dir, "..", "..", "package.json"),
762
+ path3.resolve(dir, "..", "package.json")
763
+ ];
764
+ for (const candidate of candidates) {
765
+ if (!fs4.existsSync(candidate)) continue;
766
+ const raw = JSON.parse(fs4.readFileSync(candidate, "utf8"));
767
+ if (raw !== null && typeof raw === "object" && "version" in raw && typeof raw.version === "string") {
768
+ cachedVersion = raw.version;
769
+ return cachedVersion;
770
+ }
771
+ }
772
+ throw new Error(
773
+ `Could not read version from vaultkeeper package.json. Tried paths: ${candidates.join(", ")}`
774
+ );
775
+ }
776
+
777
+ // src/backend/one-password-backend.ts
778
+ var SDK_INSTALL_URL = "https://developer.1password.com/docs/sdks/";
779
+ var TAG = "vaultkeeper";
780
+ var PASSWORD_FIELD_TITLE = "password";
781
+ var SESSION_TIMEOUT_MS = 3e4;
782
+ function isWorkerSuccess(res) {
783
+ return "value" in res;
784
+ }
785
+ function isWorkerResponse(value) {
786
+ if (value === null || typeof value !== "object") return false;
787
+ if ("value" in value && typeof value.value === "string") return true;
788
+ if ("error" in value && typeof value.error === "string" && "code" in value && typeof value.code === "string")
789
+ return true;
790
+ return false;
791
+ }
792
+ var OnePasswordBackend = class {
793
+ type = "1password";
794
+ displayName = "1Password";
795
+ vaultId;
796
+ account;
797
+ serviceAccountToken;
798
+ accessMode;
799
+ sessionTimeoutMs;
800
+ /** In-flight or resolved client promise — prevents duplicate createClient calls. */
801
+ clientPromise;
802
+ constructor(options) {
803
+ if (options.accessMode === "per-access" && options.serviceAccountToken !== void 0) {
804
+ throw new Error(
805
+ "per-access mode requires desktop biometric authentication and cannot be used with a service account token"
806
+ );
807
+ }
808
+ if (options.account !== void 0 && options.serviceAccountToken !== void 0) {
809
+ throw new Error(
810
+ "account and serviceAccountToken are mutually exclusive \u2014 provide one or the other, not both"
811
+ );
812
+ }
813
+ this.vaultId = options.vault;
814
+ this.sessionTimeoutMs = options.sessionTimeoutMs ?? SESSION_TIMEOUT_MS;
815
+ if (options.account !== void 0) {
816
+ this.account = options.account;
817
+ }
818
+ if (options.serviceAccountToken !== void 0) {
819
+ this.serviceAccountToken = options.serviceAccountToken;
820
+ }
821
+ this.accessMode = options.accessMode ?? "session";
822
+ }
823
+ async isAvailable() {
824
+ const sdk = await this.tryLoadSdk();
825
+ return sdk !== null;
826
+ }
827
+ // ---- Session client management ----
828
+ /**
829
+ * Dynamically import the SDK. Returns `null` if the SDK is not installed or
830
+ * the native library cannot be loaded.
831
+ */
832
+ async tryLoadSdk() {
833
+ try {
834
+ const sdk = await import('@1password/sdk');
835
+ return sdk;
836
+ } catch {
837
+ return null;
838
+ }
839
+ }
840
+ /**
841
+ * Acquire (or create) a cached SDK client.
842
+ * Wraps `createClient` with a configurable timeout (default 30 s) to handle
843
+ * the known beta SDK hang after session expiry.
844
+ */
845
+ acquireClient() {
846
+ this.clientPromise ??= this.createClientInternal().catch((err) => {
847
+ this.clientPromise = void 0;
848
+ throw err;
849
+ });
850
+ return this.clientPromise;
851
+ }
852
+ async createClientInternal() {
853
+ const sdk = await this.tryLoadSdk();
854
+ if (sdk === null) {
855
+ throw new PluginNotFoundError(
856
+ "1Password SDK (@1password/sdk) is not available. Install it to use this backend.",
857
+ "@1password/sdk",
858
+ SDK_INSTALL_URL
859
+ );
860
+ }
861
+ const auth = this.buildAuth(sdk);
862
+ let timerId;
863
+ const timeoutPromise = new Promise((_resolve, reject) => {
864
+ timerId = setTimeout(() => {
865
+ reject(new BackendLockedError("1Password session timed out waiting for authentication", true));
866
+ }, this.sessionTimeoutMs);
867
+ });
868
+ try {
869
+ const client = await Promise.race([
870
+ sdk.createClient({
871
+ auth,
872
+ integrationName: INTEGRATION_NAME,
873
+ integrationVersion: getIntegrationVersion()
874
+ }),
875
+ timeoutPromise
876
+ ]);
877
+ return client;
878
+ } catch (err) {
879
+ if (err instanceof BackendLockedError) {
880
+ throw err;
881
+ }
882
+ if (err instanceof sdk.DesktopSessionExpiredError) {
883
+ throw new BackendLockedError(
884
+ "1Password session has expired. Please unlock the app.",
885
+ true
886
+ );
887
+ }
888
+ throw new AuthorizationDeniedError(
889
+ `1Password authentication failed: ${String(err)}`
890
+ );
891
+ } finally {
892
+ if (timerId !== void 0) {
893
+ clearTimeout(timerId);
894
+ }
895
+ }
896
+ }
897
+ buildAuth(sdk) {
898
+ if (this.serviceAccountToken !== void 0) {
899
+ return this.serviceAccountToken;
900
+ }
901
+ const accountName = this.account ?? "";
902
+ return new sdk.DesktopAuth(accountName);
903
+ }
904
+ // ---- Helpers for item lookup by title ----
905
+ /**
906
+ * List all items in the vault tagged "vaultkeeper" and find one with the
907
+ * matching title (= secret ID). Returns `undefined` if not found.
908
+ */
909
+ async findItemOverview(client, id) {
910
+ const overviews = await client.items.list(this.vaultId);
911
+ for (const overview of overviews) {
912
+ if (overview.title === id && overview.tags.includes(TAG)) {
913
+ return overview;
914
+ }
915
+ }
916
+ return void 0;
917
+ }
918
+ /**
919
+ * Fetch the full item for a given secret id. Returns `undefined` if not found.
920
+ */
921
+ async findItem(client, id) {
922
+ const overview = await this.findItemOverview(client, id);
923
+ if (overview === void 0) return void 0;
924
+ return client.items.get(this.vaultId, overview.id);
925
+ }
926
+ /**
927
+ * Extract the concealed password field value from an item.
928
+ */
929
+ extractSecret(item, id) {
930
+ for (const field of item.fields) {
931
+ if (field.title === PASSWORD_FIELD_TITLE) {
932
+ return field.value;
933
+ }
934
+ }
935
+ throw new SecretNotFoundError(
936
+ `Secret found in 1Password but missing password field: ${id}`
937
+ );
938
+ }
939
+ // ---- SecretBackend / ListableBackend implementation ----
940
+ async store(id, secret) {
941
+ const { ItemCategory, ItemFieldType } = await this.requireSdk();
942
+ const client = await this.acquireClient();
943
+ const existing = await this.findItem(client, id);
944
+ if (existing !== void 0) {
945
+ const hasPasswordField = existing.fields.some((f) => f.title === PASSWORD_FIELD_TITLE);
946
+ const updatedFields = hasPasswordField ? existing.fields.map((f) => {
947
+ if (f.title === PASSWORD_FIELD_TITLE) {
948
+ return { ...f, value: secret };
949
+ }
950
+ return f;
951
+ }) : [
952
+ ...existing.fields,
953
+ {
954
+ id: "password",
955
+ title: PASSWORD_FIELD_TITLE,
956
+ fieldType: ItemFieldType.Concealed,
957
+ value: secret
958
+ }
959
+ ];
960
+ await client.items.put({ ...existing, fields: updatedFields });
961
+ } else {
962
+ await client.items.create({
963
+ category: ItemCategory.Password,
964
+ vaultId: this.vaultId,
965
+ title: id,
966
+ tags: [TAG],
967
+ fields: [
968
+ {
969
+ id: "password",
970
+ title: PASSWORD_FIELD_TITLE,
971
+ fieldType: ItemFieldType.Concealed,
972
+ value: secret
973
+ }
974
+ ]
975
+ });
976
+ }
977
+ }
978
+ async retrieve(id) {
979
+ if (this.accessMode === "per-access") {
980
+ return this.retrieveViaWorker(id);
981
+ }
982
+ return this.retrieveViaSession(id);
983
+ }
984
+ async retrieveViaSession(id) {
985
+ const client = await this.acquireClient();
986
+ const item = await this.findItem(client, id);
987
+ if (item === void 0) {
988
+ throw new SecretNotFoundError(`Secret not found in 1Password: ${id}`);
989
+ }
990
+ return this.extractSecret(item, id);
991
+ }
992
+ /**
993
+ * Spawn the per-access worker script that triggers a fresh biometric prompt
994
+ * for each retrieval, then returns the secret from its stdout.
995
+ */
996
+ retrieveViaWorker(id) {
997
+ return new Promise((resolve2, reject) => {
998
+ const workerPath = path3.join(
999
+ path3.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))),
1000
+ "one-password-worker.js"
1001
+ );
1002
+ const accountArg = this.account ?? "";
1003
+ const child = child_process.spawn(
1004
+ process.execPath,
1005
+ [workerPath, accountArg, this.vaultId, id],
1006
+ { stdio: ["ignore", "pipe", "pipe"] }
1007
+ );
1008
+ const stdoutChunks = [];
1009
+ const stderrChunks = [];
1010
+ child.stdout.on("data", (chunk) => {
1011
+ stdoutChunks.push(chunk);
1012
+ });
1013
+ child.stderr.on("data", (chunk) => {
1014
+ stderrChunks.push(chunk);
1015
+ });
1016
+ child.on("close", (code) => {
1017
+ const raw = Buffer.concat(stdoutChunks).toString("utf8").trim();
1018
+ if (raw === "") {
1019
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
1020
+ const detail = stderr !== "" ? stderr : `exit code ${String(code)}`;
1021
+ reject(new Error(`1Password per-access worker crashed for secret ${id}: ${detail}`));
1022
+ return;
1023
+ }
1024
+ let parsed;
1025
+ try {
1026
+ parsed = JSON.parse(raw);
1027
+ } catch {
1028
+ reject(new SecretNotFoundError(`Worker returned unparseable output for secret: ${id}`));
1029
+ return;
1030
+ }
1031
+ if (!isWorkerResponse(parsed)) {
1032
+ reject(new SecretNotFoundError(`Worker returned unexpected response shape for secret: ${id}`));
1033
+ return;
1034
+ }
1035
+ if (isWorkerSuccess(parsed)) {
1036
+ resolve2(parsed.value);
1037
+ } else {
1038
+ switch (parsed.code) {
1039
+ case "NOT_FOUND":
1040
+ reject(new SecretNotFoundError(`Secret not found in 1Password: ${id}`));
1041
+ break;
1042
+ case "AUTH_DENIED":
1043
+ reject(new AuthorizationDeniedError("1Password authentication was denied"));
1044
+ break;
1045
+ case "LOCKED":
1046
+ reject(new BackendLockedError("1Password is locked. Please unlock and retry.", true));
1047
+ break;
1048
+ default:
1049
+ reject(new SecretNotFoundError(`Worker failed for secret ${id}: ${parsed.error}`));
1050
+ }
1051
+ }
1052
+ });
1053
+ child.on("error", (err) => {
1054
+ reject(new Error(
1055
+ `Failed to spawn 1Password per-access worker at ${workerPath}: ${String(err)}`
1056
+ ));
1057
+ });
1058
+ });
1059
+ }
1060
+ async delete(id) {
1061
+ const client = await this.acquireClient();
1062
+ const overview = await this.findItemOverview(client, id);
1063
+ if (overview === void 0) {
1064
+ throw new SecretNotFoundError(`Secret not found in 1Password: ${id}`);
1065
+ }
1066
+ await client.items.delete(this.vaultId, overview.id);
1067
+ }
1068
+ async exists(id) {
1069
+ const client = await this.acquireClient();
1070
+ const overview = await this.findItemOverview(client, id);
1071
+ return overview !== void 0;
1072
+ }
1073
+ async list() {
1074
+ const client = await this.acquireClient();
1075
+ const overviews = await client.items.list(this.vaultId);
1076
+ const ids = [];
1077
+ for (const overview of overviews) {
1078
+ if (overview.tags.includes(TAG)) {
1079
+ ids.push(overview.title);
1080
+ }
1081
+ }
1082
+ return ids;
1083
+ }
1084
+ // ---- Private helpers ----
1085
+ /** Load SDK and throw PluginNotFoundError if unavailable. */
1086
+ async requireSdk() {
1087
+ const sdk = await this.tryLoadSdk();
1088
+ if (sdk === null) {
1089
+ throw new PluginNotFoundError(
1090
+ "1Password SDK (@1password/sdk) is not available.",
1091
+ "@1password/sdk",
1092
+ SDK_INSTALL_URL
1093
+ );
1094
+ }
1095
+ return sdk;
1096
+ }
1097
+ };
1098
+ var YKMAN_INSTALL_URL = "https://developers.yubico.com/yubikey-manager/";
1099
+ var STORAGE_DIR_NAME2 = path3__namespace.join(".vaultkeeper", "yubikey");
1100
+ var METADATA_FILE = "metadata.json";
1101
+ var DEVICE_TIMEOUT_MS = 5e3;
1102
+ var GCM_IV_BYTES2 = 12;
1103
+ var GCM_KEY_BYTES2 = 32;
1104
+ var GCM_TAG_LENGTH_BITS = 128;
1105
+ var FORMAT_VERSION = "1";
1106
+ function getStorageDir2() {
1107
+ return path3__namespace.join(os4__namespace.homedir(), STORAGE_DIR_NAME2);
1108
+ }
1109
+ function getEntryPath3(storageDir, id) {
1110
+ const safeId = Buffer.from(id, "utf8").toString("hex");
1111
+ return path3__namespace.join(storageDir, `${safeId}.enc`);
1112
+ }
1113
+ function isStringRecord(value) {
1114
+ if (value === null || typeof value !== "object") {
1115
+ return false;
1116
+ }
1117
+ return Object.values(value).every((v) => typeof v === "string");
1118
+ }
1119
+ async function loadMetadata(storageDir) {
1120
+ const metaPath = path3__namespace.join(storageDir, METADATA_FILE);
1121
+ try {
1122
+ const raw = await fs__namespace.readFile(metaPath, "utf8");
1123
+ const parsed = JSON.parse(raw);
1124
+ if (parsed !== null && typeof parsed === "object" && "entries" in parsed && isStringRecord(parsed.entries)) {
1125
+ return { entries: parsed.entries };
1126
+ }
1127
+ return { entries: {} };
1128
+ } catch {
1129
+ return { entries: {} };
1130
+ }
1131
+ }
1132
+ async function saveMetadata(storageDir, metadata) {
1133
+ const metaPath = path3__namespace.join(storageDir, METADATA_FILE);
1134
+ await fs__namespace.writeFile(metaPath, JSON.stringify(metadata, null, 2), { mode: 384 });
1135
+ }
1136
+ var HMAC_RESPONSE_HEX_LENGTH = 40;
1137
+ var HMAC_RESPONSE_RE = /^[0-9a-fA-F]{40}$/;
1138
+ function deriveKey(hmacResponse, id) {
1139
+ const trimmed = hmacResponse.trim();
1140
+ if (!HMAC_RESPONSE_RE.test(trimmed)) {
1141
+ throw new Error(
1142
+ `Invalid YubiKey HMAC response: expected exactly ${String(HMAC_RESPONSE_HEX_LENGTH)} hex characters (20 bytes), got ${String(trimmed.length)} characters`
1143
+ );
1144
+ }
1145
+ const ikm = Buffer.from(trimmed, "hex");
1146
+ const info = Buffer.from(`vaultkeeper-yubikey:${id}`, "utf8");
1147
+ const keyMaterial = crypto__namespace.hkdfSync("sha256", ikm, Buffer.alloc(0), info, GCM_KEY_BYTES2);
1148
+ return Buffer.from(keyMaterial);
1149
+ }
1150
+ function encryptGcm2(key, plaintext) {
1151
+ const iv = crypto__namespace.randomBytes(GCM_IV_BYTES2);
1152
+ let encrypted;
1153
+ let authTag;
1154
+ try {
1155
+ const cipher = crypto__namespace.createCipheriv("aes-256-gcm", key, iv, {
1156
+ authTagLength: GCM_TAG_LENGTH_BITS / 8
1157
+ });
1158
+ encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
1159
+ authTag = cipher.getAuthTag();
1160
+ return [
1161
+ FORMAT_VERSION,
1162
+ iv.toString("base64"),
1163
+ authTag.toString("base64"),
1164
+ encrypted.toString("base64")
1165
+ ].join(":");
1166
+ } finally {
1167
+ key.fill(0);
1168
+ iv.fill(0);
1169
+ encrypted?.fill(0);
1170
+ authTag?.fill(0);
1171
+ }
1172
+ }
1173
+ function decryptGcm2(key, encoded) {
1174
+ const parts = encoded.split(":");
1175
+ const versionSegment = parts[0] ?? "";
1176
+ const parsedVersion = parseInt(versionSegment, 10);
1177
+ const isNumericVersion = String(parsedVersion) === versionSegment && !Number.isNaN(parsedVersion);
1178
+ if (!isNumericVersion) {
1179
+ key.fill(0);
1180
+ throw new Error(
1181
+ "Encrypted file uses a legacy format (AES-256-CBC). Delete the secret and re-store it to migrate to AES-256-GCM."
1182
+ );
1183
+ }
1184
+ if (versionSegment !== FORMAT_VERSION) {
1185
+ key.fill(0);
1186
+ throw new Error(
1187
+ `Unsupported encrypted file version: ${versionSegment}. This vaultkeeper build only supports version ${FORMAT_VERSION}. Upgrade vaultkeeper to read this secret.`
1188
+ );
1189
+ }
1190
+ if (parts.length !== 4) {
1191
+ key.fill(0);
1192
+ throw new Error(
1193
+ `Invalid encrypted file format: expected ${FORMAT_VERSION}:iv:authTag:ciphertext`
1194
+ );
1195
+ }
1196
+ const [_version, ivB64, authTagB64, ciphertextB64] = parts;
1197
+ if (ivB64 === void 0 || authTagB64 === void 0 || ciphertextB64 === void 0) {
1198
+ key.fill(0);
1199
+ throw new Error("Invalid encrypted file format: missing part");
1200
+ }
1201
+ let decrypted;
1202
+ try {
1203
+ const iv = Buffer.from(ivB64, "base64");
1204
+ const authTag = Buffer.from(authTagB64, "base64");
1205
+ const ciphertext = Buffer.from(ciphertextB64, "base64");
1206
+ const decipher = crypto__namespace.createDecipheriv("aes-256-gcm", key, iv, {
1207
+ authTagLength: GCM_TAG_LENGTH_BITS / 8
1208
+ });
1209
+ decipher.setAuthTag(authTag);
1210
+ try {
1211
+ decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
1212
+ } catch (err) {
1213
+ throw new Error(
1214
+ `GCM authentication failed \u2014 ciphertext may be tampered: ${err instanceof Error ? err.message : String(err)}`
1215
+ );
1216
+ }
1217
+ const plaintext = decrypted.toString("utf8");
1218
+ return plaintext;
1219
+ } finally {
1220
+ key.fill(0);
1221
+ decrypted?.fill(0);
1222
+ }
1223
+ }
1224
+ var YubikeyBackend = class {
1225
+ type = "yubikey";
1226
+ displayName = "YubiKey";
1227
+ async isAvailable() {
1228
+ try {
1229
+ const result = await execCommandFull("ykman", ["--version"]);
1230
+ if (result.exitCode !== 0) {
1231
+ return false;
1232
+ }
1233
+ const listResult = await execCommandFull("ykman", ["list"]);
1234
+ return listResult.exitCode === 0 && listResult.stdout.trim() !== "";
1235
+ } catch {
1236
+ return false;
1237
+ }
1238
+ }
1239
+ async requireDevice() {
1240
+ const available = await this.isAvailable();
1241
+ if (!available) {
1242
+ const hasYkman = await execCommandFull("ykman", ["--version"]).then(
1243
+ (r) => r.exitCode === 0,
1244
+ () => false
1245
+ );
1246
+ if (!hasYkman) {
1247
+ throw new PluginNotFoundError("ykman is not installed", "ykman", YKMAN_INSTALL_URL);
1248
+ }
1249
+ throw new DeviceNotPresentError("No YubiKey device detected", DEVICE_TIMEOUT_MS);
1250
+ }
1251
+ }
1252
+ /**
1253
+ * Perform the YubiKey HMAC-SHA1 challenge-response for `id` and return the
1254
+ * raw hex response string. Throws on device failure.
1255
+ */
1256
+ async challengeResponse(id) {
1257
+ const challenge = Buffer.from(`vaultkeeper:${id}`, "utf8").toString("hex");
1258
+ const responseResult = await execCommandFull("ykman", ["otp", "calculate", "2", challenge]);
1259
+ if (responseResult.exitCode !== 0) {
1260
+ throw new Error(`YubiKey challenge-response failed: ${responseResult.stderr}`);
1261
+ }
1262
+ return responseResult.stdout.trim();
1263
+ }
1264
+ async store(id, secret) {
1265
+ await this.requireDevice();
1266
+ const storageDir = getStorageDir2();
1267
+ await fs__namespace.mkdir(storageDir, { recursive: true, mode: 448 });
1268
+ const hmacResponse = await this.challengeResponse(id);
1269
+ const key = deriveKey(hmacResponse, id);
1270
+ const encrypted = encryptGcm2(key, secret);
1271
+ const entryPath = getEntryPath3(storageDir, id);
1272
+ await fs__namespace.writeFile(entryPath, encrypted, { mode: 384 });
1273
+ const metadata = await loadMetadata(storageDir);
1274
+ metadata.entries[id] = entryPath;
1275
+ await saveMetadata(storageDir, metadata);
1276
+ }
1277
+ async retrieve(id) {
1278
+ await this.requireDevice();
1279
+ const storageDir = getStorageDir2();
1280
+ const entryPath = getEntryPath3(storageDir, id);
1281
+ try {
1282
+ await fs__namespace.access(entryPath);
1283
+ } catch {
1284
+ throw new SecretNotFoundError(`Secret not found in YubiKey store: ${id}`);
1285
+ }
1286
+ const encoded = await fs__namespace.readFile(entryPath, "utf8");
1287
+ const hmacResponse = await this.challengeResponse(id);
1288
+ const key = deriveKey(hmacResponse, id);
1289
+ return decryptGcm2(key, encoded);
1290
+ }
1291
+ async delete(id) {
1292
+ await this.requireDevice();
1293
+ const storageDir = getStorageDir2();
1294
+ const entryPath = getEntryPath3(storageDir, id);
1295
+ try {
1296
+ await fs__namespace.unlink(entryPath);
1297
+ } catch (err) {
1298
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
1299
+ throw new SecretNotFoundError(`Secret not found in YubiKey store: ${id}`);
1300
+ }
1301
+ throw err;
1302
+ }
1303
+ const metadata = await loadMetadata(storageDir);
1304
+ delete metadata.entries[id];
1305
+ await saveMetadata(storageDir, metadata);
1306
+ }
1307
+ async exists(id) {
1308
+ const storageDir = getStorageDir2();
1309
+ const entryPath = getEntryPath3(storageDir, id);
1310
+ try {
1311
+ await fs__namespace.access(entryPath);
1312
+ return true;
1313
+ } catch {
1314
+ return false;
1315
+ }
1316
+ }
1317
+ async list() {
1318
+ const storageDir = getStorageDir2();
1319
+ const metadata = await loadMetadata(storageDir);
1320
+ return Object.keys(metadata.entries);
1321
+ }
1322
+ };
1323
+
1324
+ // src/backend/register-builtins.ts
1325
+ function registerBuiltinBackends() {
1326
+ BackendRegistry.register("file", () => new FileBackend());
1327
+ BackendRegistry.register("keychain", () => new KeychainBackend());
1328
+ BackendRegistry.register("dpapi", () => new DpapiBackend());
1329
+ BackendRegistry.register("secret-tool", () => new SecretToolBackend());
1330
+ BackendRegistry.register("1password", (config) => {
1331
+ const opts = config?.options;
1332
+ const vaultId = opts?.vault ?? "";
1333
+ const opOptions = {
1334
+ vault: vaultId,
1335
+ // 'session' is the safer default — 'per-access' re-prompts on every read.
1336
+ accessMode: opts?.accessMode === "per-access" ? "per-access" : "session"
1337
+ };
1338
+ const account = opts?.account;
1339
+ if (account !== void 0) {
1340
+ opOptions.account = account;
1341
+ }
1342
+ const serviceAccountToken = opts?.serviceAccountToken;
1343
+ if (serviceAccountToken !== void 0) {
1344
+ opOptions.serviceAccountToken = serviceAccountToken;
1345
+ }
1346
+ return new OnePasswordBackend(opOptions);
1347
+ });
1348
+ BackendRegistry.register("yubikey", () => new YubikeyBackend());
1349
+ }
1350
+ registerBuiltinBackends();
1351
+
1352
+ // src/backend/types.ts
1353
+ function isListableBackend(backend) {
1354
+ return "list" in backend && typeof backend.list === "function";
1355
+ }
298
1356
  function hashExecutable(filePath) {
299
- return new Promise((resolve, reject) => {
300
- const hash = crypto4__namespace.createHash("sha256");
1357
+ return new Promise((resolve2, reject) => {
1358
+ const hash = crypto__namespace.createHash("sha256");
301
1359
  const stream = fs4__namespace.createReadStream(filePath);
302
1360
  stream.on("data", (chunk) => {
303
1361
  hash.update(chunk);
304
1362
  });
305
1363
  stream.on("end", () => {
306
- resolve(hash.digest("hex"));
1364
+ resolve2(hash.digest("hex"));
307
1365
  });
308
1366
  stream.on("error", (err) => {
309
1367
  reject(err);
@@ -326,10 +1384,10 @@ function isTrustManifestEntry(value) {
326
1384
  return true;
327
1385
  }
328
1386
  async function loadManifest(configDir) {
329
- const manifestPath = path5__namespace.join(configDir, MANIFEST_FILENAME);
1387
+ const manifestPath = path3__namespace.join(configDir, MANIFEST_FILENAME);
330
1388
  let rawText;
331
1389
  try {
332
- rawText = await fs5__namespace.readFile(manifestPath, "utf8");
1390
+ rawText = await fs__namespace.readFile(manifestPath, "utf8");
333
1391
  } catch (err) {
334
1392
  if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
335
1393
  return /* @__PURE__ */ new Map();
@@ -349,14 +1407,14 @@ async function loadManifest(configDir) {
349
1407
  return manifest;
350
1408
  }
351
1409
  async function saveManifest(configDir, manifest) {
352
- await fs5__namespace.mkdir(configDir, { recursive: true });
1410
+ await fs__namespace.mkdir(configDir, { recursive: true });
353
1411
  const entries = {};
354
1412
  for (const [namespace, entry] of manifest) {
355
1413
  entries[namespace] = entry;
356
1414
  }
357
1415
  const raw = { version: 1, entries };
358
- const manifestPath = path5__namespace.join(configDir, MANIFEST_FILENAME);
359
- await fs5__namespace.writeFile(manifestPath, JSON.stringify(raw, null, 2), "utf8");
1416
+ const manifestPath = path3__namespace.join(configDir, MANIFEST_FILENAME);
1417
+ await fs__namespace.writeFile(manifestPath, JSON.stringify(raw, null, 2), "utf8");
360
1418
  }
361
1419
  function addTrustedHash(manifest, namespace, hash) {
362
1420
  const next = new Map(manifest);
@@ -467,14 +1525,18 @@ function validateCapabilityToken(token) {
467
1525
  return claims;
468
1526
  }
469
1527
  function getDefaultConfigDir() {
1528
+ const envOverride = process.env.VAULTKEEPER_CONFIG_DIR;
1529
+ if (envOverride !== void 0 && envOverride !== "") {
1530
+ return envOverride;
1531
+ }
470
1532
  if (process.platform === "win32") {
471
1533
  const appData = process.env.APPDATA;
472
1534
  if (appData !== void 0) {
473
- return path5__namespace.join(appData, "vaultkeeper");
1535
+ return path3__namespace.join(appData, "vaultkeeper");
474
1536
  }
475
- return path5__namespace.join(os4__namespace.homedir(), "AppData", "Roaming", "vaultkeeper");
1537
+ return path3__namespace.join(os4__namespace.homedir(), "AppData", "Roaming", "vaultkeeper");
476
1538
  }
477
- return path5__namespace.join(os4__namespace.homedir(), ".config", "vaultkeeper");
1539
+ return path3__namespace.join(os4__namespace.homedir(), ".config", "vaultkeeper");
478
1540
  }
479
1541
  function defaultConfig() {
480
1542
  return {
@@ -513,6 +1575,21 @@ function validateBackendEntry(entry, index) {
513
1575
  }
514
1576
  result.path = entry.path;
515
1577
  }
1578
+ if (entry.options !== void 0) {
1579
+ if (!isObject(entry.options)) {
1580
+ throw new Error(`backends[${String(index)}].options must be an object`);
1581
+ }
1582
+ const opts = {};
1583
+ for (const [k, v] of Object.entries(entry.options)) {
1584
+ if (typeof v !== "string") {
1585
+ throw new Error(
1586
+ `backends[${String(index)}].options["${k}"] must be a string`
1587
+ );
1588
+ }
1589
+ opts[k] = v;
1590
+ }
1591
+ result.options = opts;
1592
+ }
516
1593
  return result;
517
1594
  }
518
1595
  function validateConfig(config) {
@@ -575,10 +1652,10 @@ function validateConfig(config) {
575
1652
  }
576
1653
  async function loadConfig(configDir) {
577
1654
  const dir = configDir ?? getDefaultConfigDir();
578
- const configPath = path5__namespace.join(dir, "config.json");
1655
+ const configPath = path3__namespace.join(dir, "config.json");
579
1656
  let raw;
580
1657
  try {
581
- raw = await fs5__namespace.readFile(configPath, "utf-8");
1658
+ raw = await fs__namespace.readFile(configPath, "utf-8");
582
1659
  } catch {
583
1660
  return defaultConfig();
584
1661
  }
@@ -597,10 +1674,10 @@ var KeyManager = class {
597
1674
  #rotating = false;
598
1675
  /** Generate a new 32-byte key with a timestamp-based id. */
599
1676
  generateKey() {
600
- const randomSuffix = crypto4__namespace.randomBytes(4).toString("hex");
1677
+ const randomSuffix = crypto__namespace.randomBytes(4).toString("hex");
601
1678
  return {
602
1679
  id: `k-${String(Date.now())}-${randomSuffix}`,
603
- key: new Uint8Array(crypto4__namespace.randomBytes(32)),
1680
+ key: new Uint8Array(crypto__namespace.randomBytes(32)),
604
1681
  createdAt: /* @__PURE__ */ new Date()
605
1682
  };
606
1683
  }
@@ -902,7 +1979,7 @@ function replaceInRecord2(record, secret) {
902
1979
  function delegatedExec(secret, request) {
903
1980
  const args = (request.args ?? []).map((arg) => replacePlaceholder2(arg, secret));
904
1981
  const env = request.env !== void 0 ? replaceInRecord2(request.env, secret) : void 0;
905
- return new Promise((resolve, reject) => {
1982
+ return new Promise((resolve2, reject) => {
906
1983
  const spawnOptions = {
907
1984
  stdio: ["ignore", "pipe", "pipe"]
908
1985
  };
@@ -922,7 +1999,7 @@ function delegatedExec(secret, request) {
922
1999
  stderr += data.toString();
923
2000
  });
924
2001
  proc.on("close", (code) => {
925
- resolve({ stdout, stderr, exitCode: code ?? 1 });
2002
+ resolve2({ stdout, stderr, exitCode: code ?? 1 });
926
2003
  });
927
2004
  proc.on("error", (error) => {
928
2005
  reject(error);
@@ -1016,6 +2093,55 @@ function createSecretAccessor(secretValue) {
1016
2093
  return proxy;
1017
2094
  }
1018
2095
 
2096
+ // src/access/sign-util.ts
2097
+ var ALLOWED_ALGORITHMS = /* @__PURE__ */ new Set(["sha256", "sha384", "sha512"]);
2098
+ function resolveAlgorithmForKey(key, override) {
2099
+ const keyType = key.asymmetricKeyType;
2100
+ if (keyType === "ed25519" || keyType === "ed448") {
2101
+ return { signAlg: null, label: keyType };
2102
+ }
2103
+ const alg = (override ?? "sha256").toLowerCase();
2104
+ if (!ALLOWED_ALGORITHMS.has(alg)) {
2105
+ throw new InvalidAlgorithmError(
2106
+ `Unsupported algorithm '${alg}'. Allowed: ${[...ALLOWED_ALGORITHMS].join(", ")}`,
2107
+ alg,
2108
+ [...ALLOWED_ALGORITHMS]
2109
+ );
2110
+ }
2111
+ return { signAlg: alg, label: alg };
2112
+ }
2113
+
2114
+ // src/access/delegated-sign.ts
2115
+ function delegatedSign(secretPem, request) {
2116
+ const key = crypto__namespace.createPrivateKey(secretPem);
2117
+ const { signAlg, label } = resolveAlgorithmForKey(key, request.algorithm);
2118
+ const data = Buffer.isBuffer(request.data) ? request.data : Buffer.from(request.data);
2119
+ const signature = crypto__namespace.sign(signAlg, data, key);
2120
+ return {
2121
+ signature: signature.toString("base64"),
2122
+ algorithm: label
2123
+ };
2124
+ }
2125
+ function delegatedVerify(request) {
2126
+ let key;
2127
+ try {
2128
+ key = crypto__namespace.createPublicKey(request.publicKey);
2129
+ } catch {
2130
+ return false;
2131
+ }
2132
+ if (typeof request.publicKey === "string" && request.publicKey.includes("PRIVATE KEY")) {
2133
+ return false;
2134
+ }
2135
+ const { signAlg } = resolveAlgorithmForKey(key, request.algorithm);
2136
+ const sig = Buffer.from(request.signature, "base64");
2137
+ try {
2138
+ const data = Buffer.isBuffer(request.data) ? request.data : Buffer.from(request.data);
2139
+ return crypto__namespace.verify(signAlg, data, key, sig);
2140
+ } catch {
2141
+ return false;
2142
+ }
2143
+ }
2144
+
1019
2145
  // src/doctor/checks.ts
1020
2146
  function parseVersion(raw) {
1021
2147
  const match = /(\d+)\.(\d+)\.(\d+)/.exec(raw);
@@ -1268,7 +2394,7 @@ var VaultKeeper = class _VaultKeeper {
1268
2394
  }
1269
2395
  const now = Math.floor(Date.now() / 1e3);
1270
2396
  const claims = {
1271
- jti: crypto4__namespace.randomUUID(),
2397
+ jti: crypto__namespace.randomUUID(),
1272
2398
  exp: now + ttlMinutes * 60,
1273
2399
  iat: now,
1274
2400
  sub: secretName,
@@ -1373,6 +2499,52 @@ var VaultKeeper = class _VaultKeeper {
1373
2499
  const claims = validateCapabilityToken(token);
1374
2500
  return createSecretAccessor(claims.val);
1375
2501
  }
2502
+ /**
2503
+ * Sign data using the private key embedded in a capability token.
2504
+ *
2505
+ * The signing key is extracted from the token's encrypted claims, used
2506
+ * for a single `crypto.sign()` call, and never exposed to the caller.
2507
+ * The algorithm is auto-detected from the key type unless overridden
2508
+ * in the request.
2509
+ *
2510
+ * @param token - A `CapabilityToken` obtained from `authorize()`.
2511
+ * @param request - The data to sign and optional algorithm override.
2512
+ * @returns The base64-encoded signature and algorithm label, together
2513
+ * with the vault metadata (`vaultResponse`).
2514
+ * @throws {VaultError} If `token` is invalid or was not created by this
2515
+ * vault instance.
2516
+ * @throws {InvalidAlgorithmError} If `request.algorithm` is not in the
2517
+ * allowed set (e.g. `'md5'`).
2518
+ */
2519
+ async sign(token, request) {
2520
+ const claims = validateCapabilityToken(token);
2521
+ const result = delegatedSign(claims.val, request);
2522
+ await Promise.resolve();
2523
+ return {
2524
+ result,
2525
+ vaultResponse: { keyStatus: "current" }
2526
+ };
2527
+ }
2528
+ /**
2529
+ * Verify a signature using a public key.
2530
+ *
2531
+ * This is a static method — no VaultKeeper instance, secrets, or
2532
+ * capability tokens are required. It is safe to call from CI or any
2533
+ * context that has access to public key material.
2534
+ *
2535
+ * Returns `false` for invalid key material, malformed signatures, or
2536
+ * any verification failure (except disallowed algorithms, which throw).
2537
+ *
2538
+ * @throws {InvalidAlgorithmError} If `request.algorithm` is not in the
2539
+ * allowed set (e.g. `'md5'`).
2540
+ *
2541
+ * @param request - The data, signature, public key, and optional
2542
+ * algorithm override.
2543
+ * @returns `true` if the signature is valid, `false` otherwise.
2544
+ */
2545
+ static verify(request) {
2546
+ return delegatedVerify(request);
2547
+ }
1376
2548
  /**
1377
2549
  * Rotate the current encryption key.
1378
2550
  *
@@ -1448,7 +2620,7 @@ var VaultKeeper = class _VaultKeeper {
1448
2620
  []
1449
2621
  );
1450
2622
  }
1451
- return BackendRegistry.create(firstEnabled.type);
2623
+ return BackendRegistry.create(firstEnabled.type, firstEnabled);
1452
2624
  }
1453
2625
  #requireBackend() {
1454
2626
  if (this.#backend === void 0) {
@@ -1497,6 +2669,7 @@ exports.CapabilityToken = CapabilityToken;
1497
2669
  exports.DeviceNotPresentError = DeviceNotPresentError;
1498
2670
  exports.FilesystemError = FilesystemError;
1499
2671
  exports.IdentityMismatchError = IdentityMismatchError;
2672
+ exports.InvalidAlgorithmError = InvalidAlgorithmError;
1500
2673
  exports.KeyRevokedError = KeyRevokedError;
1501
2674
  exports.KeyRotatedError = KeyRotatedError;
1502
2675
  exports.PluginNotFoundError = PluginNotFoundError;