vaultkeeper 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -164,6 +164,29 @@ var IdentityMismatchError = class extends VaultError {
164
164
  this.currentHash = currentHash;
165
165
  }
166
166
  };
167
+ var ExecError = class extends VaultError {
168
+ /**
169
+ * The command that failed to execute.
170
+ */
171
+ command;
172
+ constructor(message, command) {
173
+ super(message);
174
+ this.name = "ExecError";
175
+ this.command = command;
176
+ }
177
+ };
178
+ var InvalidTokenError = class extends VaultError {
179
+ constructor(message) {
180
+ super(message);
181
+ this.name = "InvalidTokenError";
182
+ }
183
+ };
184
+ var AccessorConsumedError = class extends VaultError {
185
+ constructor(message) {
186
+ super(message);
187
+ this.name = "AccessorConsumedError";
188
+ }
189
+ };
167
190
  var InvalidAlgorithmError = class extends VaultError {
168
191
  /**
169
192
  * The algorithm that was requested.
@@ -495,7 +518,17 @@ function execCommandFull(command, args, options) {
495
518
  resolve2({ stdout, stderr, exitCode: code ?? 1 });
496
519
  });
497
520
  proc.on("error", (error) => {
498
- reject(error);
521
+ if ("code" in error && error.code === "ENOENT") {
522
+ reject(
523
+ new PluginNotFoundError(
524
+ `'${command}' is not installed or not found in PATH`,
525
+ command,
526
+ ""
527
+ )
528
+ );
529
+ } else {
530
+ reject(error);
531
+ }
499
532
  });
500
533
  });
501
534
  }
@@ -1835,40 +1868,40 @@ async function decryptToken(key, jwe) {
1835
1868
  plaintext = result.plaintext;
1836
1869
  } catch (err) {
1837
1870
  const message = err instanceof Error ? err.message : String(err);
1838
- throw new VaultError(`JWE decryption failed: ${message}`);
1871
+ throw new InvalidTokenError(`JWE decryption failed: ${message}`);
1839
1872
  }
1840
1873
  let parsed;
1841
1874
  try {
1842
1875
  parsed = JSON.parse(new TextDecoder().decode(plaintext));
1843
1876
  } catch {
1844
- throw new VaultError("JWE payload is not valid JSON");
1877
+ throw new InvalidTokenError("JWE payload is not valid JSON");
1845
1878
  }
1846
1879
  const claims = parseVaultClaims(parsed);
1847
1880
  if (claims === void 0) {
1848
- throw new VaultError("JWE payload does not match VaultClaims schema");
1881
+ throw new InvalidTokenError("JWE payload does not match VaultClaims schema");
1849
1882
  }
1850
1883
  return claims;
1851
1884
  }
1852
1885
  function extractKid(jwe) {
1853
1886
  const parts = jwe.split(".");
1854
1887
  if (parts.length !== 5) {
1855
- throw new VaultError("Invalid JWE compact serialization: expected 5 parts");
1888
+ throw new InvalidTokenError("Invalid JWE compact serialization: expected 5 parts");
1856
1889
  }
1857
1890
  const headerSegment = parts[0];
1858
1891
  if (headerSegment === void 0 || headerSegment === "") {
1859
- throw new VaultError("Invalid JWE compact serialization: missing header segment");
1892
+ throw new InvalidTokenError("Invalid JWE compact serialization: missing header segment");
1860
1893
  }
1861
1894
  let headerJson;
1862
1895
  try {
1863
1896
  headerJson = Buffer.from(headerSegment, "base64url").toString("utf-8");
1864
1897
  } catch {
1865
- throw new VaultError("Invalid JWE compact serialization: header is not valid Base64URL");
1898
+ throw new InvalidTokenError("Invalid JWE compact serialization: header is not valid Base64URL");
1866
1899
  }
1867
1900
  let header;
1868
1901
  try {
1869
1902
  header = JSON.parse(headerJson);
1870
1903
  } catch {
1871
- throw new VaultError("Invalid JWE compact serialization: header is not valid JSON");
1904
+ throw new InvalidTokenError("Invalid JWE compact serialization: header is not valid JSON");
1872
1905
  }
1873
1906
  if (!isObject2(header)) {
1874
1907
  return void 0;
@@ -1983,6 +2016,12 @@ function replaceInRecord2(record, secret) {
1983
2016
  return result;
1984
2017
  }
1985
2018
  function delegatedExec(secret, request) {
2019
+ if (request.command.includes(PLACEHOLDER2)) {
2020
+ throw new ExecError(
2021
+ `The {{secret}} placeholder is not supported in the command field. Use args or env instead.`,
2022
+ request.command
2023
+ );
2024
+ }
1986
2025
  const args = (request.args ?? []).map((arg) => replacePlaceholder2(arg, secret));
1987
2026
  const env = request.env !== void 0 ? replaceInRecord2(request.env, secret) : void 0;
1988
2027
  return new Promise((resolve2, reject) => {
@@ -2008,7 +2047,22 @@ function delegatedExec(secret, request) {
2008
2047
  resolve2({ stdout, stderr, exitCode: code ?? 1 });
2009
2048
  });
2010
2049
  proc.on("error", (error) => {
2011
- reject(error);
2050
+ const isEnoent = error instanceof Error && "code" in error && error.code === "ENOENT";
2051
+ if (isEnoent) {
2052
+ reject(
2053
+ new ExecError(
2054
+ `Command not found: ${request.command}. Verify the command exists and is in PATH.`,
2055
+ request.command
2056
+ )
2057
+ );
2058
+ } else {
2059
+ reject(
2060
+ new ExecError(
2061
+ `Failed to execute command: ${request.command}. ${error instanceof Error ? error.message : String(error)}`,
2062
+ request.command
2063
+ )
2064
+ );
2065
+ }
2012
2066
  });
2013
2067
  });
2014
2068
  }
@@ -2027,7 +2081,7 @@ function createSecretAccessor(secretValue) {
2027
2081
  let consumed = false;
2028
2082
  function readImpl(callback) {
2029
2083
  if (consumed) {
2030
- throw new Error("SecretAccessor has already been consumed \u2014 call getSecret() again to obtain a new accessor");
2084
+ throw new AccessorConsumedError("SecretAccessor has already been consumed \u2014 call getSecret() again to obtain a new accessor");
2031
2085
  }
2032
2086
  consumed = true;
2033
2087
  const buf = Buffer.from(secretValue, "utf8");
@@ -2286,7 +2340,8 @@ async function runDoctor(options) {
2286
2340
  nextSteps: ["Unsupported platform. vaultkeeper supports macOS, Linux, and Windows."]
2287
2341
  };
2288
2342
  }
2289
- const entries = buildCheckList(platform);
2343
+ const enabledTypes = enabledBackendTypes(options?.backends);
2344
+ const entries = buildCheckList(platform, enabledTypes);
2290
2345
  const resolved = await Promise.all(
2291
2346
  entries.map(async ({ check, required }) => {
2292
2347
  const result = await check();
@@ -2300,16 +2355,15 @@ async function runDoctor(options) {
2300
2355
  const warnings = [];
2301
2356
  const nextSteps = [];
2302
2357
  for (const { required, result } of resolved) {
2358
+ const reasonSuffix = result.reason !== void 0 ? ` \u2014 ${result.reason}` : "";
2303
2359
  if (result.status === "missing") {
2304
2360
  if (required) {
2305
- nextSteps.push(`Install missing required dependency: ${result.name}`);
2361
+ nextSteps.push(`Install missing required dependency: ${result.name}${reasonSuffix}`);
2306
2362
  } else {
2307
- warnings.push(
2308
- `Optional dependency not found: ${result.name}${result.reason !== void 0 ? ` \u2014 ${result.reason}` : ""}`
2309
- );
2363
+ warnings.push(`Optional dependency not found: ${result.name}${reasonSuffix}`);
2310
2364
  }
2311
2365
  } else if (result.status === "version-unsupported") {
2312
- const msg = `${result.name} version is unsupported${result.reason !== void 0 ? `: ${result.reason}` : ""}`;
2366
+ const msg = `${result.name} version is unsupported${reasonSuffix}`;
2313
2367
  if (required) {
2314
2368
  nextSteps.push(`Upgrade required dependency: ${msg}`);
2315
2369
  } else {
@@ -2320,19 +2374,42 @@ async function runDoctor(options) {
2320
2374
  const checks = resolved.map(({ result }) => result);
2321
2375
  return { checks, ready, warnings, nextSteps };
2322
2376
  }
2323
- function buildCheckList(platform) {
2377
+ function enabledBackendTypes(backends) {
2378
+ if (backends === void 0) return null;
2379
+ const types = /* @__PURE__ */ new Set();
2380
+ for (const b of backends) {
2381
+ if (b.enabled) types.add(b.type);
2382
+ }
2383
+ return types;
2384
+ }
2385
+ function buildCheckList(platform, enabledTypes) {
2324
2386
  const entries = [{ check: checkOpenssl, required: true }];
2325
2387
  if (platform === "darwin") {
2326
- entries.push({ check: checkSecurity, required: true });
2388
+ entries.push({
2389
+ check: checkSecurity,
2390
+ required: enabledTypes === null || enabledTypes.has("keychain")
2391
+ });
2327
2392
  entries.push({ check: checkBash, required: false });
2328
2393
  } else if (platform === "win32") {
2329
- entries.push({ check: checkPowershell, required: true });
2394
+ entries.push({
2395
+ check: checkPowershell,
2396
+ required: enabledTypes === null || enabledTypes.has("dpapi")
2397
+ });
2330
2398
  } else {
2331
2399
  entries.push({ check: checkBash, required: true });
2332
- entries.push({ check: checkSecretTool, required: true });
2400
+ entries.push({
2401
+ check: checkSecretTool,
2402
+ required: enabledTypes === null || enabledTypes.has("secret-tool")
2403
+ });
2333
2404
  }
2334
- entries.push({ check: checkOp, required: false });
2335
- entries.push({ check: checkYkman, required: false });
2405
+ entries.push({
2406
+ check: checkOp,
2407
+ required: enabledTypes?.has("1password") ?? false
2408
+ });
2409
+ entries.push({
2410
+ check: checkYkman,
2411
+ required: enabledTypes?.has("yubikey") ?? false
2412
+ });
2336
2413
  return entries;
2337
2414
  }
