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.js CHANGED
@@ -1,9 +1,12 @@
1
- import { spawn } from 'child_process';
2
- import * as fs5 from 'fs/promises';
3
- import * as path5 from 'path';
1
+ import * as fs from 'fs/promises';
2
+ import * as path3 from 'path';
3
+ import { join, dirname, resolve } from 'path';
4
4
  import * as os4 from 'os';
5
- import * as crypto4 from 'crypto';
5
+ import * as crypto from 'crypto';
6
+ import { spawn } from 'child_process';
7
+ import { fileURLToPath } from 'url';
6
8
  import * as fs4 from 'fs';
9
+ import { existsSync, readFileSync } from 'fs';
7
10
  import { CompactEncrypt, compactDecrypt } from 'jose';
8
11
 
9
12
  // src/errors.ts
@@ -136,6 +139,22 @@ var IdentityMismatchError = class extends VaultError {
136
139
  this.currentHash = currentHash;
137
140
  }
138
141
  };
142
+ var InvalidAlgorithmError = class extends VaultError {
143
+ /**
144
+ * The algorithm that was requested.
145
+ */
146
+ algorithm;
147
+ /**
148
+ * The set of algorithms that are allowed.
149
+ */
150
+ allowed;
151
+ constructor(message, algorithm, allowed) {
152
+ super(message);
153
+ this.name = "InvalidAlgorithmError";
154
+ this.algorithm = algorithm;
155
+ this.allowed = allowed;
156
+ }
157
+ };
139
158
  var SetupError = class extends VaultError {
140
159
  /**
141
160
  * The name of the dependency that caused the setup failure.
@@ -171,14 +190,10 @@ var RotationInProgressError = class extends VaultError {
171
190
  }
172
191
  };
173
192
 
174
- // src/backend/types.ts
175
- function isListableBackend(backend) {
176
- return "list" in backend && typeof backend.list === "function";
177
- }
178
-
179
193
  // src/backend/registry.ts
180
194
  var BackendRegistry = class {
181
195
  static backends = /* @__PURE__ */ new Map();
196
+ static setups = /* @__PURE__ */ new Map();
182
197
  /**
183
198
  * Register a backend factory.
184
199
  * @param type - Backend type identifier
@@ -190,10 +205,11 @@ var BackendRegistry = class {
190
205
  /**
191
206
  * Create a backend instance by type.
192
207
  * @param type - Backend type identifier
208
+ * @param config - Optional backend configuration forwarded to the factory
193
209
  * @returns A SecretBackend instance
194
- * @throws Error if the backend type is not registered
210
+ * @throws {@link BackendUnavailableError} if the backend type is not registered
195
211
  */
196
- static create(type) {
212
+ static create(type, config) {
197
213
  const factory = this.backends.get(type);
198
214
  if (factory === void 0) {
199
215
  throw new BackendUnavailableError(
@@ -202,7 +218,7 @@ var BackendRegistry = class {
202
218
  Array.from(this.backends.keys())
203
219
  );
204
220
  }
205
- return factory();
221
+ return factory(config);
206
222
  }
207
223
  /**
208
224
  * Get all registered backend type identifiers.
@@ -238,18 +254,199 @@ var BackendRegistry = class {
238
254
  );
239
255
  return results.filter((type) => type !== null);
240
256
  }
257
+ /**
258
+ * Register a setup factory for a backend type.
259
+ * @param type - Backend type identifier
260
+ * @param factory - Factory function that creates a setup generator
261
+ */
262
+ static registerSetup(type, factory) {
263
+ this.setups.set(type, factory);
264
+ }
265
+ /**
266
+ * Get the setup factory for a backend type, if one is registered.
267
+ * @param type - Backend type identifier
268
+ * @returns The setup factory, or `undefined` if none is registered
269
+ */
270
+ static getSetup(type) {
271
+ return this.setups.get(type);
272
+ }
273
+ /**
274
+ * Check whether a setup factory is registered for the given backend type.
275
+ * @param type - Backend type identifier
276
+ * @returns `true` if a setup factory is registered
277
+ */
278
+ static hasSetup(type) {
279
+ return this.setups.has(type);
280
+ }
281
+ /**
282
+ * Clear all registered backend factories.
283
+ * Intended for use in tests only.
284
+ * @internal
285
+ */
286
+ static clearBackends() {
287
+ this.backends.clear();
288
+ }
289
+ /**
290
+ * Clear all registered setup factories.
291
+ * Intended for use in tests only.
292
+ * @internal
293
+ */
294
+ static clearSetups() {
295
+ this.setups.clear();
296
+ }
297
+ };
298
+ var STORAGE_DIR_NAME = path3.join(".vaultkeeper", "file");
299
+ var KEY_FILE = ".key";
300
+ var GCM_IV_BYTES = 12;
301
+ var GCM_KEY_BYTES = 32;
302
+ var GCM_TAG_LENGTH = 128;
303
+ function getStorageDir() {
304
+ return path3.join(os4.homedir(), STORAGE_DIR_NAME);
305
+ }
306
+ function getEntryPath(storageDir, id) {
307
+ const safeId = Buffer.from(id, "utf8").toString("hex");
308
+ return path3.join(storageDir, `${safeId}.enc`);
309
+ }
310
+ async function ensureStorageDir(storageDir) {
311
+ try {
312
+ await fs.mkdir(storageDir, { recursive: true, mode: 448 });
313
+ } catch (err) {
314
+ if (err instanceof Error && "code" in err && err.code !== "EEXIST") {
315
+ throw new FilesystemError(
316
+ `Failed to create storage directory: ${storageDir}`,
317
+ storageDir,
318
+ "rwx"
319
+ );
320
+ }
321
+ }
322
+ }
323
+ async function getOrCreateKey(storageDir) {
324
+ const keyPath = path3.join(storageDir, KEY_FILE);
325
+ try {
326
+ const data = await fs.readFile(keyPath);
327
+ return data;
328
+ } catch (err) {
329
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
330
+ const key = crypto.randomBytes(GCM_KEY_BYTES);
331
+ await fs.writeFile(keyPath, key, { mode: 384 });
332
+ return key;
333
+ }
334
+ throw err;
335
+ }
336
+ }
337
+ function encryptGcm(key, plaintext) {
338
+ const iv = crypto.randomBytes(GCM_IV_BYTES);
339
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv, {
340
+ authTagLength: GCM_TAG_LENGTH / 8
341
+ });
342
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
343
+ const authTag = cipher.getAuthTag();
344
+ return [iv.toString("base64"), authTag.toString("base64"), encrypted.toString("base64")].join(":");
345
+ }
346
+ function decryptGcm(key, encoded) {
347
+ const parts = encoded.split(":");
348
+ if (parts.length !== 3) {
349
+ throw new Error("Invalid encrypted file format: expected iv:authTag:ciphertext");
350
+ }
351
+ const [ivB64, authTagB64, ciphertextB64] = parts;
352
+ if (ivB64 === void 0 || authTagB64 === void 0 || ciphertextB64 === void 0) {
353
+ throw new Error("Invalid encrypted file format: missing part");
354
+ }
355
+ const iv = Buffer.from(ivB64, "base64");
356
+ const authTag = Buffer.from(authTagB64, "base64");
357
+ const ciphertext = Buffer.from(ciphertextB64, "base64");
358
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv, {
359
+ authTagLength: GCM_TAG_LENGTH / 8
360
+ });
361
+ decipher.setAuthTag(authTag);
362
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
363
+ return decrypted.toString("utf8");
364
+ }
365
+ var FileBackend = class {
366
+ type = "file";
367
+ displayName = "Encrypted File Store";
368
+ async isAvailable() {
369
+ try {
370
+ const storageDir = getStorageDir();
371
+ await ensureStorageDir(storageDir);
372
+ return true;
373
+ } catch {
374
+ return false;
375
+ }
376
+ }
377
+ async store(id, secret) {
378
+ const storageDir = getStorageDir();
379
+ await ensureStorageDir(storageDir);
380
+ const key = await getOrCreateKey(storageDir);
381
+ const entryPath = getEntryPath(storageDir, id);
382
+ const encrypted = encryptGcm(key, secret);
383
+ await fs.writeFile(entryPath, encrypted, { mode: 384 });
384
+ }
385
+ async retrieve(id) {
386
+ const storageDir = getStorageDir();
387
+ const entryPath = getEntryPath(storageDir, id);
388
+ let encoded;
389
+ try {
390
+ encoded = await fs.readFile(entryPath, "utf8");
391
+ } catch (err) {
392
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
393
+ throw new SecretNotFoundError(`Secret not found in file store: ${id}`);
394
+ }
395
+ throw err;
396
+ }
397
+ const key = await getOrCreateKey(storageDir);
398
+ try {
399
+ return decryptGcm(key, encoded);
400
+ } catch (err) {
401
+ throw new Error(
402
+ `Failed to decrypt secret: ${err instanceof Error ? err.message : String(err)}`
403
+ );
404
+ }
405
+ }
406
+ async delete(id) {
407
+ const storageDir = getStorageDir();
408
+ const entryPath = getEntryPath(storageDir, id);
409
+ try {
410
+ await fs.unlink(entryPath);
411
+ } catch (err) {
412
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
413
+ throw new SecretNotFoundError(`Secret not found in file store: ${id}`);
414
+ }
415
+ throw err;
416
+ }
417
+ }
418
+ async exists(id) {
419
+ const storageDir = getStorageDir();
420
+ const entryPath = getEntryPath(storageDir, id);
421
+ try {
422
+ await fs.access(entryPath);
423
+ return true;
424
+ } catch {
425
+ return false;
426
+ }
427
+ }
428
+ async list() {
429
+ const storageDir = getStorageDir();
430
+ let entries;
431
+ try {
432
+ entries = await fs.readdir(storageDir);
433
+ } catch {
434
+ return [];
435
+ }
436
+ return entries.filter((f) => f.endsWith(".enc")).map((f) => Buffer.from(f.slice(0, -4), "hex").toString("utf8"));
437
+ }
241
438
  };
