Releases: mike-north/vaultkeeper
Release list
vaultkeeper@0.8.0
Minor Changes
- #317
5231999Thanks @mike-north! - AddPresenceSimulatorBackendto@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'viaforTesting({ operations }), or armed one call at a time viaarmPresenceto 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 itsforTesting()factory throws a newTestDoubleMisuseError(exported fromvaultkeeper) whenNODE_ENVis'production'.
Patch Changes
- #323
8be4d18Thanks @mike-north! - Harden the Linuxsecret-toolbackend: 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, andretrieve()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
Minor Changes
-
#317
5231999Thanks @mike-north! - Add deterministic fault injection (FaultPlan,InMemoryBackend.injectFault/clearFault/clearAllFaults) andTestVault.signCeremonyfor exercising a consumer's error-handling paths and full signing-ceremony flow without hardware. -
#317
5231999Thanks @mike-north! - AddPresenceSimulatorBackendto@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'viaforTesting({ operations }), or armed one call at a time viaarmPresenceto 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 itsforTesting()factory throws a newTestDoubleMisuseError(exported fromvaultkeeper) whenNODE_ENVis'production'.
@vaultkeeper/cli@0.2.2
vaultkeeper@0.7.1
Patch Changes
- #287
df3ed7bThanks @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-emptyvalandbkdexactly as before; a session signing-key lease (no secret value) instead requires a non-emptykidand a presentkgen(never defaulted to generation 0). No public API changed —VaultClaimsremains an internal type, and every existing secret-token code path is unchanged.
@vaultkeeper/wasm@0.4.0
Minor Changes
-
#259
3163724Thanks @mike-north! - Phase 0 bridge contracts for the consolidation effort (issue #239): the RustHostPlatformtrait and its WASM/JS bridge gain the primitives a host-implemented backend and delegated network access will need in later phases.HostPlatform::execnow accepts anExecOptionsbundle (stdin,env,cwd) instead of a barestdinargument —@vaultkeeper/wasm'sWasmHostPlatform.execmirrors this with an optional thirdoptionsargument, andcreateNodeHost()implementsenvas{ ...process.env, ...options.env }andcwdviachild_process.execFile's owncwdoption. Omittingoptions(or any of its fields) reproduces the exact pre-#239 behavior — no existing caller's behavior changes.A new
HostPlatform::http_fetchprimitive lands with its@vaultkeeper/wasmcounterpartWasmHostPlatform.httpFetch, implemented increateNodeHost()over the globalfetch. 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_approvalcapability 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/wasmexposes this as the optionalWasmHostPlatform.promptApprovalmethod.@vaultkeeper/wasmalso publishes a newHostSecretBackendcontract type — the shape a JS/TS-implemented secret backend must satisfy to be driven by the Rust core — backed by a newJsSecretBackendscaffold incrates/vaultkeeper-wasmthat dispatchesstore/retrieve/delete/exists/listover JS callbacks (all-async,Uint8Arrayat the boundary, neverBuffer). 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
d88a17fThanks @mike-north! - Additive handle-based capability surface for theauthorize()result (issue #241):WasmAuthorizationgains ahandleIdgetter exposing the underlying core capability handle id, andWasmVaultKeepergainsresolveSecretClaims(handleId)andreleaseHandle(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-sideHandleTableentry, performs the one-timeread_secretagainst it immediately, and caches the result on the returnedWasmAuthorization, soclaimsno longer carries the raw secret (val) across the WASM boundary at all; the secret was already redacted from the observableclaimsshape 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 withAuthorizationDenied), andreleaseHandle(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 eagerauthorize()wrapper; no existing caller needs to change. -
#286
653f4afThanks @mike-north! - Introduce the environment profile primitive invaultkeeper-core(issue #277): serde schema, a fail-closed loader, andprofile init/show/list/lintin 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 insideconfig.json.@vaultkeeper/wasmgains theMaterializeModeUnsupportedErrortyped error class (and itsmaterialize-mode-unsupportederror code), thrown when a profile'smaterializefield uses the reserved-but-not-yet-implemented object form ({ "mode": "reference", ... }). -
#252
3cf4d25Thanks @mike-north! - Port encrypted key-state persistence (keys.enc+.keys.wrap) to the Rust core, closing the parity gap where the WASM SDK'sKeyManagerwas memory-only. A JWE minted by one process (orVaultKeeperinstance) 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
vaultkeeperlibrary's existingkeys/storage.ts: a store written by either implementation loads correctly in the other.Breaking (0.x):
rotateKey()andrevokeKey()are now async (Promise<void>instead ofvoid), 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
WasmHostPlatforminterface consumed bycreateNodeHost()also gains arenameFile(from, to)method, used for atomic write-then-rename persistence.
Patch Changes
-
#251
5f1f370Thanks @mike-north! - Complete theVaultErrortaxonomy so it can bridge losslessly to@vaultkeeper/wasm. The Rust core gains 14 newVaultErrorvariants (NotCapable,PresenceDeclined,PresenceTimeout,InvalidKeyMaterial,SigningKeyNotFound,SigningKeyAlreadyExists,SigningNotSupported,Exec,Fetch,InvalidToken,AccessorConsumed,ConfigValidation,UnknownBackendType,ConfigParse) with machine-readable context fields matching the pure-TypeScriptvaultkeeperlibrary's error classes.@vaultkeeper/wasmnow exports the matching typed error classes —NotCapableError,PresenceDeclinedError,PresenceTimeoutError,InvalidKeyMaterialError,SigningKeyNotFoundError,SigningKeyAlreadyExistsError,SigningNotSupportedError,ExecError,FetchError,ConfigValidationError,UnknownBackendTypeError,ConfigParseError— plusBackendLockedError,DeviceNotPresentError,AuthorizationDeniedError,BackendUnavailableError,PluginNotFoundError,InvalidAlgorithmError, andSetupError, which had Rust-side variants already but were never reconstructed at the WASM boundary and previously collapsed to the genericVaultErrorbase 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_fieldsinvaultkeeper-core) and the TypeScript reconstruction map (ALL_VAULT_ERROR_CODESin@vaultkeeper/wasm'serrors.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
df3ed7bThanks @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-emptyvalandbkdexactly as before; a session signing-key lease (no secret value) instead requires a non-emptykidand a presentkgen(never defaulted to generation 0). No public API changed —VaultClaimsremains an internal type, and every existing secret-token code path is unchanged. -
#253
c230593Thanks @mike-north! - Rebuild the committed WASM binary with explicitwasm-opt -Ozoptimization (previously relying on wasm-pack's implicit default). No runtime API changes — the artifact is smaller, not different in behavior. -
#250
4fdbe31Thanks @mike-north! - Fix the Rust core's zero-config default backend to befileon every platform, matching thevaultkeeper(TS) package's #98 fix.Previously, when no
config.jsonexisted, the Rust core (and therefore@vaultkeeper/wasm, which wraps it) fell back to a platform-native backend —keychainon macOS,dpapion Windows — instead of the portable, self-contained AES-256-GCM encryptedfilebackend. 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 selectskeychain/dpapiis unaffected; only the zero-config fall...
@vaultkeeper/cli@0.2.1
Patch Changes
- Updated dependencies [
df3ed7b]:- vaultkeeper@0.7.1
vaultkeeper@0.7.0
Minor Changes
-
#89
46df0b0Thanks @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
configsubcommand (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 initoutput now states which backend was configured and how to change it.config showreports the resolved active backend (first enabled).- New public
platformNativeBackendType()reports the OS-native credential store for the current platform (keychainon macOS,dpapion Windows,secret-toolon Linux,fileon other platforms) — the store you can opt into with--backend. - New public
VaultKeeper.activeBackendTypegetter exposes the type of the active (first enabled) backend at runtime.
-
#92
75685acThanks @mike-north! - Add a global--config-dir <path>flag /VAULTKEEPER_CONFIG_DIRenvironment 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 initcreates the override directory as needed, andconfig showreports the path it loaded from. The library'sgetDefaultConfigDir()andloadConfig()are now public so embedders and the CLI share the same resolution logic.@vaultkeeper/cli-test-helpers'screateCliTestEnv()gains aconfigDirModeoption ('env'|'flag') and no longer manipulates the subprocess'sHOMEdirectory to achieve isolation. -
#160
90a4127Thanks @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-dirorVAULTKEEPER_CONFIG_DIR), so the copy-pastedvaultkeeper 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 newgetPlatformDefaultConfigDir()export computes the machine default independent ofVAULTKEEPER_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. FilesystemErrornow renders a human message from its typedpath/permissionfields — plainly stating whether the file is missing or permission-denied, with a suggested next step — instead of leaking the raw NodeENOENT: … open '<path>'text. The typed class and its fields are unchanged.doctorprints the config remediation exactly once (under "Next steps") instead of duplicating it inline on the failing config check.
- The invalid-config recovery hint now carries an explicit
-
#95
4ebfa5dThanks @mike-north! - Uniform CLI exit-code taxonomy (0 success / 1 runtime failure / 2 usage error) applied everywhere: a top-level typo likevaultkeeper --bogusnow exits 2 with an error instead of silently exiting 0, and an unrecognized flag onstore,delete,exec,approve,dev-mode, ordoctornow exits 2 instead of a bare fatal error (exit 1).store(anddelete, for consistency) now reject an empty or whitespace-only--namewith 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--namecharacters (letters, digits,.,_,-,/) are documented in--help.execnow validates that the secret exists before the caller-approval/TTY gate, soexec --secret <nonexistent> ...reports a clearSecretNotFoundErrorregardless of TTY, instead of being masked by the generic "requires interactive approval" message. This is backed by a new publicVaultKeeper.secretExists(name)method — a side-effect-free existence check that never touches the TOFU trust manifest.config init --helpandconfig show --helpnow print help for that subcommand instead of the parentconfighelp.exec --helpincludes a worked--callerexample. -
#94
0ca9d3fThanks @mike-north! -doctorandconfig shownow detect an invalid config file instead of silently ignoring it.doctorvalidates the config file (when present) as part of its preflight checks and reports a failingconfigcheck with the parse/validation error and file path, exiting non-zero.config showon 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 byloadConfig()— surfaced throughstore,delete,exec,config show, anddoctoralike — now includes the config file path, the parse location where available, and a remediation hint namingvaultkeeper 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 typedFilesystemErrorinstead of being silently treated as "no config" — a genuinely broken config was previously invisible todoctorandconfig show.The "no config file" story is now uniform across
store,delete,exec,config show, anddoctor: each falls back to platform defaults and prints a one-line notice naming the resolved backend andvaultkeeper config init(e.g.No config file found; using platform defaults (keychain). Run 'vaultkeeper config init' to persist one.). Previouslyconfig showerrored with exit 1 on a missing config file while the other commands defaulted silently;config shownow defaults and reports it like the rest.New public
ConfigParseError(withpathandlocationfields) is thrown on invalid config JSON.ConfigValidationErrorgains an optionalconfigFilePathfield.PreflightCheckStatusgains an'invalid'value, andRunDoctorOptionsgains an optionalconfigDirfield that letsrunDoctor/VaultKeeper.doctor()load and validate the config itself. -
#120
b270562Thanks @mike-north! - Make@1password/sdkan optional peer dependency instead of a runtime dependency. Installingvaultkeeperno longer pulls@1password/sdk(and its@1password/sdk-coretransitive) into the dependency closure — the file-backend path staysjose-only. The 1Password backend now loads the SDK lazily (via dynamicimport()) only when that backend is actually used, and fails with a typedPluginNotFoundErrornaming the missing@1password/sdkpeer when it is not installed. To use the 1Password backend, install@1password/sdkalongsidevaultkeeper. -
#126
cfcd61bThanks @mike-north! - Fixdev-modeinvalid-action misdiagnosis and auditconfig.ts/the file backend for plainErrorthrows.vaultkeeper dev-mode <action> --script <path>now distinguishes an invalid action from missing args: an unrecognized action (e.g.banana) emitsunknown action "<x>" (expected "enable" or "disable")(exit 2), whilemissing action or --script flagis reserved for genuinely absent arguments.- The encrypted-file secret backend (
FileBackend) now surfacesEACCES/permission failures reading, writing, or deleting a secret entry as a typedFilesystemErrorinstead of the raw Node.js error. - Added a new
DecryptionError(extendsVaultError) for when a stored secret entry fails to decrypt (corrupted ciphertext or a failed AES-GCM auth tag check) — previously thrown as a plainError.
-
#145
f5edcd9Thanks @mike-north! - Give the doctorconfigpreflight check structured error context so the CLI can render a CLI-native remediation instead of the library's install text.- The public
PreflightCheckshape gains an optionalerrorfield (PreflightCheckError:kind+configPath+ optional parselocation) carrying remediation-free, machine-readable context when theconfigcheck 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-readablereasonprose. - `vaultkeeper doct...
- The public
@vaultkeeper/wasm@0.3.0
Minor Changes
-
#121
7c8ab85Thanks @mike-north! - Scopedoctorto the active/configured backend so a fresh install no longer looks broken. Previouslydoctorrendered every non-'ok'check with a failing✗icon, including plugin-backend tools (ykman,op) that weren't configured — on the post-#98file-default install, this meant the very firstdoctorrun showed a failing check for a YubiKey/1Password tool the user never opted into.PreflightResult.checksentries are nowScopedPreflightCheck(aPreflightCheckplusrequired: 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 theWarningssection. Opt-in backends still get their dependency checks promoted to required when configured (e.g.--backend yubikeyrequiresykman). -
#161
b37bfd7Thanks @mike-north! - Breaking (0.x):@vaultkeeper/wasm'ssetup()now requires an explicit executable-trust choice, closing a security-parity gap with the TypeScriptvaultkeeperlibrary. 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 whenexecutablePathwas omitted, so a barevault.setup(name, value)silently minted an unverified token — the same permissive default thatVaultKeeper.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, orskipTrust: true— a self-describing, greppable, development-only opt-out that deliberately skips the binding.
Supplying neither — or both — or the retired
'dev'sentinel asexecutablePathnow throws the new typedExecutableTrustRequiredError(aVaultErrorsubclass, exported from the package root) instead of silently minting an unverified token. Itsreasonfield is'missing-choice','conflicting-choice', or'legacy-dev-sentinel', matching the library'sExecutableTrustRequiredError.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 asexecutablePathmust switch toskipTrust: true; the legacy sentinel is now rejected at runtime withExecutableTrustRequiredError(reason: 'legacy-dev-sentinel'). -
#154
fc544aeThanks @mike-north! - Fix the WASM SDK's JS host bridge erasing filesystem errno codes: a permission-denied read or delete previously surfaced as a genericVaultError, indistinguishable from any other failure, instead of a typed error a caller could branch on.readFile/deleteFile/fileExistsin the Node host bridge (createNodeHost) now reject with a structured{ message, path, code }contract thatJsHostPlatform(the Rust side of the bridge) reads back to build a typedVaultError::Filesystem, mirroring the native CLI host's classification: a genuine "does not exist" still resolves toSecretNotFoundError, while permission and other errno failures now surface as a new publicFilesystemError(withpath,permission, andcodefields —codecarries the underlying errno, e.g.EACCES, when available). Exported from@vaultkeeper/wasmalongside the rest of the typed error hierarchy. -
#85
b8262d9Thanks @mike-north! - Stopauthorize()from returning the raw secret and add typed errors.authorize()no longer exposes the plaintext secret on its result: the returned
claimsno longer carryval. The secret is now read through a one-time
SecretAccessoronresult.secret(secret.read((value) => ...)), mirroring the
createSecretAccessorpattern 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 soerr instanceof VaultErrorholds across the ecosystem.This is a breaking change to the
authorize()return shape: code that read
result.claims.valmust switch toresult.secret.read(...). -
#178
09c48d7Thanks @mike-north! - Enforce executable-trust verification insetup()when anexecutablePathis supplied.Previously, passing
executablePathbound the raw path into the token'sexeclaim 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 theexeclaim, matching the pure-TypeScriptvaultkeeperlibrary'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 newIdentityMismatchError(carryingpreviousHash/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: trueis unchanged — it still opts out of verification and mints a'dev'-bound token.
Behavior change:
setup()is nowasyncand returnsPromise<string>(it performs executable hashing and manifest I/O). Callers mustawaitit. SupplyingexecutablePathnow performs real verification and can throwIdentityMismatchError. - A first encounter records the executable's hash under trust-on-first-use. A later
-
#203
9b3e193Thanks @mike-north! - Enforce thesetup()executable-trust choice at compile time in@vaultkeeper/wasm, matching the TypeScriptvaultkeeperlibrary. 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-argumentvault.setup(name, value)— orvault.setup(name, value, {})— type-checked cleanly yet threwExecutableTrustRequiredError(reason: 'missing-choice') at runtime. WASM users got no compile-time protection on the very trust choice the rest of the ecosystem type-enforces.SetupOptionsis nowSetupOptionsBase(ttlMinutes/useLimit/backendType) intersected with a discriminated union requiring exactly one ofexecutablePathorskipTrust: 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
ExecutableTrustRequiredErrorremains as a backstop for untyped (plain-JavaScript) callers.
Patch Changes
-
#205
2086c0aThanks @mike-north! - Fix the WASM SDK failing to read a config directory produced by the documentedvaultkeeper config initflow.config init(and the README example) writesdefaults.trustTieras a bare JSON number (3), but the Rust-core config reader behind the SDK required a string-encoded number, socreateVaultKeeper()threwVaultError: Failed to parse configon a CLI-produced config.The core config reader now accepts
trustTieras 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. Thetidclaim in ...
@vaultkeeper/test-helpers@0.3.0
Minor Changes
-
#131
f2fe1d2Thanks @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 whilevaultkeeperis pre-1.0).setup()previously defaultedexecutablePathto'dev'when omitted, which silently skipped Trust On First Use (TOFU) executable-identity verification — so a bareawait 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. Existingsetup(name)calls must now passexecutablePath(runs TOFU verification) orskipTrust: true(development-only opt-out); omitting both throwsExecutableTrustRequiredError.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), orskipTrust: true— a self-describing, greppable, development-only opt-out that deliberately skips verification.
Supplying neither — or both — now throws the new typed
ExecutableTrustRequiredError(aVaultErrorsubclass, exported from the package root) instead of silently skipping trust. Itsreasonfield is'missing-choice','conflicting-choice', or'legacy-dev-sentinel'(the retiredexecutablePath: 'dev'opt-out).Passing a real
executablePathbehaves exactly as before, including the existingsetDevelopmentMode()allowlist bypass andIdentityMismatchErroron 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 anExecutableTrustRequiredError(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:TestVaultgains asetup(name, options?)convenience method that defaults toskipTrust: true, so consumer tests calling it stay hermetic without naming a real executable. PassexecutablePathto exercise real verification instead. -
#56
9fc3eebThanks @mike-north! - Addstore()anddelete()convenience methods toTestVault. MovesvaultkeeperfromdependenciestopeerDependenciesto fix a dual-package hazard that causedinstanceofchecks onVaultErrorsubclasses to fail in some consumer setups. Consumers must now listvaultkeeperas a direct dependency. -
#196
7ee1a61Thanks @mike-north! - Type-enforce thesetup()trust choice, fix the quick-start rebuild footgun, and polish docs and CLI usage errors.SetupOptionsis now a discriminated union (type-enforced trust XOR).VaultKeeper.setup()'s options argument is required and must carry exactly one ofexecutablePath(TOFU verification) orskipTrust: true(development opt-out). Supplying neither — including a baresetup('NAME')orsetup('NAME', {})— or both is now a compile-time error rather than a runtime-only failure;ExecutableTrustRequiredErrorremains the runtime backstop for untyped (plain-JavaScript) callers.SetupOptionsBaseis exported for the common (non-trust) options.@vaultkeeper/test-helpersgains a matching publicTestVaultSetupOptionstype;TestVault.setup()keeps its permissive, trust-choice-optional signature (it still defaults toskipTrust: 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 throwsIdentityMismatchErroron 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 orprocess.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 theredact: falseopt-out in the library README; clarified thatVaultKeeper.init()is in-memory and does not writeconfig.json(only the CLIconfig initdoes); clarified that pre-approving a caller is a required first step for non-interactive/CI firstexec(CLI) versus auto-recorded on first encounter (library); and documented the WASMdoctor()unscoped required-vs-informational semantics. The CLI now prints aUsage:block (and exits 2) for an unknown top-level flag, matching the unknown-command and subcommand-level usage errors.
Patch Changes
-
#187
a822564Thanks @mike-north! - Ship a per-packageLICENSEand align docs for signing/verification and packaging.- Every published package now carries its own
LICENSEfile and listsLICENSE+README.mdexplicitly in itsfilesarray, so the packaging declaration matches what npm actually ships (previously only a rootLICENSEexisted, whichnpm packdoes not include in per-package tarballs). A packaging test now assertsLICENSEis present in each tarball. - Documented
sign()'s precondition that the stored secret must be PEM private-key material — secrets are stored as strings andcrypto.createPrivateKey()treats a string as PEM, so raw binary DER must be converted to PEM before storing; a plain-string secret throwsInvalidKeyMaterialError. Added a distinct example key and a runnable end-to-endgenerateKeyPairSync→ store → sign → verify walkthrough, plusInvalidKeyMaterialErrorin 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'sexecutablePathis a non-enforcing claim label, unlike this library's TOFU-verifiedexecutablePath. - 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 consumercompilerOptionsthe CI matrix verifies across TypeScript 5.0.4–7.0.2.
- Every published package now carries its own
-
#84
c521414Thanks @mike-north! - Remove the top-levelpackage.json#typesfield, which pointed at an API Extractor rollup (dist/<name>-public.d.ts) that the release pipeline never generates beforechangeset publishand was therefore absent from the published tarball. Types now resolve entirely through the conditionalexportsmap, which already pointed at the real per-formattsupoutput.@vaultkeeper/cli-test-helpers'sexportsconditions, which had the same stale rollup reference, now point at the realdist/index.d.ts/dist/index.d.ctsfiles as well.Confirms (and now enforces via a packaging test) that only
@vaultkeeper/clideclares thevaultkeeperbin — thevaultkeeperlibrary package was already free of abinfield in this repo, but the registry had previously observed contradictory bin ownership across published versions. -
#173
ee287c9Thanks @mike-north! - Loosen thevaultkeeperpeerDependency range fromworkspace:^(published as^0.6.0) to an explicit>=0.6.0 <1. The caret range was minor-locked under 0.x, so a routinevaultkeeperminor 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-helpersoncevaultkeeperitself reaches 1.0.0, which is the point such a cascade should actually happen.This also requires enabling changesets'
onlyUpdatePeerDependentsWhenOutOfRangeoption (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
26c876cThanks [@mike-north](https://github.com/mike-nor...
@vaultkeeper/cli@0.2.0
Minor Changes
-
#89
46df0b0Thanks @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
configsubcommand (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 initoutput now states which backend was configured and how to change it.config showreports the resolved active backend (first enabled).- New public
platformNativeBackendType()reports the OS-native credential store for the current platform (keychainon macOS,dpapion Windows,secret-toolon Linux,fileon other platforms) — the store you can opt into with--backend. - New public
VaultKeeper.activeBackendTypegetter exposes the type of the active (first enabled) backend at runtime.
-
#92
75685acThanks @mike-north! - Add a global--config-dir <path>flag /VAULTKEEPER_CONFIG_DIRenvironment 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 initcreates the override directory as needed, andconfig showreports the path it loaded from. The library'sgetDefaultConfigDir()andloadConfig()are now public so embedders and the CLI share the same resolution logic.@vaultkeeper/cli-test-helpers'screateCliTestEnv()gains aconfigDirModeoption ('env'|'flag') and no longer manipulates the subprocess'sHOMEdirectory to achieve isolation. -
#95
4ebfa5dThanks @mike-north! - Uniform CLI exit-code taxonomy (0 success / 1 runtime failure / 2 usage error) applied everywhere: a top-level typo likevaultkeeper --bogusnow exits 2 with an error instead of silently exiting 0, and an unrecognized flag onstore,delete,exec,approve,dev-mode, ordoctornow exits 2 instead of a bare fatal error (exit 1).store(anddelete, for consistency) now reject an empty or whitespace-only--namewith 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--namecharacters (letters, digits,.,_,-,/) are documented in--help.execnow validates that the secret exists before the caller-approval/TTY gate, soexec --secret <nonexistent> ...reports a clearSecretNotFoundErrorregardless of TTY, instead of being masked by the generic "requires interactive approval" message. This is backed by a new publicVaultKeeper.secretExists(name)method — a side-effect-free existence check that never touches the TOFU trust manifest.config init --helpandconfig show --helpnow print help for that subcommand instead of the parentconfighelp.exec --helpincludes a worked--callerexample. -
#94
0ca9d3fThanks @mike-north! -doctorandconfig shownow detect an invalid config file instead of silently ignoring it.doctorvalidates the config file (when present) as part of its preflight checks and reports a failingconfigcheck with the parse/validation error and file path, exiting non-zero.config showon 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 byloadConfig()— surfaced throughstore,delete,exec,config show, anddoctoralike — now includes the config file path, the parse location where available, and a remediation hint namingvaultkeeper 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 typedFilesystemErrorinstead of being silently treated as "no config" — a genuinely broken config was previously invisible todoctorandconfig show.The "no config file" story is now uniform across
store,delete,exec,config show, anddoctor: each falls back to platform defaults and prints a one-line notice naming the resolved backend andvaultkeeper config init(e.g.No config file found; using platform defaults (keychain). Run 'vaultkeeper config init' to persist one.). Previouslyconfig showerrored with exit 1 on a missing config file while the other commands defaulted silently;config shownow defaults and reports it like the rest.New public
ConfigParseError(withpathandlocationfields) is thrown on invalid config JSON.ConfigValidationErrorgains an optionalconfigFilePathfield.PreflightCheckStatusgains an'invalid'value, andRunDoctorOptionsgains an optionalconfigDirfield that letsrunDoctor/VaultKeeper.doctor()load and validate the config itself. -
#121
7c8ab85Thanks @mike-north! - Scopedoctorto the active/configured backend so a fresh install no longer looks broken. Previouslydoctorrendered every non-'ok'check with a failing✗icon, including plugin-backend tools (ykman,op) that weren't configured — on the post-#98file-default install, this meant the very firstdoctorrun showed a failing check for a YubiKey/1Password tool the user never opted into.PreflightResult.checksentries are nowScopedPreflightCheck(aPreflightCheckplusrequired: 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 theWarningssection. Opt-in backends still get their dependency checks promoted to required when configured (e.g.--backend yubikeyrequiresykman). -
#86
be28555Thanks @mike-north! -vaultkeeper execcan now run non-interactively. A caller already recorded in the TOFU trust manifest (viaapproveor a prior approval) runs without any prompt on a TTY or not. A new explicit opt-in — the--yesflag and theVAULTKEEPER_YES=1environment variable — approves an untrusted caller for a single invocation without prompting, recording the approval the same way an interactiveywould. 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 --helpdocuments 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 withvaultkeeper approve. -
#210
38fafb5Thanks @mike-north! - Add a backendpresencePerUsecapability and the ability to require it for an operation.presencePerUsemeans "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
PresenceCapableBackendextension interface (getCapabilities(): Promise<BackendCapabilities>) — mirrorsListableBackend/SigningBackend; it is not a required member ofSecretBackend.BackendCapabilitiesis{ presencePerUse: boolean }and is open to extension. - New
getBackendCapabilities(backend)helper andisPresenceCapableBackend(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?: booleanoption 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 backendsignWithKeyround-trip per call, so no cached key material can satisfy it. - Enforcement is operation-aware and fail-closed ...
- New