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.js CHANGED
@@ -139,6 +139,29 @@ var IdentityMismatchError = class extends VaultError {
139
139
  this.currentHash = currentHash;
140
140
  }
141
141
  };
142
+ var ExecError = class extends VaultError {
143
+ /**
144
+ * The command that failed to execute.
145
+ */
146
+ command;
147
+ constructor(message, command) {
148
+ super(message);
149
+ this.name = "ExecError";
150
+ this.command = command;
151
+ }
152
+ };
153
+ var InvalidTokenError = class extends VaultError {
154
+ constructor(message) {
155
+ super(message);
156
+ this.name = "InvalidTokenError";
157
+ }
158
+ };
159
+ var AccessorConsumedError = class extends VaultError {
160
+ constructor(message) {
161
+ super(message);
162
+ this.name = "AccessorConsumedError";
163
+ }
164
+ };
142
165
  var InvalidAlgorithmError = class extends VaultError {
143
166
  /**
144
167
  * The algorithm that was requested.
@@ -470,7 +493,17 @@ function execCommandFull(command, args, options) {
470
493
  resolve2({ stdout, stderr, exitCode: code ?? 1 });
471
494
  });
472
495
  proc.on("error", (error) => {
473
- reject(error);
496
+ if ("code" in error && error.code === "ENOENT") {
497
+ reject(
498
+ new PluginNotFoundError(
499
+ `'${command}' is not installed or not found in PATH`,
500
+ command,
501
+ ""
502
+ )
503
+ );
504
+ } else {
505
+ reject(error);
506
+ }
474
507
  });
475
508
  });
476
509
  }
@@ -1810,40 +1843,40 @@ async function decryptToken(key, jwe) {
1810
1843
  plaintext = result.plaintext;
1811
1844
  } catch (err) {
1812
1845
  const message = err instanceof Error ? err.message : String(err);
1813
- throw new VaultError(`JWE decryption failed: ${message}`);
1846
+ throw new InvalidTokenError(`JWE decryption failed: ${message}`);
1814
1847
  }
1815
1848
  let parsed;
1816
1849
  try {
1817
1850
  parsed = JSON.parse(new TextDecoder().decode(plaintext));
1818
1851
  } catch {
1819
- throw new VaultError("JWE payload is not valid JSON");
1852
+ throw new InvalidTokenError("JWE payload is not valid JSON");
1820
1853
  }
1821
1854
  const claims = parseVaultClaims(parsed);
1822
1855
  if (claims === void 0) {
1823
- throw new VaultError("JWE payload does not match VaultClaims schema");
1856
+ throw new InvalidTokenError("JWE payload does not match VaultClaims schema");
1824
1857
  }
1825
1858
  return claims;
1826
1859
  }
1827
1860
  function extractKid(jwe) {
1828
1861
  const parts = jwe.split(".");
1829
1862
  if (parts.length !== 5) {
1830
- throw new VaultError("Invalid JWE compact serialization: expected 5 parts");
1863
+ throw new InvalidTokenError("Invalid JWE compact serialization: expected 5 parts");
1831
1864
  }
1832
1865
  const headerSegment = parts[0];
1833
1866
  if (headerSegment === void 0 || headerSegment === "") {
1834
- throw new VaultError("Invalid JWE compact serialization: missing header segment");
1867
+ throw new InvalidTokenError("Invalid JWE compact serialization: missing header segment");
1835
1868
  }
1836
1869
  let headerJson;
1837
1870
  try {
1838
1871
  headerJson = Buffer.from(headerSegment, "base64url").toString("utf-8");
1839
1872
  } catch {
1840
- throw new VaultError("Invalid JWE compact serialization: header is not valid Base64URL");
1873
+ throw new InvalidTokenError("Invalid JWE compact serialization: header is not valid Base64URL");
1841
1874
  }
1842
1875
  let header;
1843
1876
  try {
1844
1877
  header = JSON.parse(headerJson);
1845
1878
  } catch {
1846
- throw new VaultError("Invalid JWE compact serialization: header is not valid JSON");
1879
+ throw new InvalidTokenError("Invalid JWE compact serialization: header is not valid JSON");
1847
1880
  }
1848
1881
  if (!isObject2(header)) {
1849
1882
  return void 0;
@@ -1958,6 +1991,12 @@ function replaceInRecord2(record, secret) {
1958
1991
  return result;
1959
1992
  }
1960
1993
  function delegatedExec(secret, request) {
1994
+ if (request.command.includes(PLACEHOLDER2)) {
1995
+ throw new ExecError(
1996
+ `The {{secret}} placeholder is not supported in the command field. Use args or env instead.`,
1997
+ request.command
1998
+ );
1999
+ }
1961
2000
  const args = (request.args ?? []).map((arg) => replacePlaceholder2(arg, secret));
1962
2001
  const env = request.env !== void 0 ? replaceInRecord2(request.env, secret) : void 0;
1963
2002
  return new Promise((resolve2, reject) => {
@@ -1983,7 +2022,22 @@ function delegatedExec(secret, request) {
1983
2022
  resolve2({ stdout, stderr, exitCode: code ?? 1 });
1984
2023
  });
1985
2024
  proc.on("error", (error) => {
1986
- reject(error);
2025
+ const isEnoent = error instanceof Error && "code" in error && error.code === "ENOENT";
2026
+ if (isEnoent) {
2027
+ reject(
2028
+ new ExecError(
2029
+ `Command not found: ${request.command}. Verify the command exists and is in PATH.`,
2030
+ request.command
2031
+ )
2032
+ );
2033
+ } else {
2034
+ reject(
2035
+ new ExecError(
2036
+ `Failed to execute command: ${request.command}. ${error instanceof Error ? error.message : String(error)}`,
2037
+ request.command
2038
+ )
2039
+ );
2040
+ }
1987
2041
  });
1988
2042
  });
1989
2043
  }
@@ -2002,7 +2056,7 @@ function createSecretAccessor(secretValue) {
2002
2056
  let consumed = false;
2003
2057
  function readImpl(callback) {
2004
2058
  if (consumed) {
2005
- throw new Error("SecretAccessor has already been consumed \u2014 call getSecret() again to obtain a new accessor");
2059
+ throw new AccessorConsumedError("SecretAccessor has already been consumed \u2014 call getSecret() again to obtain a new accessor");
2006
2060
  }
2007
2061
  consumed = true;
2008
2062
  const buf = Buffer.from(secretValue, "utf8");
@@ -2261,7 +2315,8 @@ async function runDoctor(options) {
2261
2315
  nextSteps: ["Unsupported platform. vaultkeeper supports macOS, Linux, and Windows."]
2262
2316
  };
2263
2317
  }
2264
- const entries = buildCheckList(platform);
2318
+ const enabledTypes = enabledBackendTypes(options?.backends);
2319
+ const entries = buildCheckList(platform, enabledTypes);
2265
2320
  const resolved = await Promise.all(
2266
2321
  entries.map(async ({ check, required }) => {
2267
2322
  const result = await check();
@@ -2275,16 +2330,15 @@ async function runDoctor(options) {
2275
2330
  const warnings = [];
2276
2331
  const nextSteps = [];
2277
2332
  for (const { required, result } of resolved) {
2333
+ const reasonSuffix = result.reason !== void 0 ? ` \u2014 ${result.reason}` : "";
2278
2334
  if (result.status === "missing") {
2279
2335
  if (required) {
2280
- nextSteps.push(`Install missing required dependency: ${result.name}`);
2336
+ nextSteps.push(`Install missing required dependency: ${result.name}${reasonSuffix}`);
2281
2337
  } else {
2282
- warnings.push(
2283
- `Optional dependency not found: ${result.name}${result.reason !== void 0 ? ` \u2014 ${result.reason}` : ""}`
2284
- );
2338
+ warnings.push(`Optional dependency not found: ${result.name}${reasonSuffix}`);
2285
2339
  }
2286
2340
  } else if (result.status === "version-unsupported") {
2287
- const msg = `${result.name} version is unsupported${result.reason !== void 0 ? `: ${result.reason}` : ""}`;
2341
+ const msg = `${result.name} version is unsupported${reasonSuffix}`;
2288
2342
  if (required) {
2289
2343
  nextSteps.push(`Upgrade required dependency: ${msg}`);
2290
2344
  } else {
@@ -2295,19 +2349,42 @@ async function runDoctor(options) {
2295
2349
  const checks = resolved.map(({ result }) => result);
2296
2350
  return { checks, ready, warnings, nextSteps };
2297
2351
  }
2298
- function buildCheckList(platform) {
2352
+ function enabledBackendTypes(backends) {
2353
+ if (backends === void 0) return null;
2354
+ const types = /* @__PURE__ */ new Set();
2355
+ for (const b of backends) {
2356
+ if (b.enabled) types.add(b.type);
2357
+ }
2358
+ return types;
2359
+ }
2360
+ function buildCheckList(platform, enabledTypes) {
2299
2361
  const entries = [{ check: checkOpenssl, required: true }];
2300
2362
  if (platform === "darwin") {
2301
- entries.push({ check: checkSecurity, required: true });
2363
+ entries.push({
2364
+ check: checkSecurity,
2365
+ required: enabledTypes === null || enabledTypes.has("keychain")
2366
+ });
2302
2367
  entries.push({ check: checkBash, required: false });
2303
2368
  } else if (platform === "win32") {
2304
- entries.push({ check: checkPowershell, required: true });
2369
+ entries.push({
2370
+ check: checkPowershell,
2371
+ required: enabledTypes === null || enabledTypes.has("dpapi")
2372
+ });
2305
2373
  } else {
2306
2374
  entries.push({ check: checkBash, required: true });
2307
- entries.push({ check: checkSecretTool, required: true });
2375
+ entries.push({
2376
+ check: checkSecretTool,
2377
+ required: enabledTypes === null || enabledTypes.has("secret-tool")
2378
+ });
2308
2379
  }
2309
- entries.push({ check: checkOp, required: false });
2310
- entries.push({ check: checkYkman, required: false });
2380
+ entries.push({
2381
+ check: checkOp,
2382
+ required: enabledTypes?.has("1password") ?? false
2383
+ });
2384
+ entries.push({
2385
+ check: checkYkman,
2386
+ required: enabledTypes?.has("yubikey") ?? false
2387
+ });
2311
2388
  return entries;
2312
2389
  }
