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 +160 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -33
- package/dist/index.d.ts +112 -33
- package/dist/index.js +158 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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${
|
|
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
|
|
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({
|
|
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({
|
|
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({
|
|
2400
|
+
entries.push({
|
|
2401
|
+
check: checkSecretTool,
|
|
2402
|
+
required: enabledTypes === null || enabledTypes.has("secret-tool")
|
|
2403
|
+
});
|
|
2333
2404
|
}
|
|
2334
|
-
entries.push({
|
|
2335
|
-
|
|
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
|
-
/**
|
|
2374
|
-
|
|
2375
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
2572
|
+
vaultResponse.rotatedJwt = rotatedJwt;
|
|
2454
2573
|
}
|
|
2455
|
-
return { token,
|
|
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;
|