242
439
  async function execCommand(command, args, options) {
243
- const result = await execCommandFull(command, args);
440
+ const result = await execCommandFull(command, args, options);
244
441
  if (result.exitCode !== 0) {
245
442
  throw new Error(`Command failed with exit code ${String(result.exitCode)}: ${result.stderr}`);
246
443
  }
247
444
  return result.stdout.trim();
248
445
  }
249
446
  function execCommandFull(command, args, options) {
250
- return new Promise((resolve, reject) => {
447
+ return new Promise((resolve2, reject) => {
251
448
  const proc = spawn(command, args, {
252
- stdio: ["ignore", "pipe", "pipe"]
449
+ stdio: [options?.stdin !== void 0 ? "pipe" : "ignore", "pipe", "pipe"]
253
450
  });
254
451
  let stdout = "";
255
452
  let stderr = "";
@@ -259,25 +456,887 @@ function execCommandFull(command, args, options) {
259
456
  proc.stderr?.on("data", (data) => {
260
457
  stderr += data.toString();
261
458
  });
459
+ if (options?.stdin !== void 0 && proc.stdin) {
460
+ proc.stdin.write(options.stdin);
461
+ proc.stdin.end();
462
+ }
463
+ if (options?.timeoutMs !== void 0) {
464
+ setTimeout(() => {
465
+ proc.kill("SIGTERM");
466
+ reject(new Error(`Command timed out after ${String(options.timeoutMs)}ms`));
467
+ }, options.timeoutMs);
468
+ }
262
469
  proc.on("close", (code) => {
263
- resolve({ stdout, stderr, exitCode: code ?? 1 });
470
+ resolve2({ stdout, stderr, exitCode: code ?? 1 });
264
471
  });
265
472
  proc.on("error", (error) => {
266
473
  reject(error);
267
474
  });
268
475
  });
269
476
  }
270
- path5.join(".vaultkeeper", "file");
271
- path5.join(".vaultkeeper", "yubikey");
477
+
478
+ // src/backend/keychain-backend.ts
479
+ var ACCOUNT = "vaultkeeper";
480
+ var SERVICE_PREFIX = "vaultkeeper:";
481
+ var KeychainBackend = class {
482
+ type = "keychain";
483
+ displayName = "macOS Keychain";
484
+ async isAvailable() {
485
+ if (process.platform !== "darwin") {
486
+ return false;
487
+ }
488
+ try {
489
+ const result = await execCommandFull("security", ["version"]);
490
+ return result.exitCode === 0;
491
+ } catch {
492
+ return false;
493
+ }
494
+ }
495
+ async store(id, secret) {
496
+ const service = `${SERVICE_PREFIX}${id}`;
497
+ const encoded = Buffer.from(secret, "utf8").toString("base64");
498
+ await execCommandFull("security", [
499
+ "delete-generic-password",
500
+ "-a",
501
+ ACCOUNT,
502
+ "-s",
503
+ service
504
+ ]);
505
+ await execCommand("security", [
506
+ "add-generic-password",
507
+ "-a",
508
+ ACCOUNT,
509
+ "-s",
510
+ service,
511
+ "-w",
512
+ encoded
513
+ ]);
514
+ }
515
+ async retrieve(id) {
516
+ const service = `${SERVICE_PREFIX}${id}`;
517
+ const result = await execCommandFull("security", [
518
+ "find-generic-password",
519
+ "-a",
520
+ ACCOUNT,
521
+ "-s",
522
+ service,
523
+ "-w"
524
+ ]);
525
+ if (result.exitCode !== 0) {
526
+ throw new SecretNotFoundError(`Secret not found in macOS Keychain: ${id}`);
527
+ }
528
+ const encoded = result.stdout.trim();
529
+ return Buffer.from(encoded, "base64").toString("utf8");
530
+ }
531
+ async delete(id) {
532
+ const service = `${SERVICE_PREFIX}${id}`;
533
+ const result = await execCommandFull("security", [
534
+ "delete-generic-password",
535
+ "-a",
536
+ ACCOUNT,
537
+ "-s",
538
+ service
539
+ ]);
540
+ if (result.exitCode !== 0) {
541
+ throw new SecretNotFoundError(`Secret not found in macOS Keychain: ${id}`);
542
+ }
543
+ }
544
+ async exists(id) {
545
+ const service = `${SERVICE_PREFIX}${id}`;
546
+ const result = await execCommandFull("security", [
547
+ "find-generic-password",
548
+ "-a",
549
+ ACCOUNT,
550
+ "-s",
551
+ service
552
+ ]);
553
+ return result.exitCode === 0;
554
+ }
555
+ async list() {
556
+ const result = await execCommandFull("security", [
557
+ "dump-keychain"
558
+ ]);
559
+ if (result.exitCode !== 0) {
560
+ return [];
561
+ }
562
+ const ids = [];
563
+ const servicePattern = /0x00000007 <blob>="vaultkeeper:([^"]+)"/g;
564
+ let match = servicePattern.exec(result.stdout);
565
+ while (match !== null) {
566
+ const id = match[1];
567
+ if (id !== void 0) {
568
+ ids.push(id);
569
+ }
570
+ match = servicePattern.exec(result.stdout);
571
+ }
572
+ return ids;
573
+ }
574
+ };
575
+ function getStoragePath() {
576
+ return path3.join(os4.homedir(), ".vaultkeeper", "dpapi");
577
+ }
578
+ function getEntryPath2(storageDir, id) {
579
+ const safeId = Buffer.from(id, "utf8").toString("hex");
580
+ return path3.join(storageDir, `${safeId}.enc`);
581
+ }
582
+ var DpapiBackend = class {
583
+ type = "dpapi";
584
+ displayName = "Windows DPAPI";
585
+ async isAvailable() {
586
+ if (process.platform !== "win32") {
587
+ return false;
588
+ }
589
+ try {
590
+ const result = await execCommandFull("powershell", [
591
+ "-NoProfile",
592
+ "-Command",
593
+ "[System.Security.Cryptography.ProtectedData] | Out-Null; exit 0"
594
+ ]);
595
+ return result.exitCode === 0;
596
+ } catch {
597
+ return false;
598
+ }
599
+ }
600
+ async store(id, secret) {
601
+ const storageDir = getStoragePath();
602
+ await fs.mkdir(storageDir, { recursive: true });
603
+ const entryPath = getEntryPath2(storageDir, id);
604
+ const script = [
605
+ "Add-Type -AssemblyName System.Security",
606
+ `$bytes = [System.Text.Encoding]::UTF8.GetBytes(${JSON.stringify(secret)})`,
607
+ "$entropy = $null",
608
+ "$scope = [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
609
+ "$encrypted = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $entropy, $scope)",
610
+ `[System.IO.File]::WriteAllBytes(${JSON.stringify(entryPath)}, $encrypted)`
611
+ ].join("; ");
612
+ await execCommand("powershell", ["-NoProfile", "-Command", script]);
613
+ }
614
+ async retrieve(id) {
615
+ const storageDir = getStoragePath();
616
+ const entryPath = getEntryPath2(storageDir, id);
617
+ try {
618
+ await fs.access(entryPath);
619
+ } catch {
620
+ throw new SecretNotFoundError(`Secret not found in Windows DPAPI store: ${id}`);
621
+ }
622
+ const script = [
623
+ "Add-Type -AssemblyName System.Security",
624
+ `$encrypted = [System.IO.File]::ReadAllBytes(${JSON.stringify(entryPath)})`,
625
+ "$entropy = $null",
626
+ "$scope = [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
627
+ "$bytes = [System.Security.Cryptography.ProtectedData]::Unprotect($encrypted, $entropy, $scope)",
628
+ "Write-Output ([System.Text.Encoding]::UTF8.GetString($bytes))"
629
+ ].join("; ");
630
+ return execCommand("powershell", ["-NoProfile", "-Command", script]);
631
+ }
632
+ async delete(id) {
633
+ const storageDir = getStoragePath();
634
+ const entryPath = getEntryPath2(storageDir, id);
635
+ try {
636
+ await fs.unlink(entryPath);
637
+ } catch (err) {
638
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
639
+ throw new SecretNotFoundError(`Secret not found in Windows DPAPI store: ${id}`);
640
+ }
641
+ throw err;
642
+ }
643
+ }
644
+ async exists(id) {
645
+ const storageDir = getStoragePath();
646
+ const entryPath = getEntryPath2(storageDir, id);
647
+ try {
648
+ await fs.access(entryPath);
649
+ return true;
650
+ } catch {
651
+ return false;
652
+ }
653
+ }
654
+ async list() {
655
+ const storageDir = getStoragePath();
656
+ let entries;
657
+ try {
658
+ entries = await fs.readdir(storageDir);
659
+ } catch {
660
+ return [];
661
+ }
662
+ return entries.filter((f) => f.endsWith(".enc")).map((f) => Buffer.from(f.slice(0, -4), "hex").toString("utf8"));
663
+ }
664
+ };
665
+
666
+ // src/backend/secret-tool-backend.ts
667
+ var ATTRIBUTE_KEY = "vaultkeeper-id";
668
+ var LABEL_PREFIX = "vaultkeeper: ";
669
+ var SecretToolBackend = class {
670
+ type = "secret-tool";
671
+ displayName = "Linux Secret Service (secret-tool)";
672
+ async isAvailable() {
673
+ if (process.platform !== "linux") {
674
+ return false;
675
+ }
676
+ try {
677
+ const result = await execCommandFull("secret-tool", ["--version"]);
678
+ return result.exitCode === 0;
679
+ } catch {
680
+ return false;
681
+ }
682
+ }
683
+ async store(id, secret) {
684
+ const label = `${LABEL_PREFIX}${id}`;
685
+ await execCommand(
686
+ "secret-tool",
687
+ ["store", "--label", label, ATTRIBUTE_KEY, id],
688
+ { stdin: secret }
689
+ );
690
+ }
691
+ async retrieve(id) {
692
+ const result = await execCommandFull("secret-tool", ["lookup", ATTRIBUTE_KEY, id]);
693
+ if (result.exitCode !== 0 || result.stdout.trim() === "") {
694
+ throw new SecretNotFoundError(`Secret not found in Secret Service: ${id}`);
695
+ }
696
+ return result.stdout.trim();
697
+ }
698
+ async delete(id) {
699
+ const result = await execCommandFull("secret-tool", ["clear", ATTRIBUTE_KEY, id]);
700
+ if (result.exitCode !== 0) {
701
+ throw new SecretNotFoundError(`Secret not found in Secret Service: ${id}`);
702
+ }
703
+ }
704
+ async exists(id) {
705
+ const result = await execCommandFull("secret-tool", ["lookup", ATTRIBUTE_KEY, id]);
706
+ return result.exitCode === 0 && result.stdout.trim() !== "";
707
+ }
708
+ async list() {
709
+ const result = await execCommandFull("secret-tool", [
710
+ "search",
711
+ ATTRIBUTE_KEY,
712
+ ""
713
+ ]);
714
+ if (result.exitCode !== 0) {
715
+ return [];
716
+ }
717
+ const ids = [];
718
+ const attrPattern = new RegExp(`attribute\\.${ATTRIBUTE_KEY} = (.+)`, "g");
719
+ let match = attrPattern.exec(result.stdout);
720
+ while (match !== null) {
721
+ const id = match[1];
722
+ if (id !== void 0) {
723
+ ids.push(id);
724
+ }
725
+ match = attrPattern.exec(result.stdout);
726
+ }
727
+ return ids;
728
+ }
729
+ };
730
+ var INTEGRATION_NAME = "vaultkeeper";
731
+ var cachedVersion;
732
+ function getIntegrationVersion() {
733
+ if (cachedVersion !== void 0) return cachedVersion;
734
+ const dir = dirname(fileURLToPath(import.meta.url));
735
+ const candidates = [
736
+ resolve(dir, "..", "..", "package.json"),
737
+ resolve(dir, "..", "package.json")
738
+ ];
739
+ for (const candidate of candidates) {
740
+ if (!existsSync(candidate)) continue;
741
+ const raw = JSON.parse(readFileSync(candidate, "utf8"));
742
+ if (raw !== null && typeof raw === "object" && "version" in raw && typeof raw.version === "string") {
743
+ cachedVersion = raw.version;
744
+ return cachedVersion;
745
+ }
746
+ }
747
+ throw new Error(
748
+ `Could not read version from vaultkeeper package.json. Tried paths: ${candidates.join(", ")}`
749
+ );
750
+ }
751
+
752
+ // src/backend/one-password-backend.ts
753
+ var SDK_INSTALL_URL = "https://developer.1password.com/docs/sdks/";
754
+ var TAG = "vaultkeeper";
755
+ var PASSWORD_FIELD_TITLE = "password";
756
+ var SESSION_TIMEOUT_MS = 3e4;
757
+ function isWorkerSuccess(res) {
758
+ return "value" in res;
759
+ }
760
+ function isWorkerResponse(value) {
761
+ if (value === null || typeof value !== "object") return false;
762
+ if ("value" in value && typeof value.value === "string") return true;
763
+ if ("error" in value && typeof value.error === "string" && "code" in value && typeof value.code === "string")
764
+ return true;
765
+ return false;
766
+ }
767
+ var OnePasswordBackend = class {
768
+ type = "1password";
769
+ displayName = "1Password";
770
+ vaultId;
771
+ account;
772
+ serviceAccountToken;
773
+ accessMode;
774
+ sessionTimeoutMs;
775
+ /** In-flight or resolved client promise — prevents duplicate createClient calls. */
776
+ clientPromise;
777
+ constructor(options) {
778
+ if (options.accessMode === "per-access" && options.serviceAccountToken !== void 0) {
779
+ throw new Error(
780
+ "per-access mode requires desktop biometric authentication and cannot be used with a service account token"
781
+ );
782
+ }
783
+ if (options.account !== void 0 && options.serviceAccountToken !== void 0) {
784
+ throw new Error(
785
+ "account and serviceAccountToken are mutually exclusive \u2014 provide one or the other, not both"
786
+ );
787
+ }
788
+ this.vaultId = options.vault;
789
+ this.sessionTimeoutMs = options.sessionTimeoutMs ?? SESSION_TIMEOUT_MS;
790
+ if (options.account !== void 0) {
791
+ this.account = options.account;
792
+ }
793
+ if (options.serviceAccountToken !== void 0) {
794
+ this.serviceAccountToken = options.serviceAccountToken;
795
+ }
796
+ this.accessMode = options.accessMode ?? "session";
797
+ }
798
+ async isAvailable() {
799
+ const sdk = await this.tryLoadSdk();
800
+ return sdk !== null;
801
+ }
802
+ // ---- Session client management ----
803
+ /**
804
+ * Dynamically import the SDK. Returns `null` if the SDK is not installed or
805
+ * the native library cannot be loaded.
806
+ */
807
+ async tryLoadSdk() {
808
+ try {
809
+ const sdk = await import('@1password/sdk');
810
+ return sdk;
811
+ } catch {
812
+ return null;
813
+ }
814
+ }
815
+ /**
816
+ * Acquire (or create) a cached SDK client.
817
+ * Wraps `createClient` with a configurable timeout (default 30 s) to handle
818
+ * the known beta SDK hang after session expiry.
819
+ */
820
+ acquireClient() {
821
+ this.clientPromise ??= this.createClientInternal().catch((err) => {
822
+ this.clientPromise = void 0;
823
+ throw err;
824
+ });
825
+ return this.clientPromise;
826
+ }
827
+ async createClientInternal() {
828
+ const sdk = await this.tryLoadSdk();
829
+ if (sdk === null) {
830
+ throw new PluginNotFoundError(
831
+ "1Password SDK (@1password/sdk) is not available. Install it to use this backend.",
832
+ "@1password/sdk",
833
+ SDK_INSTALL_URL
834
+ );
835
+ }
836
+ const auth = this.buildAuth(sdk);
837
+ let timerId;
838
+ const timeoutPromise = new Promise((_resolve, reject) => {
839
+ timerId = setTimeout(() => {
840
+ reject(new BackendLockedError("1Password session timed out waiting for authentication", true));
841
+ }, this.sessionTimeoutMs);
842
+ });
843
+ try {
844
+ const client = await Promise.race([
845
+ sdk.createClient({
846
+ auth,
847
+ integrationName: INTEGRATION_NAME,
848
+ integrationVersion: getIntegrationVersion()
849
+ }),
850
+ timeoutPromise
851
+ ]);
852
+ return client;
853
+ } catch (err) {
854
+ if (err instanceof BackendLockedError) {
855
+ throw err;
856
+ }
857
+ if (err instanceof sdk.DesktopSessionExpiredError) {
858
+ throw new BackendLockedError(
859
+ "1Password session has expired. Please unlock the app.",
860
+ true
861
+ );
862
+ }
863
+ throw new AuthorizationDeniedError(
864
+ `1Password authentication failed: ${String(err)}`
865
+ );
866
+ } finally {
867
+ if (timerId !== void 0) {
868
+ clearTimeout(timerId);
869
+ }
870
+ }
871
+ }
872
+ buildAuth(sdk) {
873
+ if (this.serviceAccountToken !== void 0) {
874
+ return this.serviceAccountToken;
875
+ }
876
+ const accountName = this.account ?? "";
877
+ return new sdk.DesktopAuth(accountName);
878
+ }
879
+ // ---- Helpers for item lookup by title ----
880
+ /**
881
+ * List all items in the vault tagged "vaultkeeper" and find one with the
882
+ * matching title (= secret ID). Returns `undefined` if not found.
883
+ */
884
+ async findItemOverview(client, id) {
885
+ const overviews = await client.items.list(this.vaultId);
886
+ for (const overview of overviews) {
887
+ if (overview.title === id && overview.tags.includes(TAG)) {
888
+ return overview;
889
+ }
890
+ }
891
+ return void 0;
892
+ }
893
+ /**
894
+ * Fetch the full item for a given secret id. Returns `undefined` if not found.
895
+ */
896
+ async findItem(client, id) {
897
+ const overview = await this.findItemOverview(client, id);
898
+ if (overview === void 0) return void 0;
899
+ return client.items.get(this.vaultId, overview.id);
900
+ }
901
+ /**
902
+ * Extract the concealed password field value from an item.
903
+ */
904
+ extractSecret(item, id) {
905
+ for (const field of item.fields) {
906
+ if (field.title === PASSWORD_FIELD_TITLE) {
907
+ return field.value;
908
+ }
909
+ }
910
+ throw new SecretNotFoundError(
911
+ `Secret found in 1Password but missing password field: ${id}`
912
+ );
913
+ }
914
+ // ---- SecretBackend / ListableBackend implementation ----
915
+ async store(id, secret) {
916
+ const { ItemCategory, ItemFieldType } = await this.requireSdk();
917
+ const client = await this.acquireClient();
918
+ const existing = await this.findItem(client, id);
919
+ if (existing !== void 0) {
920
+ const hasPasswordField = existing.fields.some((f) => f.title === PASSWORD_FIELD_TITLE);
921
+ const updatedFields = hasPasswordField ? existing.fields.map((f) => {
922
+ if (f.title === PASSWORD_FIELD_TITLE) {
923
+ return { ...f, value: secret };
924
+ }
925
+ return f;
926
+ }) : [
927
+ ...existing.fields,
928
+ {
929
+ id: "password",
930
+ title: PASSWORD_FIELD_TITLE,
931
+ fieldType: ItemFieldType.Concealed,
932
+ value: secret
933
+ }
934
+ ];
935
+ await client.items.put({ ...existing, fields: updatedFields });
936
+ } else {
937
+ await client.items.create({
938
+ category: ItemCategory.Password,
939
+ vaultId: this.vaultId,
940
+ title: id,
941
+ tags: [TAG],
942
+ fields: [
943
+ {
944
+ id: "password",
945
+ title: PASSWORD_FIELD_TITLE,
946
+ fieldType: ItemFieldType.Concealed,
947
+ value: secret
948
+ }
949
+ ]
950
+ });
951
+ }
952
+ }
953
+ async retrieve(id) {
954
+ if (this.accessMode === "per-access") {
955
+ return this.retrieveViaWorker(id);
956
+ }
957
+ return this.retrieveViaSession(id);
958
+ }
959
+ async retrieveViaSession(id) {
960
+ const client = await this.acquireClient();
961
+ const item = await this.findItem(client, id);
962
+ if (item === void 0) {
963
+ throw new SecretNotFoundError(`Secret not found in 1Password: ${id}`);
964
+ }
965
+ return this.extractSecret(item, id);
966
+ }
967
+ /**
968
+ * Spawn the per-access worker script that triggers a fresh biometric prompt
969
+ * for each retrieval, then returns the secret from its stdout.
970
+ */
971
+ retrieveViaWorker(id) {
972
+ return new Promise((resolve2, reject) => {
973
+ const workerPath = join(
974
+ dirname(fileURLToPath(import.meta.url)),
975
+ "one-password-worker.js"
976
+ );
977
+ const accountArg = this.account ?? "";
978
+ const child = spawn(
979
+ process.execPath,
980
+ [workerPath, accountArg, this.vaultId, id],
981
+ { stdio: ["ignore", "pipe", "pipe"] }
982
+ );
983
+ const stdoutChunks = [];
984
+ const stderrChunks = [];
985
+ child.stdout.on("data", (chunk) => {
986
+ stdoutChunks.push(chunk);
987
+ });
988
+ child.stderr.on("data", (chunk) => {
989
+ stderrChunks.push(chunk);
990
+ });
991
+ child.on("close", (code) => {
992
+ const raw = Buffer.concat(stdoutChunks).toString("utf8").trim();
993
+ if (raw === "") {
994
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
995
+ const detail = stderr !== "" ? stderr : `exit code ${String(code)}`;
996
+ reject(new Error(`1Password per-access worker crashed for secret ${id}: ${detail}`));
997
+ return;
998
+ }
999
+ let parsed;
1000
+ try {
1001
+ parsed = JSON.parse(raw);
1002
+ } catch {
1003
+ reject(new SecretNotFoundError(`Worker returned unparseable output for secret: ${id}`));
1004
+ return;
1005
+ }
1006
+ if (!isWorkerResponse(parsed)) {
1007
+ reject(new SecretNotFoundError(`Worker returned unexpected response shape for secret: ${id}`));
1008
+ return;
1009
+ }
1010
+ if (isWorkerSuccess(parsed)) {
1011
+ resolve2(parsed.value);
1012
+ } else {
1013
+ switch (parsed.code) {
1014
+ case "NOT_FOUND":
1015
+ reject(new SecretNotFoundError(`Secret not found in 1Password: ${id}`));
1016
+ break;
1017
+ case "AUTH_DENIED":
1018
+ reject(new AuthorizationDeniedError("1Password authentication was denied"));
1019
+ break;
1020
+ case "LOCKED":
1021
+ reject(new BackendLockedError("1Password is locked. Please unlock and retry.", true));
1022
+ break;
1023
+ default:
1024
+ reject(new SecretNotFoundError(`Worker failed for secret ${id}: ${parsed.error}`));
1025
+ }
1026
+ }
1027
+ });
1028
+ child.on("error", (err) => {
1029
+ reject(new Error(
1030
+ `Failed to spawn 1Password per-access worker at ${workerPath}: ${String(err)}`
1031
+ ));
1032
+ });
1033
+ });
1034
+ }
1035
+ async delete(id) {
1036
+ const client = await this.acquireClient();
1037
+ const overview = await this.findItemOverview(client, id);
1038
+ if (overview === void 0) {
1039
+ throw new SecretNotFoundError(`Secret not found in 1Password: ${id}`);
1040
+ }
1041
+ await client.items.delete(this.vaultId, overview.id);
1042
+ }
1043
+ async exists(id) {
1044
+ const client = await this.acquireClient();
1045
+ const overview = await this.findItemOverview(client, id);
1046
+ return overview !== void 0;
1047
+ }
1048
+ async list() {
1049
+ const client = await this.acquireClient();
1050
+ const overviews = await client.items.list(this.vaultId);
1051
+ const ids = [];
1052
+ for (const overview of overviews) {
1053
+ if (overview.tags.includes(TAG)) {
1054
+ ids.push(overview.title);
1055
+ }
1056
+ }
1057
+ return ids;
1058
+ }
1059
+ // ---- Private helpers ----
1060
+ /** Load SDK and throw PluginNotFoundError if unavailable. */
1061
+ async requireSdk() {
1062
+ const sdk = await this.tryLoadSdk();
1063
+ if (sdk === null) {
1064
+ throw new PluginNotFoundError(
1065
+ "1Password SDK (@1password/sdk) is not available.",
1066
+ "@1password/sdk",
1067
+ SDK_INSTALL_URL
1068
+ );
1069
+ }
1070
+ return sdk;
1071
+ }
1072
+ };
1073
+ var YKMAN_INSTALL_URL = "https://developers.yubico.com/yubikey-manager/";
1074
+ var STORAGE_DIR_NAME2 = path3.join(".vaultkeeper", "yubikey");
1075
+ var METADATA_FILE = "metadata.json";
1076
+ var DEVICE_TIMEOUT_MS = 5e3;
1077
+ var GCM_IV_BYTES2 = 12;
1078
+ var GCM_KEY_BYTES2 = 32;
1079
+ var GCM_TAG_LENGTH_BITS = 128;
1080
+ var FORMAT_VERSION = "1";
1081
+ function getStorageDir2() {
1082
+ return path3.join(os4.homedir(), STORAGE_DIR_NAME2);
1083
+ }
1084
+ function getEntryPath3(storageDir, id) {
1085
+ const safeId = Buffer.from(id, "utf8").toString("hex");
1086
+ return path3.join(storageDir, `${safeId}.enc`);
1087
+ }
1088
+ function isStringRecord(value) {
1089
+ if (value === null || typeof value !== "object") {
1090
+ return false;
1091
+ }
1092
+ return Object.values(value).every((v) => typeof v === "string");
1093
+ }
1094
+ async function loadMetadata(storageDir) {
1095
+ const metaPath = path3.join(storageDir, METADATA_FILE);
1096
+ try {
1097
+ const raw = await fs.readFile(metaPath, "utf8");
1098
+ const parsed = JSON.parse(raw);
1099
+ if (parsed !== null && typeof parsed === "object" && "entries" in parsed && isStringRecord(parsed.entries)) {
1100
+ return { entries: parsed.entries };
1101
+ }
1102
+ return { entries: {} };
1103
+ } catch {
1104
+ return { entries: {} };
1105
+ }
1106
+ }
1107
+ async function saveMetadata(storageDir, metadata) {
1108
+ const metaPath = path3.join(storageDir, METADATA_FILE);
1109
+ await fs.writeFile(metaPath, JSON.stringify(metadata, null, 2), { mode: 384 });
1110
+ }
1111
+ var HMAC_RESPONSE_HEX_LENGTH = 40;
1112
+ var HMAC_RESPONSE_RE = /^[0-9a-fA-F]{40}$/;
1113
+ function deriveKey(hmacResponse, id) {
1114
+ const trimmed = hmacResponse.trim();
1115
+ if (!HMAC_RESPONSE_RE.test(trimmed)) {
1116
+ throw new Error(
1117
+ `Invalid YubiKey HMAC response: expected exactly ${String(HMAC_RESPONSE_HEX_LENGTH)} hex characters (20 bytes), got ${String(trimmed.length)} characters`
1118
+ );
1119
+ }
1120
+ const ikm = Buffer.from(trimmed, "hex");
1121
+ const info = Buffer.from(`vaultkeeper-yubikey:${id}`, "utf8");
1122
+ const keyMaterial = crypto.hkdfSync("sha256", ikm, Buffer.alloc(0), info, GCM_KEY_BYTES2);
1123
+ return Buffer.from(keyMaterial);
1124
+ }
1125
+ function encryptGcm2(key, plaintext) {
1126
+ const iv = crypto.randomBytes(GCM_IV_BYTES2);
1127
+ let encrypted;
1128
+ let authTag;
1129
+ try {
1130
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv, {
1131
+ authTagLength: GCM_TAG_LENGTH_BITS / 8
1132
+ });
1133
+ encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
1134
+ authTag = cipher.getAuthTag();
1135
+ return [
1136
+ FORMAT_VERSION,
1137
+ iv.toString("base64"),
1138
+ authTag.toString("base64"),
1139
+ encrypted.toString("base64")
1140
+ ].join(":");
1141
+ } finally {
1142
+ key.fill(0);
1143
+ iv.fill(0);
1144
+ encrypted?.fill(0);
1145
+ authTag?.fill(0);
1146
+ }
1147
+ }
1148
+ function decryptGcm2(key, encoded) {
1149
+ const parts = encoded.split(":");
1150
+ const versionSegment = parts[0] ?? "";
1151
+ const parsedVersion = parseInt(versionSegment, 10);
1152
+ const isNumericVersion = String(parsedVersion) === versionSegment && !Number.isNaN(parsedVersion);
1153
+ if (!isNumericVersion) {
1154
+ key.fill(0);
1155
+ throw new Error(
1156
+ "Encrypted file uses a legacy format (AES-256-CBC). Delete the secret and re-store it to migrate to AES-256-GCM."
1157
+ );
1158
+ }
1159
+ if (versionSegment !== FORMAT_VERSION) {
1160
+ key.fill(0);
1161
+ throw new Error(
1162
+ `Unsupported encrypted file version: ${versionSegment}. This vaultkeeper build only supports version ${FORMAT_VERSION}. Upgrade vaultkeeper to read this secret.`
1163
+ );
1164
+ }
1165
+ if (parts.length !== 4) {
1166
+ key.fill(0);
1167
+ throw new Error(
1168
+ `Invalid encrypted file format: expected ${FORMAT_VERSION}:iv:authTag:ciphertext`
1169
+ );
1170
+ }
1171
+ const [_version, ivB64, authTagB64, ciphertextB64] = parts;
1172
+ if (ivB64 === void 0 || authTagB64 === void 0 || ciphertextB64 === void 0) {
1173
+ key.fill(0);
1174
+ throw new Error("Invalid encrypted file format: missing part");
1175
+ }
1176
+ let decrypted;
1177
+ try {
1178
+ const iv = Buffer.from(ivB64, "base64");
1179
+ const authTag = Buffer.from(authTagB64, "base64");
1180
+ const ciphertext = Buffer.from(ciphertextB64, "base64");
1181
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv, {
1182
+ authTagLength: GCM_TAG_LENGTH_BITS / 8
1183
+ });
1184
+ decipher.setAuthTag(authTag);
1185
+ try {
1186
+ decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
1187
+ } catch (err) {
1188
+ throw new Error(
1189
+ `GCM authentication failed \u2014 ciphertext may be tampered: ${err instanceof Error ? err.message : String(err)}`
1190
+ );
1191
+ }
1192
+ const plaintext = decrypted.toString("utf8");
1193
+ return plaintext;
1194
+ } finally {
1195
+ key.fill(0);
1196
+ decrypted?.fill(0);
1197
+ }
1198
+ }
1199
+ var YubikeyBackend = class {
1200
+ type = "yubikey";
1201
+ displayName = "YubiKey";
1202
+ async isAvailable() {
1203
+ try {
1204
+ const result = await execCommandFull("ykman", ["--version"]);
1205
+ if (result.exitCode !== 0) {
1206
+ return false;
1207
+ }
1208
+ const listResult = await execCommandFull("ykman", ["list"]);
1209
+ return listResult.exitCode === 0 && listResult.stdout.trim() !== "";
1210
+ } catch {
1211
+ return false;
1212
+ }
1213
+ }
1214
+ async requireDevice() {
1215
+ const available = await this.isAvailable();
1216
+ if (!available) {
1217
+ const hasYkman = await execCommandFull("ykman", ["--version"]).then(
1218
+ (r) => r.exitCode === 0,
1219
+ () => false
1220
+ );
1221
+ if (!hasYkman) {
1222
+ throw new PluginNotFoundError("ykman is not installed", "ykman", YKMAN_INSTALL_URL);
1223
+ }
1224
+ throw new DeviceNotPresentError("No YubiKey device detected", DEVICE_TIMEOUT_MS);
1225
+ }
1226
+ }
1227
+ /**
1228
+ * Perform the YubiKey HMAC-SHA1 challenge-response for `id` and return the
1229
+ * raw hex response string. Throws on device failure.
1230
+ */
1231
+ async challengeResponse(id) {
1232
+ const challenge = Buffer.from(`vaultkeeper:${id}`, "utf8").toString("hex");
1233
+ const responseResult = await execCommandFull("ykman", ["otp", "calculate", "2", challenge]);
1234
+ if (responseResult.exitCode !== 0) {
1235
+ throw new Error(`YubiKey challenge-response failed: ${responseResult.stderr}`);
1236
+ }
1237
+ return responseResult.stdout.trim();
1238
+ }
1239
+ async store(id, secret) {
1240
+ await this.requireDevice();
1241
+ const storageDir = getStorageDir2();
1242
+ await fs.mkdir(storageDir, { recursive: true, mode: 448 });
1243
+ const hmacResponse = await this.challengeResponse(id);
1244
+ const key = deriveKey(hmacResponse, id);
1245
+ const encrypted = encryptGcm2(key, secret);
1246
+ const entryPath = getEntryPath3(storageDir, id);
1247
+ await fs.writeFile(entryPath, encrypted, { mode: 384 });
1248
+ const metadata = await loadMetadata(storageDir);
1249
+ metadata.entries[id] = entryPath;
1250
+ await saveMetadata(storageDir, metadata);
1251
+ }
1252
+ async retrieve(id) {
1253
+ await this.requireDevice();
1254
+ const storageDir = getStorageDir2();
1255
+ const entryPath = getEntryPath3(storageDir, id);
1256
+ try {
1257
+ await fs.access(entryPath);
1258
+ } catch {
1259
+ throw new SecretNotFoundError(`Secret not found in YubiKey store: ${id}`);
1260
+ }
1261
+ const encoded = await fs.readFile(entryPath, "utf8");
1262
+ const hmacResponse = await this.challengeResponse(id);
1263
+ const key = deriveKey(hmacResponse, id);
1264
+ return decryptGcm2(key, encoded);
1265
+ }
1266
+ async delete(id) {
1267
+ await this.requireDevice();
1268
+ const storageDir = getStorageDir2();
1269
+ const entryPath = getEntryPath3(storageDir, id);
1270
+ try {
1271
+ await fs.unlink(entryPath);
1272
+ } catch (err) {
1273
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
1274
+ throw new SecretNotFoundError(`Secret not found in YubiKey store: ${id}`);
1275
+ }
1276
+ throw err;
1277
+ }
1278
+ const metadata = await loadMetadata(storageDir);
1279
+ delete metadata.entries[id];
1280
+ await saveMetadata(storageDir, metadata);
1281
+ }
1282
+ async exists(id) {
1283
+ const storageDir = getStorageDir2();
1284
+ const entryPath = getEntryPath3(storageDir, id);
1285
+ try {
1286
+ await fs.access(entryPath);
1287
+ return true;
1288
+ } catch {
1289
+ return false;
1290
+ }
1291
+ }
1292
+ async list() {
1293
+ const storageDir = getStorageDir2();
1294
+ const metadata = await loadMetadata(storageDir);
1295
+ return Object.keys(metadata.entries);
1296
+ }
1297
+ };
1298
+
1299
+ // src/backend/register-builtins.ts
1300
+ function registerBuiltinBackends() {
1301
+ BackendRegistry.register("file", () => new FileBackend());
1302
+ BackendRegistry.register("keychain", () => new KeychainBackend());
1303
+ BackendRegistry.register("dpapi", () => new DpapiBackend());
1304
+ BackendRegistry.register("secret-tool", () => new SecretToolBackend());
1305
+ BackendRegistry.register("1password", (config) => {
1306
+ const opts = config?.options;
1307
+ const vaultId = opts?.vault ?? "";
1308
+ const opOptions = {
1309
+ vault: vaultId,
1310
+ // 'session' is the safer default — 'per-access' re-prompts on every read.
1311
+ accessMode: opts?.accessMode === "per-access" ? "per-access" : "session"
1312
+ };
1313
+ const account = opts?.account;
1314
+ if (account !== void 0) {
1315
+ opOptions.account = account;
1316
+ }
1317
+ const serviceAccountToken = opts?.serviceAccountToken;
1318
+ if (serviceAccountToken !== void 0) {
1319
+ opOptions.serviceAccountToken = serviceAccountToken;
1320
+ }
1321
+ return new OnePasswordBackend(opOptions);
1322
+ });
1323
+ BackendRegistry.register("yubikey", () => new YubikeyBackend());
1324
+ }
1325
+ registerBuiltinBackends();
1326
+
1327
+ // src/backend/types.ts
1328
+ function isListableBackend(backend) {
1329
+ return "list" in backend && typeof backend.list === "function";
1330
+ }
272
1331
  function hashExecutable(filePath) {
273
- return new Promise((resolve, reject) => {
274
- const hash = crypto4.createHash("sha256");
1332
+ return new Promise((resolve2, reject) => {
1333
+ const hash = crypto.createHash("sha256");
275
1334
  const stream = fs4.createReadStream(filePath);
276
1335
  stream.on("data", (chunk) => {
277
1336
  hash.update(chunk);
278
1337
  });
279
1338
  stream.on("end", () => {
280
- resolve(hash.digest("hex"));
1339
+ resolve2(hash.digest("hex"));
281
1340
  });
282
1341
  stream.on("error", (err) => {
283
1342
  reject(err);
@@ -300,10 +1359,10 @@ function isTrustManifestEntry(value) {
300
1359
  return true;
301
1360
  }
302
1361
  async function loadManifest(configDir) {
303
- const manifestPath = path5.join(configDir, MANIFEST_FILENAME);
1362
+ const manifestPath = path3.join(configDir, MANIFEST_FILENAME);
304
1363
  let rawText;
305
1364
  try {
306
- rawText = await fs5.readFile(manifestPath, "utf8");
1365
+ rawText = await fs.readFile(manifestPath, "utf8");
307
1366
  } catch (err) {
308
1367
  if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
309
1368
  return /* @__PURE__ */ new Map();
@@ -323,14 +1382,14 @@ async function loadManifest(configDir) {
323
1382
  return manifest;
324
1383
  }
325
1384
  async function saveManifest(configDir, manifest) {
326
- await fs5.mkdir(configDir, { recursive: true });
1385
+ await fs.mkdir(configDir, { recursive: true });
327
1386
  const entries = {};
328
1387
  for (const [namespace, entry] of manifest) {
329
1388
  entries[namespace] = entry;
330
1389
  }
331
1390
  const raw = { version: 1, entries };
332
- const manifestPath = path5.join(configDir, MANIFEST_FILENAME);
333
- await fs5.writeFile(manifestPath, JSON.stringify(raw, null, 2), "utf8");
1391
+ const manifestPath = path3.join(configDir, MANIFEST_FILENAME);
1392
+ await fs.writeFile(manifestPath, JSON.stringify(raw, null, 2), "utf8");
334
1393
  }
335
1394
  function addTrustedHash(manifest, namespace, hash) {
336
1395
  const next = new Map(manifest);
@@ -441,14 +1500,18 @@ function validateCapabilityToken(token) {
441
1500
  return claims;
442
1501
  }
443
1502
  function getDefaultConfigDir() {
1503
+ const envOverride = process.env.VAULTKEEPER_CONFIG_DIR;
1504
+ if (envOverride !== void 0 && envOverride !== "") {
1505
+ return envOverride;
1506
+ }
444
1507
  if (process.platform === "win32") {
445
1508
  const appData = process.env.APPDATA;
446
1509
  if (appData !== void 0) {
447
- return path5.join(appData, "vaultkeeper");
1510
+ return path3.join(appData, "vaultkeeper");
448
1511
  }
449
- return path5.join(os4.homedir(), "AppData", "Roaming", "vaultkeeper");
1512
+ return path3.join(os4.homedir(), "AppData", "Roaming", "vaultkeeper");
450
1513
  }
451
- return path5.join(os4.homedir(), ".config", "vaultkeeper");
1514
+ return path3.join(os4.homedir(), ".config", "vaultkeeper");
452
1515
  }
453
1516
  function defaultConfig() {
454
1517
  return {
@@ -487,6 +1550,21 @@ function validateBackendEntry(entry, index) {
487
1550
  }
488
1551
  result.path = entry.path;
489
1552
  }
1553
+ if (entry.options !== void 0) {
1554
+ if (!isObject(entry.options)) {
1555
+ throw new Error(`backends[${String(index)}].options must be an object`);
1556
+ }
1557
+ const opts = {};
1558
+ for (const [k, v] of Object.entries(entry.options)) {
1559
+ if (typeof v !== "string") {
1560
+ throw new Error(
1561
+ `backends[${String(index)}].options["${k}"] must be a string`
1562
+ );
1563
+ }
1564
+ opts[k] = v;
1565
+ }
1566
+ result.options = opts;
1567
+ }
490
1568
  return result;
491
1569
  }
492
1570
  function validateConfig(config) {
@@ -549,10 +1627,10 @@ function validateConfig(config) {
549
1627
  }
550
1628
  async function loadConfig(configDir) {
551
1629
  const dir = configDir ?? getDefaultConfigDir();
552
- const configPath = path5.join(dir, "config.json");
1630
+ const configPath = path3.join(dir, "config.json");
553
1631
  let raw;
554
1632
  try {
555
- raw = await fs5.readFile(configPath, "utf-8");
1633
+ raw = await fs.readFile(configPath, "utf-8");
556
1634
  } catch {
557
1635
  return defaultConfig();
558
1636
  }
@@ -571,10 +1649,10 @@ var KeyManager = class {
571
1649
  #rotating = false;
572
1650
  /** Generate a new 32-byte key with a timestamp-based id. */
573
1651
  generateKey() {
574
- const randomSuffix = crypto4.randomBytes(4).toString("hex");
1652
+ const randomSuffix = crypto.randomBytes(4).toString("hex");
575
1653
  return {
576
1654
  id: `k-${String(Date.now())}-${randomSuffix}`,
577
- key: new Uint8Array(crypto4.randomBytes(32)),
1655
+ key: new Uint8Array(crypto.randomBytes(32)),
578
1656
  createdAt: /* @__PURE__ */ new Date()
579
1657
  };
580
1658
  }
@@ -876,7 +1954,7 @@ function replaceInRecord2(record, secret) {
876
1954
  function delegatedExec(secret, request) {
877
1955
  const args = (request.args ?? []).map((arg) => replacePlaceholder2(arg, secret));
878
1956
  const env = request.env !== void 0 ? replaceInRecord2(request.env, secret) : void 0;
879
- return new Promise((resolve, reject) => {
1957
+ return new Promise((resolve2, reject) => {
880
1958
  const spawnOptions = {
881
1959
  stdio: ["ignore", "pipe", "pipe"]
882
1960
  };
@@ -896,7 +1974,7 @@ function delegatedExec(secret, request) {
896
1974
  stderr += data.toString();
897
1975
  });
898
1976
  proc.on("close", (code) => {
899
- resolve({ stdout, stderr, exitCode: code ?? 1 });
1977
+ resolve2({ stdout, stderr, exitCode: code ?? 1 });
900
1978
  });
901
1979
  proc.on("error", (error) => {
902
1980
  reject(error);
@@ -997,10 +2075,12 @@ function resolveAlgorithmForKey(key, override) {
997
2075
  if (keyType === "ed25519" || keyType === "ed448") {
998
2076
  return { signAlg: null, label: keyType };
999
2077
  }
1000
- const alg = override ?? "sha256";
2078
+ const alg = (override ?? "sha256").toLowerCase();
1001
2079
  if (!ALLOWED_ALGORITHMS.has(alg)) {
1002
- throw new VaultError(
1003
- `Unsupported signing algorithm '${alg}'. Allowed: ${[...ALLOWED_ALGORITHMS].join(", ")}`
2080
+ throw new InvalidAlgorithmError(
2081
+ `Unsupported algorithm '${alg}'. Allowed: ${[...ALLOWED_ALGORITHMS].join(", ")}`,
2082
+ alg,
2083
+ [...ALLOWED_ALGORITHMS]
1004
2084
  );
1005
2085
  }
1006
2086
  return { signAlg: alg, label: alg };
@@ -1008,10 +2088,10 @@ function resolveAlgorithmForKey(key, override) {
1008
2088
 
1009
2089
  // src/access/delegated-sign.ts
1010
2090
  function delegatedSign(secretPem, request) {
1011
- const key = crypto4.createPrivateKey(secretPem);
2091
+ const key = crypto.createPrivateKey(secretPem);
1012
2092
  const { signAlg, label } = resolveAlgorithmForKey(key, request.algorithm);
1013
2093
  const data = Buffer.isBuffer(request.data) ? request.data : Buffer.from(request.data);
1014
- const signature = crypto4.sign(signAlg, data, key);
2094
+ const signature = crypto.sign(signAlg, data, key);
1015
2095
  return {
1016
2096
  signature: signature.toString("base64"),
1017
2097
  algorithm: label
@@ -1020,15 +2100,18 @@ function delegatedSign(secretPem, request) {
1020
2100
  function delegatedVerify(request) {
1021
2101
  let key;
1022
2102
  try {
1023
- key = crypto4.createPublicKey(request.publicKey);
2103
+ key = crypto.createPublicKey(request.publicKey);
1024
2104
  } catch {
1025
2105
  return false;
1026
2106
  }
2107
+ if (typeof request.publicKey === "string" && request.publicKey.includes("PRIVATE KEY")) {
2108
+ return false;
2109
+ }
1027
2110
  const { signAlg } = resolveAlgorithmForKey(key, request.algorithm);
1028
2111
  const sig = Buffer.from(request.signature, "base64");
1029
2112
  try {
1030
2113
  const data = Buffer.isBuffer(request.data) ? request.data : Buffer.from(request.data);
1031
- return crypto4.verify(signAlg, data, key, sig);
2114
+ return crypto.verify(signAlg, data, key, sig);
1032
2115
  } catch {
1033
2116
  return false;
1034
2117
  }
@@ -1165,7 +2248,17 @@ function currentPlatform() {
1165
2248
 
1166
2249
  // src/doctor/runner.ts
1167
2250
  async function runDoctor(options) {
1168
- const platform = currentPlatform();
2251
+ let platform;
2252
+ try {
2253
+ platform = options?.platform ?? currentPlatform();
2254
+ } catch {
2255
+ return {
2256
+ checks: [],
2257
+ ready: false,
2258
+ warnings: [],
2259
+ nextSteps: ["Unsupported platform. vaultkeeper supports macOS, Linux, and Windows."]
2260
+ };
2261
+ }
1169
2262
  const entries = buildCheckList(platform);
1170
2263
  const resolved = await Promise.all(
1171
2264
  entries.map(async ({ check, required }) => {
@@ -1254,7 +2347,7 @@ var VaultKeeper = class _VaultKeeper {
1254
2347
  return runDoctor();
1255
2348
  }
1256
2349
  /**
1257
- * Store a secret and return a JWE token that encapsulates it.
2350
+ * Retrieve a secret from the backend and return a JWE token that encapsulates it.
1258
2351
  *
1259
2352
  * @param secretName - Identifier for the secret
1260
2353
  * @param options - Setup options
@@ -1286,7 +2379,7 @@ var VaultKeeper = class _VaultKeeper {
1286
2379
  }
1287
2380
  const now = Math.floor(Date.now() / 1e3);
1288
2381
  const claims = {
1289
- jti: crypto4.randomUUID(),
2382
+ jti: crypto.randomUUID(),
1290
2383
  exp: now + ttlMinutes * 60,
1291
2384
  iat: now,
1292
2385
  sub: secretName,
@@ -1405,6 +2498,8 @@ var VaultKeeper = class _VaultKeeper {
1405
2498
  * with the vault metadata (`vaultResponse`).
1406
2499
  * @throws {VaultError} If `token` is invalid or was not created by this
1407
2500
  * vault instance.
2501
+ * @throws {InvalidAlgorithmError} If `request.algorithm` is not in the
2502
+ * allowed set (e.g. `'md5'`).
1408
2503
  */
1409
2504
  async sign(token, request) {
1410
2505
  const claims = validateCapabilityToken(token);
@@ -1422,8 +2517,11 @@ var VaultKeeper = class _VaultKeeper {
1422
2517
  * capability tokens are required. It is safe to call from CI or any
1423
2518
  * context that has access to public key material.
1424
2519
  *
1425
- * Never throws. Returns `false` for invalid key material, malformed
1426
- * signatures, or any verification failure.
2520
+ * Returns `false` for invalid key material, malformed signatures, or
2521
+ * any verification failure (except disallowed algorithms, which throw).
2522
+ *
2523
+ * @throws {InvalidAlgorithmError} If `request.algorithm` is not in the
2524
+ * allowed set (e.g. `'md5'`).
1427
2525
  *
1428
2526
  * @param request - The data, signature, public key, and optional
1429
2527
  * algorithm override.
@@ -1507,7 +2605,7 @@ var VaultKeeper = class _VaultKeeper {
1507
2605
  []
1508
2606
  );
1509
2607
  }
1510
- return BackendRegistry.create(firstEnabled.type);
2608
+ return BackendRegistry.create(firstEnabled.type, firstEnabled);
1511
2609
  }
1512
2610
  #requireBackend() {
1513
2611
  if (this.#backend === void 0) {
@@ -1548,6 +2646,6 @@ var VaultKeeper = class _VaultKeeper {
1548
2646
  }
1549
2647
  };
1550
2648
 
1551
- export { AuthorizationDeniedError, BackendLockedError, BackendRegistry, BackendUnavailableError, CapabilityToken, DeviceNotPresentError, FilesystemError, IdentityMismatchError, KeyRevokedError, KeyRotatedError, PluginNotFoundError, RotationInProgressError, SecretNotFoundError, SetupError, TokenExpiredError, TokenRevokedError, UsageLimitExceededError, VaultError, VaultKeeper, isListableBackend };
2649
+ export { AuthorizationDeniedError, BackendLockedError, BackendRegistry, BackendUnavailableError, CapabilityToken, DeviceNotPresentError, FilesystemError, IdentityMismatchError, InvalidAlgorithmError, KeyRevokedError, KeyRotatedError, PluginNotFoundError, RotationInProgressError, SecretNotFoundError, SetupError, TokenExpiredError, TokenRevokedError, UsageLimitExceededError, VaultError, VaultKeeper, isListableBackend, runDoctor };
1552
2650
  //# sourceMappingURL=index.js.map
1553
2651
  //# sourceMappingURL=index.js.map