2313
2390
 
@@ -2329,34 +2406,73 @@ var VaultKeeper = class _VaultKeeper {
2329
2406
  * Runs doctor checks (unless skipped), loads config, and sets up the key manager.
2330
2407
  */
2331
2408
  static async init(options) {
2409
+ const configDir = options?.configDir ?? getDefaultConfigDir();
2410
+ const config = options?.config ?? await loadConfig(configDir);
2332
2411
  if (options?.skipDoctor !== true) {
2333
- const doctorResult = await runDoctor();
2412
+ const doctorResult = await runDoctor({ backends: config.backends });
2334
2413
  if (!doctorResult.ready) {
2335
2414
  throw new VaultError(
2336
2415
  `System not ready: ${doctorResult.nextSteps.join("; ")}`
2337
2416
  );
2338
2417
  }
2339
2418
  }
2340
- const configDir = options?.configDir ?? getDefaultConfigDir();
2341
- const config = options?.config ?? await loadConfig(configDir);
2342
2419
  const keyManager = new KeyManager();
2343
2420
  await keyManager.init();
2344
2421
  const vault = new _VaultKeeper(config, keyManager, configDir);
2345
2422
  vault.#backend = vault.#resolveBackend();
2346
2423
  return vault;
2347
2424
  }
2348
- /** Run doctor checks without full initialization. */
2349
- static async doctor() {
2350
- return runDoctor();
2425
+ /**
2426
+ * Run doctor checks without full initialization.
2427
+ *
2428
+ * When called without arguments, uses conservative platform defaults —
2429
+ * all platform-native dependency checks are treated as required. Pass
2430
+ * `{ backends }` to scope checks to only the backends you plan to use.
2431
+ *
2432
+ * @param options - Optional doctor options (e.g. `{ backends }` to scope checks).
2433
+ */
2434
+ static async doctor(options) {
2435
+ return runDoctor(options);
2351
2436
  }
2352
2437
  /**
2353
- * Retrieve a secret from the backend and return a JWE token that encapsulates it.
2438
+ * Store a secret in the configured backend.
2439
+ *
2440
+ * This is a convenience method that delegates to the active backend's
2441
+ * `store()` method. If a secret with the same name already exists, it is
2442
+ * overwritten.
2443
+ *
2444
+ * @param name - Identifier for the secret.
2445
+ * @param value - The secret value to store.
2446
+ * @public
2447
+ */
2448
+ async store(name, value) {
2449
+ _VaultKeeper.#validateSecretName(name);
2450
+ const backend = this.#requireBackend();
2451
+ await backend.store(name, value);
2452
+ }
2453
+ /**
2454
+ * Delete a secret from the configured backend.
2455
+ *
2456
+ * This is a convenience method that delegates to the active backend's
2457
+ * `delete()` method.
2458
+ *
2459
+ * @param name - Identifier for the secret to delete.
2460
+ * @public
2461
+ */
2462
+ async delete(name) {
2463
+ _VaultKeeper.#validateSecretName(name);
2464
+ const backend = this.#requireBackend();
2465
+ await backend.delete(name);
2466
+ }
2467
+ /**
2468
+ * Read a stored secret from the backend and mint a JWE token that encapsulates it.
2354
2469
  *
2355
2470
  * @param secretName - Identifier for the secret
2356
2471
  * @param options - Setup options
2357
2472
  * @returns Compact JWE string
2358
2473
  */
2359
2474
  async setup(secretName, options) {
2475
+ _VaultKeeper.#validateSecretName(secretName);
2360
2476
  const backend = this.#requireBackend();
2361
2477
  const backendType = options?.backendType ?? backend.type;
2362
2478
  const ttlMinutes = options?.ttlMinutes ?? this.#config.defaults.ttlMinutes;
@@ -2401,7 +2517,10 @@ var VaultKeeper = class _VaultKeeper {
2401
2517
  * an opaque CapabilityToken.
2402
2518
  *
2403
2519
  * @param jwe - Compact JWE string from setup()
2404
- * @returns Opaque capability token for use with fetch/exec/getSecret
2520
+ * @returns Object containing an opaque {@link CapabilityToken} for use with
2521
+ * fetch/exec/getSecret, and a {@link VaultResponse} describing key status.
2522
+ * When the JWE was decrypted with a non-current key,
2523
+ * `vaultResponse.rotatedJwt` contains a re-encrypted JWE for the current key.
2405
2524
  */
2406
2525
  async authorize(jwe) {
2407
2526
  const kid = extractKid(jwe);
@@ -2421,13 +2540,13 @@ var VaultKeeper = class _VaultKeeper {
2421
2540
  }
2422
2541
  }
2423
2542
  const token = createCapabilityToken(claims);
2424
- const response = { keyStatus };
2543
+ const vaultResponse = { keyStatus };
2425
2544
  if (keyStatus === "previous") {
2426
2545
  const currentKey = this.#keyManager.getCurrentKey();
2427
2546
  const rotatedJwt = await createToken(currentKey.key, claims, { kid: currentKey.id });
2428
- response.rotatedJwt = rotatedJwt;
2547
+ vaultResponse.rotatedJwt = rotatedJwt;
2429
2548
  }
2430
- return { token, response };
2549
+ return { token, vaultResponse };
2431
2550
  }
2432
2551
  /**
2433
2552
  * Execute a delegated HTTP fetch, injecting the secret from the token.
@@ -2595,6 +2714,11 @@ var VaultKeeper = class _VaultKeeper {
2595
2714
  // ---------------------------------------------------------------------------
2596
2715
  // Private helpers
2597
2716
  // ---------------------------------------------------------------------------
2717
+ static #validateSecretName(name) {
2718
+ if (name.trim() === "") {
2719
+ throw new VaultError("Secret name must not be empty");
2720
+ }
2721
+ }
2598
2722
  #resolveBackend() {
2599
2723
  const enabledBackends = this.#config.backends.filter((b) => b.enabled);
2600
2724
  if (enabledBackends.length === 0) {
@@ -2653,6 +2777,6 @@ var VaultKeeper = class _VaultKeeper {
2653
2777
  }
2654
2778
  };
2655
2779
 
2656
- export { AuthorizationDeniedError, BackendLockedError, BackendRegistry, BackendUnavailableError, CapabilityToken, DeviceNotPresentError, FilesystemError, IdentityMismatchError, InvalidAlgorithmError, KeyRevokedError, KeyRotatedError, PluginNotFoundError, RotationInProgressError, SecretNotFoundError, SetupError, TokenExpiredError, TokenRevokedError, UsageLimitExceededError, VaultError, VaultKeeper, isListableBackend, runDoctor };
2780
+ export { AccessorConsumedError, AuthorizationDeniedError, BackendLockedError, BackendRegistry, BackendUnavailableError, CapabilityToken, DeviceNotPresentError, ExecError, FilesystemError, IdentityMismatchError, InvalidAlgorithmError, InvalidTokenError, KeyRevokedError, KeyRotatedError, PluginNotFoundError, RotationInProgressError, SecretNotFoundError, SetupError, TokenExpiredError, TokenRevokedError, UsageLimitExceededError, VaultError, VaultKeeper, isListableBackend, runDoctor };
2657
2781
  //# sourceMappingURL=index.js.map
2658
2782
  //# sourceMappingURL=index.js.map