2338
2415
 
@@ -2354,34 +2431,73 @@ var VaultKeeper = class _VaultKeeper {
2354
2431
  * Runs doctor checks (unless skipped), loads config, and sets up the key manager.
2355
2432
  */
2356
2433
  static async init(options) {
2434
+ const configDir = options?.configDir ?? getDefaultConfigDir();
2435
+ const config = options?.config ?? await loadConfig(configDir);
2357
2436
  if (options?.skipDoctor !== true) {
2358
- const doctorResult = await runDoctor();
2437
+ const doctorResult = await runDoctor({ backends: config.backends });
2359
2438
  if (!doctorResult.ready) {
2360
2439
  throw new VaultError(
2361
2440
  `System not ready: ${doctorResult.nextSteps.join("; ")}`
2362
2441
  );
2363
2442
  }
2364
2443
  }
2365
- const configDir = options?.configDir ?? getDefaultConfigDir();
2366
- const config = options?.config ?? await loadConfig(configDir);
2367
2444
  const keyManager = new KeyManager();
2368
2445
  await keyManager.init();
2369
2446
  const vault = new _VaultKeeper(config, keyManager, configDir);
2370
2447
  vault.#backend = vault.#resolveBackend();
2371
2448
  return vault;
2372
2449
  }
2373
- /** Run doctor checks without full initialization. */
2374
- static async doctor() {
2375
- return runDoctor();
2450
+ /**
2451
+ * Run doctor checks without full initialization.
2452
+ *
2453
+ * When called without arguments, uses conservative platform defaults —
2454
+ * all platform-native dependency checks are treated as required. Pass
2455
+ * `{ backends }` to scope checks to only the backends you plan to use.
2456
+ *
2457
+ * @param options - Optional doctor options (e.g. `{ backends }` to scope checks).
2458
+ */
2459
+ static async doctor(options) {
2460
+ return runDoctor(options);
2376
2461
  }
2377
2462
  /**
2378
- * Retrieve a secret from the backend and return a JWE token that encapsulates it.
2463
+ * Store a secret in the configured backend.
2464
+ *
2465
+ * This is a convenience method that delegates to the active backend's
2466
+ * `store()` method. If a secret with the same name already exists, it is
2467
+ * overwritten.
2468
+ *
2469
+ * @param name - Identifier for the secret.
2470
+ * @param value - The secret value to store.
2471
+ * @public
2472
+ */
2473
+ async store(name, value) {
2474
+ _VaultKeeper.#validateSecretName(name);
2475
+ const backend = this.#requireBackend();
2476
+ await backend.store(name, value);
2477
+ }
2478
+ /**
2479
+ * Delete a secret from the configured backend.
2480
+ *
2481
+ * This is a convenience method that delegates to the active backend's
2482
+ * `delete()` method.
2483
+ *
2484
+ * @param name - Identifier for the secret to delete.
2485
+ * @public
2486
+ */
2487
+ async delete(name) {
2488
+ _VaultKeeper.#validateSecretName(name);
2489
+ const backend = this.#requireBackend();
2490
+ await backend.delete(name);
2491
+ }
2492
+ /**
2493
+ * Read a stored secret from the backend and mint a JWE token that encapsulates it.
2379
2494
  *
2380
2495
  * @param secretName - Identifier for the secret
2381
2496
  * @param options - Setup options
2382
2497
  * @returns Compact JWE string
2383
2498
  */
2384
2499
  async setup(secretName, options) {
2500
+ _VaultKeeper.#validateSecretName(secretName);
2385
2501
  const backend = this.#requireBackend();
2386
2502
  const backendType = options?.backendType ?? backend.type;
2387
2503
  const ttlMinutes = options?.ttlMinutes ?? this.#config.defaults.ttlMinutes;
@@ -2426,7 +2542,10 @@ var VaultKeeper = class _VaultKeeper {
2426
2542
  * an opaque CapabilityToken.
2427
2543
  *
2428
2544
  * @param jwe - Compact JWE string from setup()
2429
- * @returns Opaque capability token for use with fetch/exec/getSecret
2545
+ * @returns Object containing an opaque {@link CapabilityToken} for use with
2546
+ * fetch/exec/getSecret, and a {@link VaultResponse} describing key status.
2547
+ * When the JWE was decrypted with a non-current key,
2548
+ * `vaultResponse.rotatedJwt` contains a re-encrypted JWE for the current key.
2430
2549
  */
2431
2550
  async authorize(jwe) {
2432
2551
  const kid = extractKid(jwe);
@@ -2446,13 +2565,13 @@ var VaultKeeper = class _VaultKeeper {
2446
2565
  }
2447
2566
  }
2448
2567
  const token = createCapabilityToken(claims);
2449
- const response = { keyStatus };
2568
+ const vaultResponse = { keyStatus };
2450
2569
  if (keyStatus === "previous") {
2451
2570
  const currentKey = this.#keyManager.getCurrentKey();
2452
2571
  const rotatedJwt = await createToken(currentKey.key, claims, { kid: currentKey.id });
2453
- response.rotatedJwt = rotatedJwt;
2572
+ vaultResponse.rotatedJwt = rotatedJwt;
2454
2573
  }
2455
- return { token, response };
2574
+ return { token, vaultResponse };
2456
2575
  }
2457
2576
  /**
2458
2577
  * Execute a delegated HTTP fetch, injecting the secret from the token.
@@ -2620,6 +2739,11 @@ var VaultKeeper = class _VaultKeeper {
2620
2739
  // ---------------------------------------------------------------------------
2621
2740
  // Private helpers
2622
2741
  // ---------------------------------------------------------------------------
2742
+ static #validateSecretName(name) {
2743
+ if (name.trim() === "") {
2744
+ throw new VaultError("Secret name must not be empty");
2745
+ }
2746
+ }
2623
2747
  #resolveBackend() {
2624
2748
  const enabledBackends = this.#config.backends.filter((b) => b.enabled);
2625
2749
  if (enabledBackends.length === 0) {
@@ -2678,15 +2802,18 @@ var VaultKeeper = class _VaultKeeper {
2678
2802
  }
2679
2803
  };
2680
2804
 
2805
+ exports.AccessorConsumedError = AccessorConsumedError;
2681
2806
  exports.AuthorizationDeniedError = AuthorizationDeniedError;
2682
2807
  exports.BackendLockedError = BackendLockedError;
2683
2808
  exports.BackendRegistry = BackendRegistry;
2684
2809
  exports.BackendUnavailableError = BackendUnavailableError;
2685
2810
  exports.CapabilityToken = CapabilityToken;
2686
2811
  exports.DeviceNotPresentError = DeviceNotPresentError;
2812
+ exports.ExecError = ExecError;
2687
2813
  exports.FilesystemError = FilesystemError;
2688
2814
  exports.IdentityMismatchError = IdentityMismatchError;
2689
2815
  exports.InvalidAlgorithmError = InvalidAlgorithmError;
2816
+ exports.InvalidTokenError = InvalidTokenError;
2690
2817
  exports.KeyRevokedError = KeyRevokedError;
2691
2818
  exports.KeyRotatedError = KeyRotatedError;
2692
2819
  exports.PluginNotFoundError = PluginNotFoundError;