Skip to content

Releases: mike-north/vaultkeeper

vaultkeeper@0.8.0

Choose a tag to compare

@github-actions github-actions released this 23 Jul 01:19
87c615f

Minor Changes

  • #317 5231999 Thanks @mike-north! - Add PresenceSimulatorBackend to @vaultkeeper/test-helpers: a test-only backend that scripts vaultkeeper's presence signal (including its absence) so a consumer can prove in CI that an automation signer attempting a presence-gated operation is refused. Per-operation outcomes are scriptable across 'grant' / 'refuse' / 'timeout' / 'not-capable' via forTesting({ operations }), or armed one call at a time via armPresence to prove presence is demanded fresh on every call. Three stacked guards keep it unreachable from production: it is never registered with the backend registry, has no default constructor, and its forTesting() factory throws a new TestDoubleMisuseError (exported from vaultkeeper) when NODE_ENV is 'production'.

Patch Changes

  • #323 8be4d18 Thanks @mike-north! - Harden the Linux secret-tool backend: a -- separator now precedes every positional attribute/id argument so ids beginning with dashes cannot be parsed as flags; not-found detection depends solely on the exit code, and retrieve() strips exactly one trailing newline instead of trimming, so empty and whitespace-only secret values round-trip byte-for-byte.

@vaultkeeper/test-helpers@0.4.0

Choose a tag to compare

@github-actions github-actions released this 23 Jul 01:23
87c615f

Minor Changes

  • #317 5231999 Thanks @mike-north! - Add deterministic fault injection (FaultPlan, InMemoryBackend.injectFault/clearFault/clearAllFaults) and TestVault.signCeremony for exercising a consumer's error-handling paths and full signing-ceremony flow without hardware.

  • #317 5231999 Thanks @mike-north! - Add PresenceSimulatorBackend to @vaultkeeper/test-helpers: a test-only backend that scripts vaultkeeper's presence signal (including its absence) so a consumer can prove in CI that an automation signer attempting a presence-gated operation is refused. Per-operation outcomes are scriptable across 'grant' / 'refuse' / 'timeout' / 'not-capable' via forTesting({ operations }), or armed one call at a time via armPresence to prove presence is demanded fresh on every call. Three stacked guards keep it unreachable from production: it is never registered with the backend registry, has no default constructor, and its forTesting() factory throws a new TestDoubleMisuseError (exported from vaultkeeper) when NODE_ENV is 'production'.

@vaultkeeper/cli@0.2.2

Choose a tag to compare

@github-actions github-actions released this 23 Jul 01:19
87c615f

Patch Changes

vaultkeeper@0.7.1

Choose a tag to compare

@github-actions github-actions released this 22 Jul 00:39
ba0d3c8

Patch Changes

  • #287 df3ed7b Thanks @mike-north! - Internal: validateClaims/validate_claims — the single validation chokepoint every token passes through, in both the TypeScript library and the Rust core — now discriminate on a claims payload's kind. An ordinary secret claim still requires a non-empty val and bkd exactly as before; a session signing-key lease (no secret value) instead requires a non-empty kid and a present kgen (never defaulted to generation 0). No public API changed — VaultClaims remains an internal type, and every existing secret-token code path is unchanged.

@vaultkeeper/wasm@0.4.0

Choose a tag to compare

@github-actions github-actions released this 22 Jul 00:39
ba0d3c8

