vaultkeeper 0.4.0 → 0.5.1

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);
@@ -1023,10 +2100,12 @@ function resolveAlgorithmForKey(key, override) {
1023
2100
  if (keyType === "ed25519" || keyType === "ed448") {
1024
2101
  return { signAlg: null, label: keyType };
1025
2102
  }
1026
- const alg = override ?? "sha256";
2103
+ const alg = (override ?? "sha256").toLowerCase();
1027
2104
  if (!ALLOWED_ALGORITHMS.has(alg)) {
1028
- throw new VaultError(
1029
- `Unsupported signing algorithm '${alg}'. Allowed: ${[...ALLOWED_ALGORITHMS].join(", ")}`
2105
+ throw new InvalidAlgorithmError(
2106
+ `Unsupported algorithm '${alg}'. Allowed: ${[...ALLOWED_ALGORITHMS].join(", ")}`,
2107
+ alg,
2108
+ [...ALLOWED_ALGORITHMS]
1030
2109
  );
1031
2110
  }
1032
2111
  return { signAlg: alg, label: alg };
@@ -1034,10 +2113,10 @@ function resolveAlgorithmForKey(key, override) {
1034
2113
 
1035
2114
  // src/access/delegated-sign.ts
1036
2115
  function delegatedSign(secretPem, request) {
1037
- const key = crypto4__namespace.createPrivateKey(secretPem);
2116
+ const key = crypto__namespace.createPrivateKey(secretPem);
1038
2117
  const { signAlg, label } = resolveAlgorithmForKey(key, request.algorithm);
1039
2118
  const data = Buffer.isBuffer(request.data) ? request.data : Buffer.from(request.data);
1040
- const signature = crypto4__namespace.sign(signAlg, data, key);
2119
+ const signature = crypto__namespace.sign(signAlg, data, key);
1041
2120
  return {
1042
2121
  signature: signature.toString("base64"),
1043
2122
  algorithm: label
@@ -1046,15 +2125,18 @@ function delegatedSign(secretPem, request) {
1046
2125
  function delegatedVerify(request) {
1047
2126
  let key;
1048
2127
  try {
1049
- key = crypto4__namespace.createPublicKey(request.publicKey);
2128
+ key = crypto__namespace.createPublicKey(request.publicKey);
1050
2129
  } catch {
1051
2130
  return false;
1052
2131
  }
2132
+ if (typeof request.publicKey === "string" && request.publicKey.includes("PRIVATE KEY")) {
2133
+ return false;
2134
+ }
1053
2135
  const { signAlg } = resolveAlgorithmForKey(key, request.algorithm);
1054
2136
  const sig = Buffer.from(request.signature, "base64");
1055
2137
  try {
1056
2138
  const data = Buffer.isBuffer(request.data) ? request.data : Buffer.from(request.data);
1057
- return crypto4__namespace.verify(signAlg, data, key, sig);
2139
+ return crypto__namespace.verify(signAlg, data, key, sig);
1058
2140
  } catch {
1059
2141
  return false;
1060
2142
  }
@@ -1191,7 +2273,17 @@ function currentPlatform() {
1191
2273
 
1192
2274
  // src/doctor/runner.ts
1193
2275
  async function runDoctor(options) {
1194
- const platform = currentPlatform();
2276
+ let platform;
2277
+ try {
2278
+ platform = options?.platform ?? currentPlatform();
2279
+ } catch {
2280
+ return {
2281
+ checks: [],
2282
+ ready: false,
2283
+ warnings: [],
2284
+ nextSteps: ["Unsupported platform. vaultkeeper supports macOS, Linux, and Windows."]
2285
+ };
2286
+ }
1195
2287
  const entries = buildCheckList(platform);
1196
2288
  const resolved = await Promise.all(
1197
2289
  entries.map(async ({ check, required }) => {
@@ -1280,7 +2372,7 @@ var VaultKeeper = class _VaultKeeper {
1280
2372
  return runDoctor();
1281
2373
  }
1282
2374
  /**
1283
- * Store a secret and return a JWE token that encapsulates it.
2375
+ * Retrieve a secret from the backend and return a JWE token that encapsulates it.
1284
2376
  *
1285
2377
  * @param secretName - Identifier for the secret
1286
2378
  * @param options - Setup options
@@ -1312,7 +2404,7 @@ var VaultKeeper = class _VaultKeeper {
1312
2404
  }
1313
2405
  const now = Math.floor(Date.now() / 1e3);
1314
2406
  const claims = {
1315
- jti: crypto4__namespace.randomUUID(),
2407
+ jti: crypto__namespace.randomUUID(),
1316
2408
  exp: now + ttlMinutes * 60,
1317
2409
  iat: now,
1318
2410
  sub: secretName,
@@ -1431,6 +2523,8 @@ var VaultKeeper = class _VaultKeeper {
1431
2523
  * with the vault metadata (`vaultResponse`).
1432
2524
  * @throws {VaultError} If `token` is invalid or was not created by this
1433
2525
  * vault instance.
2526
+ * @throws {InvalidAlgorithmError} If `request.algorithm` is not in the
2527
+ * allowed set (e.g. `'md5'`).
1434
2528
  */
1435
2529
  async sign(token, request) {
1436
2530
  const claims = validateCapabilityToken(token);
@@ -1448,8 +2542,11 @@ var VaultKeeper = class _VaultKeeper {
1448
2542
  * capability tokens are required. It is safe to call from CI or any
1449
2543
  * context that has access to public key material.
1450
2544
  *
1451
- * Never throws. Returns `false` for invalid key material, malformed
1452
- * signatures, or any verification failure.
2545
+ * Returns `false` for invalid key material, malformed signatures, or
2546
+ * any verification failure (except disallowed algorithms, which throw).
2547
+ *
2548
+ * @throws {InvalidAlgorithmError} If `request.algorithm` is not in the
2549
+ * allowed set (e.g. `'md5'`).
1453
2550
  *
1454
2551
  * @param request - The data, signature, public key, and optional
1455
2552
  * algorithm override.
@@ -1533,7 +2630,7 @@ var VaultKeeper = class _VaultKeeper {
1533
2630
  []
1534
2631
  );
1535
2632
  }
1536
- return BackendRegistry.create(firstEnabled.type);
2633
+ return BackendRegistry.create(firstEnabled.type, firstEnabled);
1537
2634
  }
1538
2635
  #requireBackend() {
1539
2636
  if (this.#backend === void 0) {
@@ -1582,6 +2679,7 @@ exports.CapabilityToken = CapabilityToken;
1582
2679
  exports.DeviceNotPresentError = DeviceNotPresentError;
1583
2680
  exports.FilesystemError = FilesystemError;
1584
2681
  exports.IdentityMismatchError = IdentityMismatchError;
2682
+ exports.InvalidAlgorithmError = InvalidAlgorithmError;
1585
2683
  exports.KeyRevokedError = KeyRevokedError;
1586
2684
  exports.KeyRotatedError = KeyRotatedError;
1587
2685
  exports.PluginNotFoundError = PluginNotFoundError;
@@ -1594,5 +2692,6 @@ exports.UsageLimitExceededError = UsageLimitExceededError;
1594
2692
  exports.VaultError = VaultError;
1595
2693
  exports.VaultKeeper = VaultKeeper;
1596
2694
  exports.isListableBackend = isListableBackend;
2695
+ exports.runDoctor = runDoctor;
1597
2696
  //# sourceMappingURL=index.cjs.map
1598
2697
  //# sourceMappingURL=index.cjs.map