Minor Changes

  • #259 3163724 Thanks @mike-north! - Phase 0 bridge contracts for the consolidation effort (issue #239): the Rust HostPlatform trait and its WASM/JS bridge gain the primitives a host-implemented backend and delegated network access will need in later phases.

    HostPlatform::exec now accepts an ExecOptions bundle (stdin, env, cwd) instead of a bare stdin argument — @vaultkeeper/wasm's WasmHostPlatform.exec mirrors this with an optional third options argument, and createNodeHost() implements env as { ...process.env, ...options.env } and cwd via child_process.execFile's own cwd option. Omitting options (or any of its fields) reproduces the exact pre-#239 behavior — no existing caller's behavior changes.

    A new HostPlatform::http_fetch primitive lands with its @vaultkeeper/wasm counterpart WasmHostPlatform.httpFetch, implemented in createNodeHost() over the global fetch. No core consumer calls it yet — the delegated-access port in a later issue is the first real caller — but the primitive is fully wired end-to-end and covered by direct tests today.

    A new optional HostPlatform::prompt_approval capability lets a host offer interactive human approval for a sensitive action; an absent implementation (the default on every existing host) fails closed (false) rather than auto-approving. @vaultkeeper/wasm exposes this as the optional WasmHostPlatform.promptApproval method.

    @vaultkeeper/wasm also publishes a new HostSecretBackend contract type — the shape a JS/TS-implemented secret backend must satisfy to be driven by the Rust core — backed by a new JsSecretBackend scaffold in crates/vaultkeeper-wasm that dispatches store/retrieve/delete/exists/list over JS callbacks (all-async, Uint8Array at the boundary, never Buffer). Registry dispatch and the capability/signing methods on the contract (getCapabilities, generateSigningKey, getPublicKey, signWithKey) are forward-looking — not yet wired to Rust — pending the capability trait (issue #242) and signing trait (issue #237).

  • #284 d88a17f Thanks @mike-north! - Additive handle-based capability surface for the authorize() result (issue #241): WasmAuthorization gains a handleId getter exposing the underlying core capability handle id, and WasmVaultKeeper gains resolveSecretClaims(handleId) and releaseHandle(handleId).

    authorize()'s existing public shape (claims, response, secretAvailable, readSecret()) is unchanged and continues to work exactly as before — internally it now mints a core-side HandleTable entry, performs the one-time read_secret against it immediately, and caches the result on the returned WasmAuthorization, so claims no longer carries the raw secret (val) across the WASM boundary at all; the secret was already redacted from the observable claims shape before this change, and still is.

    resolveSecretClaims(handleId) lets a caller re-fetch the same non-secret claims later from the retained handle (refusing a signing-key handle with AuthorizationDenied), and releaseHandle(handleId) lets a caller evict the handle explicitly once done with it rather than waiting on expiry or the table's FIFO size cap. These are the primitives a future handle-based engine swap builds on directly instead of the eager authorize() wrapper; no existing caller needs to change.

  • #286 653f4af Thanks @mike-north! - Introduce the environment profile primitive in vaultkeeper-core (issue #277): serde schema, a fail-closed loader, and profile init/show/list/lint in the Rust CLI. Profiles are named, declarative binding sets (env-var name → secret source → materialization mode → policy) stored at $CONFIG_DIR/profiles/<name>.json, never inside config.json.

    @vaultkeeper/wasm gains the MaterializeModeUnsupportedError typed error class (and its materialize-mode-unsupported error code), thrown when a profile's materialize field uses the reserved-but-not-yet-implemented object form ({ "mode": "reference", ... }).

  • #252 3cf4d25 Thanks @mike-north! - Port encrypted key-state persistence (keys.enc + .keys.wrap) to the Rust core, closing the parity gap where the WASM SDK's KeyManager was memory-only. A JWE minted by one process (or VaultKeeper instance) is now authorized by a later one sharing the same config directory, and the rotation grace-period guard (RotationInProgressError) now survives a restart instead of resetting.

    The on-disk format is byte-for-byte compatible with the pure-TypeScript vaultkeeper library's existing keys/storage.ts: a store written by either implementation loads correctly in the other.

    Breaking (0.x): rotateKey() and revokeKey() are now async (Promise<void> instead of void), since persisting the new key state requires an I/O call. Versioned as a minor bump under 0.x semver (breaking changes ship as minor bumps while the SDK is pre-1.0).

    // Before — synchronous:
    vault.rotateKey()
    
    // After — await the persisted rotation:
    await vault.rotateKey()

    The WasmHostPlatform interface consumed by createNodeHost() also gains a renameFile(from, to) method, used for atomic write-then-rename persistence.

Patch Changes

  • #251 5f1f370 Thanks @mike-north! - Complete the VaultError taxonomy so it can bridge losslessly to @vaultkeeper/wasm. The Rust core gains 14 new VaultError variants (NotCapable, PresenceDeclined, PresenceTimeout, InvalidKeyMaterial, SigningKeyNotFound, SigningKeyAlreadyExists, SigningNotSupported, Exec, Fetch, InvalidToken, AccessorConsumed, ConfigValidation, UnknownBackendType, ConfigParse) with machine-readable context fields matching the pure-TypeScript vaultkeeper library's error classes.

    @vaultkeeper/wasm now exports the matching typed error classes — NotCapableError, PresenceDeclinedError, PresenceTimeoutError, InvalidKeyMaterialError, SigningKeyNotFoundError, SigningKeyAlreadyExistsError, SigningNotSupportedError, ExecError, FetchError, ConfigValidationError, UnknownBackendTypeError, ConfigParseError — plus BackendLockedError, DeviceNotPresentError, AuthorizationDeniedError, BackendUnavailableError, PluginNotFoundError, InvalidAlgorithmError, and SetupError, which had Rust-side variants already but were never reconstructed at the WASM boundary and previously collapsed to the generic VaultError base class.

    The error-code table that drives the boundary (vaultErrorCode) is now a single source of truth shared by the Rust match (vault_error_code/vault_error_fields in vaultkeeper-core) and the TypeScript reconstruction map (ALL_VAULT_ERROR_CODES in @vaultkeeper/wasm's errors.ts), with a parity test asserting both sides list exactly the same codes and that every code round-trips to the correct typed subclass with the correct field values. No existing error path changed behavior.

  • #287 df3ed7b Thanks @mike-north! - Internal: validateClaims/validate_claims — the single validation chokepoint every token passes through, in both the TypeScript library and the Rust core — now discriminate on a claims payload's kind. An ordinary secret claim still requires a non-empty val and bkd exactly as before; a session signing-key lease (no secret value) instead requires a non-empty kid and a present kgen (never defaulted to generation 0). No public API changed — VaultClaims remains an internal type, and every existing secret-token code path is unchanged.

  • #253 c230593 Thanks @mike-north! - Rebuild the committed WASM binary with explicit wasm-opt -Oz optimization (previously relying on wasm-pack's implicit default). No runtime API changes — the artifact is smaller, not different in behavior.

  • #250 4fdbe31 Thanks @mike-north! - Fix the Rust core's zero-config default backend to be file on every platform, matching the vaultkeeper (TS) package's #98 fix.

    Previously, when no config.json existed, the Rust core (and therefore @vaultkeeper/wasm, which wraps it) fell back to a platform-native backend — keychain on macOS, dpapi on Windows — instead of the portable, self-contained AES-256-GCM encrypted file backend. This silently wrote secrets into the real OS keychain/credential store for any consumer that never wrote an explicit config, reintroducing the exact regression #98 fixed on the TypeScript side. Explicit configuration that selects keychain/dpapi is unaffected; only the zero-config fall...

Read more

@vaultkeeper/cli@0.2.1

Choose a tag to compare

@github-actions github-actions released this 22 Jul 00:39
ba0d3c8

Patch Changes

  • Updated dependencies [df3ed7b]:
    • vaultkeeper@0.7.1

vaultkeeper@0.7.0

Choose a tag to compare

@github-actions github-actions released this 16 Jul 13:26
7000f95

Minor Changes

  • #89 46df0b0 Thanks @mike-north! - Make backend selection visible and overridable from the CLI and introspectable from the library.

    • vaultkeeper config init --backend <type> now writes a config whose first enabled backend is <type>. Valid values are the registered backend types; an unknown value exits 2 and lists the valid types.
    • Any unknown flag on a config subcommand (config init, config show) now exits 2 with an "unknown option" error instead of being silently ignored — a typo can no longer send secrets to an unintended credential store.
    • config init output now states which backend was configured and how to change it. config show reports the resolved active backend (first enabled).
    • New public platformNativeBackendType() reports the OS-native credential store for the current platform (keychain on macOS, dpapi on Windows, secret-tool on Linux, file on other platforms) — the store you can opt into with --backend.
    • New public VaultKeeper.activeBackendType getter exposes the type of the active (first enabled) backend at runtime.
  • #92 75685ac Thanks @mike-north! - Add a global --config-dir <path> flag / VAULTKEEPER_CONFIG_DIR environment variable to the CLI so every command (store, delete, exec, approve, dev-mode, doctor, config, rotate-key, revoke-key) can be pointed at an isolated config directory — the flag wins over the env var, which wins over the platform default. config init creates the override directory as needed, and config show reports the path it loaded from. The library's getDefaultConfigDir() and loadConfig() are now public so embedders and the CLI share the same resolution logic. @vaultkeeper/cli-test-helpers's createCliTestEnv() gains a configDirMode option ('env' | 'flag') and no longer manipulates the subprocess's HOME directory to achieve isolation.

  • #160 90a4127 Thanks @mike-north! - Fixed CLI error output so recovery hints repair the file they diagnose and read cleanly.

    • The invalid-config recovery hint now carries an explicit --config-dir '<dir>' whenever a non-default config directory is active (from --config-dir or VAULTKEEPER_CONFIG_DIR), so the copy-pasted vaultkeeper config init --force … command repairs the exact diagnosed file instead of writing a fresh config to the platform default and leaving the corrupt file untouched. The default-directory case stays bare (no path is leaked). A new getPlatformDefaultConfigDir() export computes the machine default independent of VAULTKEEPER_CONFIG_DIR, so a directory that came only from the environment variable still gets an explicit flag (a fresh shell running the pasted command won't have that variable set); getDefaultConfigDir() now delegates to it.
    • FilesystemError now renders a human message from its typed path/permission fields — plainly stating whether the file is missing or permission-denied, with a suggested next step — instead of leaking the raw Node ENOENT: … open '<path>' text. The typed class and its fields are unchanged.
    • doctor prints the config remediation exactly once (under "Next steps") instead of duplicating it inline on the failing config check.
  • #95 4ebfa5d Thanks @mike-north! - Uniform CLI exit-code taxonomy (0 success / 1 runtime failure / 2 usage error) applied everywhere: a top-level typo like vaultkeeper --bogus now exits 2 with an error instead of silently exiting 0, and an unrecognized flag on store, delete, exec, approve, dev-mode, or doctor now exits 2 instead of a bare fatal error (exit 1).

    store (and delete, for consistency) now reject an empty or whitespace-only --name with exit 2 and the same error style as a missing flag, instead of persisting a near-unreachable secret or surfacing a generic runtime error. Allowed --name characters (letters, digits, ., _, -, /) are documented in --help.

    exec now validates that the secret exists before the caller-approval/TTY gate, so exec --secret <nonexistent> ... reports a clear SecretNotFoundError regardless of TTY, instead of being masked by the generic "requires interactive approval" message. This is backed by a new public VaultKeeper.secretExists(name) method — a side-effect-free existence check that never touches the TOFU trust manifest.

    config init --help and config show --help now print help for that subcommand instead of the parent config help. exec --help includes a worked --caller example.

  • #94 0ca9d3f Thanks @mike-north! - doctor and config show now detect an invalid config file instead of silently ignoring it. doctor validates the config file (when present) as part of its preflight checks and reports a failing config check with the parse/validation error and file path, exiting non-zero. config show on invalid JSON now exits non-zero with the parse error (including a line/column location when available) instead of dumping the raw file with exit 0. Every config parse/validation error raised by loadConfig() — surfaced through store, delete, exec, config show, and doctor alike — now includes the config file path, the parse location where available, and a remediation hint naming vaultkeeper config init.

    loadConfig() now falls back to platform defaults only when the config file is missing (ENOENT). A present-but-unreadable file (e.g. a permissions error) is rethrown as a typed FilesystemError instead of being silently treated as "no config" — a genuinely broken config was previously invisible to doctor and config show.

    The "no config file" story is now uniform across store, delete, exec, config show, and doctor: each falls back to platform defaults and prints a one-line notice naming the resolved backend and vaultkeeper config init (e.g. No config file found; using platform defaults (keychain). Run 'vaultkeeper config init' to persist one.). Previously config show errored with exit 1 on a missing config file while the other commands defaulted silently; config show now defaults and reports it like the rest.

    New public ConfigParseError (with path and location fields) is thrown on invalid config JSON. ConfigValidationError gains an optional configFilePath field. PreflightCheckStatus gains an 'invalid' value, and RunDoctorOptions gains an optional configDir field that lets runDoctor/VaultKeeper.doctor() load and validate the config itself.

  • #120 b270562 Thanks @mike-north! - Make @1password/sdk an optional peer dependency instead of a runtime dependency. Installing vaultkeeper no longer pulls @1password/sdk (and its @1password/sdk-core transitive) into the dependency closure — the file-backend path stays jose-only. The 1Password backend now loads the SDK lazily (via dynamic import()) only when that backend is actually used, and fails with a typed PluginNotFoundError naming the missing @1password/sdk peer when it is not installed. To use the 1Password backend, install @1password/sdk alongside vaultkeeper.

  • #126 cfcd61b Thanks @mike-north! - Fix dev-mode invalid-action misdiagnosis and audit config.ts/the file backend for plain Error throws.

    • vaultkeeper dev-mode <action> --script <path> now distinguishes an invalid action from missing args: an unrecognized action (e.g. banana) emits unknown action "<x>" (expected "enable" or "disable") (exit 2), while missing action or --script flag is reserved for genuinely absent arguments.
    • The encrypted-file secret backend (FileBackend) now surfaces EACCES/permission failures reading, writing, or deleting a secret entry as a typed FilesystemError instead of the raw Node.js error.
    • Added a new DecryptionError (extends VaultError) for when a stored secret entry fails to decrypt (corrupted ciphertext or a failed AES-GCM auth tag check) — previously thrown as a plain Error.
  • #145 f5edcd9 Thanks @mike-north! - Give the doctor config preflight check structured error context so the CLI can render a CLI-native remediation instead of the library's install text.

    • The public PreflightCheck shape gains an optional error field (PreflightCheckError: kind + configPath + optional parse location) carrying remediation-free, machine-readable context when the config check fails on a present-but-invalid config file. A consumer can build its own audience-appropriate remediation from these fields instead of parsing the human-readable reason prose.
    • `vaultkeeper doct...
Read more

@vaultkeeper/wasm@0.3.0

Choose a tag to compare

@github-actions github-actions released this 16 Jul 13:26
7000f95

Minor Changes

  • #121 7c8ab85 Thanks @mike-north! - Scope doctor to the active/configured backend so a fresh install no longer looks broken. Previously doctor rendered every non-'ok' check with a failing icon, including plugin-backend tools (ykman, op) that weren't configured — on the post-#98 file-default install, this meant the very first doctor run showed a failing check for a YubiKey/1Password tool the user never opted into.

    PreflightResult.checks entries are now ScopedPreflightCheck (a PreflightCheck plus required: boolean), reflecting whether each dependency is required for the active/configured backend(s). The CLI only renders the icon for checks that are both required and failing; unmet optional checks still surface, without the failure icon, in the Warnings section. Opt-in backends still get their dependency checks promoted to required when configured (e.g. --backend yubikey requires ykman).

  • #161 b37bfd7 Thanks @mike-north! - Breaking (0.x): @vaultkeeper/wasm's setup() now requires an explicit executable-trust choice, closing a security-parity gap with the TypeScript vaultkeeper library. Versioned as a minor bump under 0.x semver (breaking changes ship as minor bumps while the SDK is pre-1.0).

    Previously the WASM SDK's setup() defaulted the executable identity to the 'dev' sentinel when executablePath was omitted, so a bare vault.setup(name, value) silently minted an unverified token — the same permissive default that VaultKeeper.setup() in the pure-TypeScript library retired. The two SDKs now share the same explicit-choice contract.

    Callers must now provide exactly one of:

    • executablePath — the calling executable's real path, bound into the minted token, or
    • skipTrust: true — a self-describing, greppable, development-only opt-out that deliberately skips the binding.

    Supplying neither — or both — or the retired 'dev' sentinel as executablePath now throws the new typed ExecutableTrustRequiredError (a VaultError subclass, exported from the package root) instead of silently minting an unverified token. Its reason field is 'missing-choice', 'conflicting-choice', or 'legacy-dev-sentinel', matching the library's ExecutableTrustRequiredError.

    Migration — every existing setup() call must now name a trust choice:

    // Before — unverified by default (silent skip):
    vault.setup('MY_API_KEY', 'my-secret-value')
    
    // After — bind the calling executable (production):
    vault.setup('MY_API_KEY', 'my-secret-value', { executablePath: process.argv[1] })
    
    // After — deliberately skip the binding (development/tests only):
    vault.setup('MY_API_KEY', 'my-secret-value', { skipTrust: true })

    Callers that passed the 'dev' sentinel as executablePath must switch to skipTrust: true; the legacy sentinel is now rejected at runtime with ExecutableTrustRequiredError (reason: 'legacy-dev-sentinel').

  • #154 fc544ae Thanks @mike-north! - Fix the WASM SDK's JS host bridge erasing filesystem errno codes: a permission-denied read or delete previously surfaced as a generic VaultError, indistinguishable from any other failure, instead of a typed error a caller could branch on.

    readFile/deleteFile/fileExists in the Node host bridge (createNodeHost) now reject with a structured { message, path, code } contract that JsHostPlatform (the Rust side of the bridge) reads back to build a typed VaultError::Filesystem, mirroring the native CLI host's classification: a genuine "does not exist" still resolves to SecretNotFoundError, while permission and other errno failures now surface as a new public FilesystemError (with path, permission, and code fields — code carries the underlying errno, e.g. EACCES, when available). Exported from @vaultkeeper/wasm alongside the rest of the typed error hierarchy.

  • #85 b8262d9 Thanks @mike-north! - Stop authorize() from returning the raw secret and add typed errors.

    authorize() no longer exposes the plaintext secret on its result: the returned
    claims no longer carry val. The secret is now read through a one-time
    SecretAccessor on result.secret (secret.read((value) => ...)), mirroring the
    createSecretAccessor pattern in the TypeScript library — the value is available
    exactly once and is never part of the default return shape.

    The SDK now exports a typed error hierarchy aligned with VaultError
    (SecretNotFoundError, InvalidTokenError, TokenExpiredError, KeyRotatedError,
    KeyRevokedError, TokenRevokedError, UsageLimitExceededError,
    RotationInProgressError, AccessorConsumedError), and thrown errors are real
    instances of these classes so err instanceof VaultError holds across the ecosystem.

    This is a breaking change to the authorize() return shape: code that read
    result.claims.val must switch to result.secret.read(...).

  • #178 09c48d7 Thanks @mike-north! - Enforce executable-trust verification in setup() when an executablePath is supplied.

    Previously, passing executablePath bound the raw path into the token's exe claim with no hashing and no trust-manifest consultation — a caller that explicitly asked for executable trust got none. setup() now hashes the executable and runs trust-on-first-use verification (Sigstore → trust-manifest match → TOFU first-encounter) through the host bridge, binding the verified hash into the exe claim, matching the pure-TypeScript vaultkeeper library's behavior.

    • A first encounter records the executable's hash under trust-on-first-use. A later setup() with a matching hash passes; a changed hash throws the new IdentityMismatchError (carrying previousHash / currentHash) rather than silently re-approving.
    • The first-encounter manifest write is committed only after the token has been minted, so a failed setup() never leaves a premature trust record behind.
    • skipTrust: true is unchanged — it still opts out of verification and mints a 'dev'-bound token.

    Behavior change: setup() is now async and returns Promise<string> (it performs executable hashing and manifest I/O). Callers must await it. Supplying executablePath now performs real verification and can throw IdentityMismatchError.

  • #203 9b3e193 Thanks @mike-north! - Enforce the setup() executable-trust choice at compile time in @vaultkeeper/wasm, matching the TypeScript vaultkeeper library. Versioned as a minor bump under 0.x semver (a tightened type contract is a compile break for callers omitting the choice, but ships as a minor while the SDK is pre-1.0).

    Previously setup()'s options argument was typed as optional (options?: SetupOptions) with all-optional fields, so a two-argument vault.setup(name, value) — or vault.setup(name, value, {}) — type-checked cleanly yet threw ExecutableTrustRequiredError (reason: 'missing-choice') at runtime. WASM users got no compile-time protection on the very trust choice the rest of the ecosystem type-enforces.

    SetupOptions is now SetupOptionsBase (ttlMinutes / useLimit / backendType) intersected with a discriminated union requiring exactly one of executablePath or skipTrust: true, and the options argument is required. As a result these are now compile errors instead of runtime-only failures:

    vault.setup('MY_API_KEY', 'my-secret-value') // missing choice
    vault.setup('MY_API_KEY', 'my-secret-value', {}) // missing choice
    vault.setup('MY_API_KEY', 'my-secret-value', { executablePath: p, skipTrust: true }) // both

    The valid single-choice forms are unchanged:

    vault.setup('MY_API_KEY', 'my-secret-value', { executablePath: process.argv[1] })
    vault.setup('MY_API_KEY', 'my-secret-value', { skipTrust: true })

    The runtime ExecutableTrustRequiredError remains as a backstop for untyped (plain-JavaScript) callers.

Patch Changes

  • #205 2086c0a Thanks @mike-north! - Fix the WASM SDK failing to read a config directory produced by the documented vaultkeeper config init flow. config init (and the README example) writes defaults.trustTier as a bare JSON number (3), but the Rust-core config reader behind the SDK required a string-encoded number, so createVaultKeeper() threw VaultError: Failed to parse config on a CLI-produced config.

    The core config reader now accepts trustTier as either a bare number (3) or a string-encoded number ("3"), and writes the bare-number form — aligning the native CLI output, the TS CLI, the TS library, and the README on one canonical wire form while remaining backward compatible with existing string-form configs. The tid claim in ...

Read more

@vaultkeeper/test-helpers@0.3.0

Choose a tag to compare

@github-actions github-actions released this 16 Jul 13:26
7000f95

Minor Changes

  • #131 f2fe1d2 Thanks @mike-north! - Breaking (0.x): VaultKeeper.setup() now requires an explicit executable-trust choice. Versioned as a minor bump under 0.x semver (breaking changes ship as minor bumps while vaultkeeper is pre-1.0).

    setup() previously defaulted executablePath to 'dev' when omitted, which silently skipped Trust On First Use (TOFU) executable-identity verification — so a bare await vault.setup(name) minted an unverified token even though the caller may have believed trust was enforced. This was a permissive security default for a secrets tool. Existing setup(name) calls must now pass executablePath (runs TOFU verification) or skipTrust: true (development-only opt-out); omitting both throws ExecutableTrustRequiredError.

    setup() now requires the caller to make the decision explicitly. Provide exactly one of:

    • executablePath — the calling executable's real path, which runs TOFU verification (the safe, production choice), or
    • skipTrust: true — a self-describing, greppable, development-only opt-out that deliberately skips verification.

    Supplying neither — or both — now throws the new typed ExecutableTrustRequiredError (a VaultError subclass, exported from the package root) instead of silently skipping trust. Its reason field is 'missing-choice', 'conflicting-choice', or 'legacy-dev-sentinel' (the retired executablePath: 'dev' opt-out).

    Passing a real executablePath behaves exactly as before, including the existing setDevelopmentMode() allowlist bypass and IdentityMismatchError on a hash conflict.

    Migration — every existing setup() call must now name a trust choice:

    // Before — unverified by default (silent skip):
    await vault.setup('MY_API_KEY')
    
    // After — verify the calling executable (production):
    await vault.setup('MY_API_KEY', { executablePath: process.argv[1] })
    
    // After — deliberately skip verification (development/tests only):
    await vault.setup('MY_API_KEY', { skipTrust: true })

    Callers that previously passed the 'dev' sentinel must switch to the dedicated opt-out — the legacy 'dev' sentinel is no longer supported and is now rejected at runtime with an ExecutableTrustRequiredError (reason: 'legacy-dev-sentinel') instead of being resolved as a real path:

    // Before:
    await vault.setup('MY_API_KEY', { executablePath: 'dev' })
    // After:
    await vault.setup('MY_API_KEY', { skipTrust: true })

    @vaultkeeper/test-helpers: TestVault gains a setup(name, options?) convenience method that defaults to skipTrust: true, so consumer tests calling it stay hermetic without naming a real executable. Pass executablePath to exercise real verification instead.

  • #56 9fc3eeb Thanks @mike-north! - Add store() and delete() convenience methods to TestVault. Moves vaultkeeper from dependencies to peerDependencies to fix a dual-package hazard that caused instanceof checks on VaultError subclasses to fail in some consumer setups. Consumers must now list vaultkeeper as a direct dependency.

  • #196 7ee1a61 Thanks @mike-north! - Type-enforce the setup() trust choice, fix the quick-start rebuild footgun, and polish docs and CLI usage errors.

    SetupOptions is now a discriminated union (type-enforced trust XOR). VaultKeeper.setup()'s options argument is required and must carry exactly one of executablePath (TOFU verification) or skipTrust: true (development opt-out). Supplying neither — including a bare setup('NAME') or setup('NAME', {}) — or both is now a compile-time error rather than a runtime-only failure; ExecutableTrustRequiredError remains the runtime backstop for untyped (plain-JavaScript) callers. SetupOptionsBase is exported for the common (non-trust) options. @vaultkeeper/test-helpers gains a matching public TestVaultSetupOptions type; TestVault.setup() keeps its permissive, trust-choice-optional signature (it still defaults to skipTrust: true).

    Quick-start rebuild footgun fixed. The library quick start no longer steers first-timers to executablePath: process.argv[1], which pins TOFU trust to the compiled entry-file hash and throws IdentityMismatchError on the next run after any rebuild. The runnable snippets now use the development-safe { skipTrust: true }, with an inline warning and a clearly-framed production example that binds a stable anchor (a released binary or process.execPath), plus a cross-reference to Development mode for frequently-rebuilt local callers.

    Docs and CLI papercuts. Documented exec()'s [REDACTED]-by-default output redaction and the redact: false opt-out in the library README; clarified that VaultKeeper.init() is in-memory and does not write config.json (only the CLI config init does); clarified that pre-approving a caller is a required first step for non-interactive/CI first exec (CLI) versus auto-recorded on first encounter (library); and documented the WASM doctor() unscoped required-vs-informational semantics. The CLI now prints a Usage: block (and exits 2) for an unknown top-level flag, matching the unknown-command and subcommand-level usage errors.

Patch Changes

  • #187 a822564 Thanks @mike-north! - Ship a per-package LICENSE and align docs for signing/verification and packaging.

    • Every published package now carries its own LICENSE file and lists LICENSE + README.md explicitly in its files array, so the packaging declaration matches what npm actually ships (previously only a root LICENSE existed, which npm pack does not include in per-package tarballs). A packaging test now asserts LICENSE is present in each tarball.
    • Documented sign()'s precondition that the stored secret must be PEM private-key material — secrets are stored as strings and crypto.createPrivateKey() treats a string as PEM, so raw binary DER must be converted to PEM before storing; a plain-string secret throws InvalidKeyMaterialError. Added a distinct example key and a runnable end-to-end generateKeyPairSync → store → sign → verify walkthrough, plus InvalidKeyMaterialError in the repository README's error table.
    • Scoped the delegated access patterns (fetch()/exec()/getSecret()/sign()/verify()) explicitly to the TypeScript library and clarified that @vaultkeeper/wasm's executablePath is a non-enforcing claim label, unlike this library's TOFU-verified executablePath.
    • Added a getSecret() code sample, a top-of-README quick-links/TL;DR block, a note that doctor deliberately checks all supported backends' tooling, and a more precise TypeScript-version note that shows the exact known-good consumer compilerOptions the CI matrix verifies across TypeScript 5.0.4–7.0.2.
  • #84 c521414 Thanks @mike-north! - Remove the top-level package.json#types field, which pointed at an API Extractor rollup (dist/<name>-public.d.ts) that the release pipeline never generates before changeset publish and was therefore absent from the published tarball. Types now resolve entirely through the conditional exports map, which already pointed at the real per-format tsup output. @vaultkeeper/cli-test-helpers's exports conditions, which had the same stale rollup reference, now point at the real dist/index.d.ts / dist/index.d.cts files as well.

    Confirms (and now enforces via a packaging test) that only @vaultkeeper/cli declares the vaultkeeper bin — the vaultkeeper library package was already free of a bin field in this repo, but the registry had previously observed contradictory bin ownership across published versions.

  • #173 ee287c9 Thanks @mike-north! - Loosen the vaultkeeper peerDependency range from workspace:^ (published as ^0.6.0) to an explicit >=0.6.0 <1. The caret range was minor-locked under 0.x, so a routine vaultkeeper minor bump (e.g. 0.6.0 → 0.7.0) would exit the range and trigger a changesets-driven major bump on @vaultkeeper/test-helpers — silently graduating it to 1.0.0 with no changeset declaring that intent. The new range tracks the pre-1.0 vaultkeeper line explicitly and only forces a major on @vaultkeeper/test-helpers once vaultkeeper itself reaches 1.0.0, which is the point such a cascade should actually happen.

    This also requires enabling changesets' onlyUpdatePeerDependentsWhenOutOfRange option (see .changeset/config.json): by default, changesets bumps a package major on any non-patch release of a peer dependency, regardless of whether the new version still satisfies the declared peer range. Without that option, the widened range alone would not have stopped the cascade.

  • #77 26c876c Thanks [@mike-north](https://github.com/mike-nor...

Read more

@vaultkeeper/cli@0.2.0

Choose a tag to compare

@github-actions github-actions released this 16 Jul 13:26
7000f95

Minor Changes

  • #89 46df0b0 Thanks @mike-north! - Make backend selection visible and overridable from the CLI and introspectable from the library.

    • vaultkeeper config init --backend <type> now writes a config whose first enabled backend is <type>. Valid values are the registered backend types; an unknown value exits 2 and lists the valid types.
    • Any unknown flag on a config subcommand (config init, config show) now exits 2 with an "unknown option" error instead of being silently ignored — a typo can no longer send secrets to an unintended credential store.
    • config init output now states which backend was configured and how to change it. config show reports the resolved active backend (first enabled).
    • New public platformNativeBackendType() reports the OS-native credential store for the current platform (keychain on macOS, dpapi on Windows, secret-tool on Linux, file on other platforms) — the store you can opt into with --backend.
    • New public VaultKeeper.activeBackendType getter exposes the type of the active (first enabled) backend at runtime.
  • #92 75685ac Thanks @mike-north! - Add a global --config-dir <path> flag / VAULTKEEPER_CONFIG_DIR environment variable to the CLI so every command (store, delete, exec, approve, dev-mode, doctor, config, rotate-key, revoke-key) can be pointed at an isolated config directory — the flag wins over the env var, which wins over the platform default. config init creates the override directory as needed, and config show reports the path it loaded from. The library's getDefaultConfigDir() and loadConfig() are now public so embedders and the CLI share the same resolution logic. @vaultkeeper/cli-test-helpers's createCliTestEnv() gains a configDirMode option ('env' | 'flag') and no longer manipulates the subprocess's HOME directory to achieve isolation.

  • #95 4ebfa5d Thanks @mike-north! - Uniform CLI exit-code taxonomy (0 success / 1 runtime failure / 2 usage error) applied everywhere: a top-level typo like vaultkeeper --bogus now exits 2 with an error instead of silently exiting 0, and an unrecognized flag on store, delete, exec, approve, dev-mode, or doctor now exits 2 instead of a bare fatal error (exit 1).

    store (and delete, for consistency) now reject an empty or whitespace-only --name with exit 2 and the same error style as a missing flag, instead of persisting a near-unreachable secret or surfacing a generic runtime error. Allowed --name characters (letters, digits, ., _, -, /) are documented in --help.

    exec now validates that the secret exists before the caller-approval/TTY gate, so exec --secret <nonexistent> ... reports a clear SecretNotFoundError regardless of TTY, instead of being masked by the generic "requires interactive approval" message. This is backed by a new public VaultKeeper.secretExists(name) method — a side-effect-free existence check that never touches the TOFU trust manifest.

    config init --help and config show --help now print help for that subcommand instead of the parent config help. exec --help includes a worked --caller example.

  • #94 0ca9d3f Thanks @mike-north! - doctor and config show now detect an invalid config file instead of silently ignoring it. doctor validates the config file (when present) as part of its preflight checks and reports a failing config check with the parse/validation error and file path, exiting non-zero. config show on invalid JSON now exits non-zero with the parse error (including a line/column location when available) instead of dumping the raw file with exit 0. Every config parse/validation error raised by loadConfig() — surfaced through store, delete, exec, config show, and doctor alike — now includes the config file path, the parse location where available, and a remediation hint naming vaultkeeper config init.

    loadConfig() now falls back to platform defaults only when the config file is missing (ENOENT). A present-but-unreadable file (e.g. a permissions error) is rethrown as a typed FilesystemError instead of being silently treated as "no config" — a genuinely broken config was previously invisible to doctor and config show.

    The "no config file" story is now uniform across store, delete, exec, config show, and doctor: each falls back to platform defaults and prints a one-line notice naming the resolved backend and vaultkeeper config init (e.g. No config file found; using platform defaults (keychain). Run 'vaultkeeper config init' to persist one.). Previously config show errored with exit 1 on a missing config file while the other commands defaulted silently; config show now defaults and reports it like the rest.

    New public ConfigParseError (with path and location fields) is thrown on invalid config JSON. ConfigValidationError gains an optional configFilePath field. PreflightCheckStatus gains an 'invalid' value, and RunDoctorOptions gains an optional configDir field that lets runDoctor/VaultKeeper.doctor() load and validate the config itself.

  • #121 7c8ab85 Thanks @mike-north! - Scope doctor to the active/configured backend so a fresh install no longer looks broken. Previously doctor rendered every non-'ok' check with a failing icon, including plugin-backend tools (ykman, op) that weren't configured — on the post-#98 file-default install, this meant the very first doctor run showed a failing check for a YubiKey/1Password tool the user never opted into.

    PreflightResult.checks entries are now ScopedPreflightCheck (a PreflightCheck plus required: boolean), reflecting whether each dependency is required for the active/configured backend(s). The CLI only renders the icon for checks that are both required and failing; unmet optional checks still surface, without the failure icon, in the Warnings section. Opt-in backends still get their dependency checks promoted to required when configured (e.g. --backend yubikey requires ykman).

  • #86 be28555 Thanks @mike-north! - vaultkeeper exec can now run non-interactively. A caller already recorded in the TOFU trust manifest (via approve or a prior approval) runs without any prompt on a TTY or not. A new explicit opt-in — the --yes flag and the VAULTKEEPER_YES=1 environment variable — approves an untrusted caller for a single invocation without prompting, recording the approval the same way an interactive y would. Without trust and without --yes, an untrusted caller on non-TTY stdin still fails, but the error now tells you exactly how to proceed (vaultkeeper approve --script <caller> or --yes). exec --help documents the TTY requirement and both escape hatches, and the README gains a "Running in CI" note. A caller whose contents changed since approval is never auto-approved by --yes; it must be re-approved with vaultkeeper approve.

  • #210 38fafb5 Thanks @mike-north! - Add a backend presencePerUse capability and the ability to require it for an operation.

    presencePerUse means "every operation with a key in this backend forces a distinct, fresh physical human action, and can never be satisfied from a cached or session-unlocked state." vaultkeeper is now the single place that knows this per configured backend instance and enforces it, so consumers never have to reason about YubiKey touch policies or 1Password per-access tricks themselves.

    Library:

    • New PresenceCapableBackend extension interface (getCapabilities(): Promise<BackendCapabilities>) — mirrors ListableBackend/SigningBackend; it is not a required member of SecretBackend. BackendCapabilities is { presencePerUse: boolean } and is open to extension.
    • New getBackendCapabilities(backend) helper and isPresenceCapableBackend(backend) guard. A backend that does not implement the interface reports { presencePerUse: false } — an unknown backend never silently claims presence. The capability reflects the configured instance (a YubiKey slot's touch policy, 1Password's access mode), never a hardcoded per-type answer.
    • New VaultKeeper.getActiveBackendCapabilities() introspection method.
    • New requirePresencePerUse?: boolean option on the shared access path (store, delete, setup, sign). Enforcement is queried fresh on every call and refuses before any credential/session/device is touched when unsatisfied; when capable, the operation forces a fresh action for that specific call. Presence-gated signing performs a fresh backend signWithKey round-trip per call, so no cached key material can satisfy it.
    • Enforcement is operation-aware and fail-closed ...
Read more