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 doctorrun against a corrupt or invalid config now shows the CLI-native remediation (config path +vaultkeeper config init --force), wording-consistent with every other command, and no longer tells a user already running the CLI to "install @vaultkeeper/cli". The library's ownreasontext is unchanged for library consumers.
- The public
-
#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). -
#177
16f67a9Thanks @mike-north! - Render an unreadable config directory as a failing doctor check instead of a raw crash.vaultkeeper doctorrun against a config directory the process cannot read (e.g. achmod 000directory, so readingconfig.jsoninside it fails withEACCES/EPERM) previously aborted with a raw NodeError: EACCES: permission denied, access '.../config.json'— no typed class, no fix hint, and no checks rendered at all. It now surfaces the read failure as a failingconfigcheck (just like a parse or validation failure), keeps rendering the other checks, prints a permissions-oriented remediation under "Next steps", and exits non-zero. The raw errno string no longer leaks to the user.- The public
PreflightCheckErrorKindgains a'config-read'member, andPreflightCheckErrorgains an optionalcodefield carrying the underlying errno (e.g.EACCES), so a consumer can build a permissions-specific remediation.config init --forceis deliberately not suggested for this failure — it cannot repair a config the process cannot read.
-
#195
c7f0068Thanks @mike-north! - Redact injected secrets from libraryexec()output, and give the CLI a typed error when a wrapped command cannot be spawned.VaultKeeper.exec()now redacts every injected secret value from the capturedstdout/stderrbefore returning, replacing each occurrence with[REDACTED]. This upholds the documented guarantee that the raw secret never appears in the return value, even when the spawned command echoes it. Multi-secret ({{secret:name}}) injections redact all injected values. Pass the newExecRequest.redact: falseto opt out and receive raw output. The redaction logic is shared with the CLI's streaming--no-redactpath via the new publicredactSecretshelper, so the two surfaces cannot drift.- The CLI
execcommand now maps a spawn failure of the wrapped command (ENOENTfor a nonexistent command,EACCESfor a non-executable file) to a typedExecErrorwith remediation, rendered through the CLI's typed-error formatter, instead of leaking a bareError: spawn <path> ENOENT.
-
#141
16e68b0Thanks @mike-north! -FilesystemErrornow preserves the underlying Node.js filesystem failure it wraps. A newreadonly code: string | undefinedproperty exposes the original errno code (e.g.EACCES,ENOSPC,EISDIR) so callers can discriminate the failure kind without parsing the message text, and the original error is now recorded as the standardError.cause. The two near-duplicate internal helpers that builtFilesystemError(one in the file backend, one in the shared at-rest key-wrapping module) have been merged into a single shared helper so this population happens in one place. -
#174
ea628e5Thanks @mike-north! - Fix two library rough edges surfaced by the direct-integration path.VaultKeeper.activeBackendTypeno longer throwsBackendUnavailableErrorwhen a backend was injected viainit({ backend }). It now reports the injected backend's declaredtype(or the stable'custom'sentinel if it declares an empty type).setup()derives the token'sbkdclaim from the same rule, so an injected backend with an empty type mints a valid token (bkd: "custom") instead of one rejected by claim validation. The config-driven path is unchanged.SecretAccessor.read()now passes the callback's return value through:read<T>(cb: (buf: Buffer) => T): T, soconst value = accessor.read((buf) => buf.toString('utf8'))yields the derived value instead ofundefined. The buffer is still zeroed after the callback returns, so returning the raw buffer only ever yields zeroed bytes — derive a value inside the callback.
-
#222
8fd800cThanks @mike-north! - Extend the 1Password per-access worker with astore/deletewrite path so--require-presence-per-usecovers writes, not just reads. Previously the per-access worker was read-only, soOnePasswordBackendreportedpresenceEnforcedOperations: ['read']and a flaggedstore/deletefailed closed withNotCapableError— correct, but limited. Now every keyed operation (retrieve,store,delete) spawns its own fresh worker process forcing a distinct biometric approval,presenceEnforcedOperationsreports['read', 'store', 'delete'], and the earlierNotCapableErrorrefusal is replaced by the same fresh-action guarantee reads already had. The secret value forstoreis delivered to the worker over stdin — never argv — so it never appears in a process listing, shell history, or log. A declined presence action throwsPresenceDeclinedError; a timed-out one throwsPresenceTimeoutError. -
#93
8124067Thanks @mike-north! - Persist key material across processes so cached tokens and the rotation grace period work between CLI invocations.KeyManagerencryption keys are now persisted under the config directory, encrypted at rest with AES-256-GCM under an owner-only (0600) wrapping key — reusing the same authenticated-cipher primitives as the file backend. Persistence is active only whenVaultKeeperloads its configuration from disk (no injectedconfig/backend); instances built with an injectedconfigorbackendkeep keys in memory, so tests and embedders stay hermetic.- A JWE minted by one process is now authorizable by a later process within its validity window: its
kidstill resolves after the minting process exits. Previously every process generated fresh keys, so a cached token always failed withKeyRevokedError. vaultkeeper exec --cachenow genuinely reuses a cached token on a second run by the same trusted caller — without re-minting and without the misleading "Cached token expired" message. Cached tokens are reusable until they expire (the secret's TTL) or the key is rotated/revoked, after whichexectransparently mints a fresh one.- The cached-token path no longer collapses every authorization failure into a generic "expired" message. Each failure now surfaces its actual cause (e.g.
KeyRevokedError,TokenExpiredError). - The rotation grace-period guard now survives across processes: running
rotate-keytwice while the previous key is still in its grace period fails the second time withRotationInProgressError(non-zero exit) instead of silently rotating again.
-
#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 via
BackendCapabilities.presenceEnforcedOperations(a list ofPresenceOperation; omitted means all operations). A backend that forces presence for only some operations refuses a flagged uncovered operation withNotCapableErrorrather than passing without a fresh action. 1Passwordper-accessforces presence for reads only (store/deleteroute through the cached session client), so a flaggedstore/deleteon 1Password is correctly refused; a YubiKey touch slot covers every operation. - New typed errors (all extend
VaultError, machine-readable fields, exported):NotCapableError { backendType, capability },PresenceDeclinedError { backendType },PresenceTimeoutError { backendType, timeoutMs }.
CLI:
- New
vaultkeeper backend capabilities [--json]command lists each registered backend as{ type, displayName, presencePerUse }(a flat array with--json, human-readable text otherwise). - New per-command
--require-presence-per-useflag onstore,delete,sign, andexec(never global, never onverify). Withexec, a cached token is never reused under the flag.
- New
-
#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. -
#108
5958996Thanks @mike-north! - Make the zero-config default backend safe: the shortest documented getting-started path can no longer silently write a secret to your real OS credential store.- With no config file present, a bare
VaultKeeper.init()— andvaultkeeper config initwith no--backend— now resolves to the portable, self-containedfilebackend on every platform, including macOS and Windows. Previously it targeted the OS-native credential store (macOS Keychain, Windows DPAPI), so copy-pasting the first documented example could store a secret in the real login keychain before the user knew backends existed. - The platform-native store stays fully supported as an explicit opt-in:
vaultkeeper config init --backend keychain/--backend dpapi, or an explicit config. - New public
defaultBackendType()returns the zero-config default ('file') on every platform. The formerplatformDefaultBackendType()is renamed toplatformNativeBackendType()— it never was the zero-config default and now reads as what it is: the OS-native store you can opt into. - The
doctor/store(anddelete/exec/config show) "no config file" advisory now names thefiledefault and spells out the remediation asvaultkeeper config init --backend file, never a bareconfig init, so following the hint verbatim persists exactly the backend that was in effect.
Note: this changes only the TypeScript library and Node.js CLI. The native Rust CLI's zero-config default is unchanged for now.
- With no config file present, a bare
-
#76
1c115c8Thanks @mike-north! - sign() now throws a typed InvalidKeyMaterialError (instead of a raw OpenSSL decoder error) when the stored secret is not valid PEM/DER private key material; delegatedFetch() now wraps network failures in a typed FetchError instead of letting the raw fetch() rejection escape -
#159
68d8b9cThanks @mike-north! - Make signing and verification of arbitrary challenges a first-class, CLI-exposed primitive with a stable, third-party-verifiable signature format.- New CLI commands (
@vaultkeeper/cli):key create --name <n> --type ed25519provisions a signing keypair (unknown--typeexits 2, never a silent default);key export --name <n>prints the SPKI PEM public key;sign --name <n>reads all of stdin and writes exactly the detached signature to stdout (pipeline-safe; status on stderr);verify --public-key <pem> --signature <sig>verifies a detached signature fully offline (no config, backend, or key store).verifyadds exit code3for a signature that did not verify — a deliberate, documented exception to the0/1/2taxonomy so scripts can tell a bad signature from a broken tool. - Signatures are detached-payload Compact JWS (RFC 7515 §7.2.2 + RFC 7797
b64:false,crit:["b64"],algEdDSA/Ed25519). Any standards-compliant JOSE library can verify a signature given the payload and the public key. - Signing keys are a distinct resource from secrets: a new backend signing contract (
generateSigningKey/getPublicKey/signWithKey, mirroringListableBackend) keeps private key material backend-side. It never flows throughstore()/retrieve()/fetch()/exec()or a capability token's claims, andfetch()/exec()/getSecret()reject a signing-key token outright. Thefilebackend implements the contract; backends that do not fail with a typedSigningNotSupportedError. - Breaking (library): the
SignRequest/SignResult/VerifyRequestshapes andVaultKeeper.sign()/VaultKeeper.verify()are reshaped to the JWS contract.sign()now takes a signing-key capability token from the newauthorizeSigningKey()and returns{ jws };verify()is async and takes{ payload, jws, publicKey }. New public API:createSigningKey(),exportPublicKey(),authorizeSigningKey(),SigningBackend/isSigningBackend,SigningAlgorithm,SigningPublicKey, and theSigningKeyNotFoundError/SigningNotSupportedErrortyped errors.
- New CLI commands (
-
#91
11fe95dThanks @mike-north! - Add abackendoption toVaultKeeperOptionsthat accepts aSecretBackendinstance directly, so tests and embedders can inject a backend without registering it globally viaBackendRegistryor hand-assembling a fullVaultConfig. Whenbackendis set it always takes precedence over the backend thatconfig.backends(or the config loaded fromconfigDir) would otherwise resolve; other config fields still come fromconfig/configDirwhen supplied, and a minimal built-in default config is used automatically whenconfigis omitted. The README's "Testing your own code" and "Injecting a backend directly" sections document a dependency-injection pattern for testing code that usesVaultKeeper. -
#79
bfa32f3Thanks @mike-north! - Make the CLI's trust-on-first-use (TOFU) model functional.vaultkeeper approve --script <path>now computes the script's SHA-256 and records it in the trust manifest (idempotently), andvaultkeeper execconsults the manifest before prompting: a caller whose current hash is already approved runs without an interactive prompt and reports a verified trust state, while a modified or unapproved caller is treated as untrusted. The library gains two public methods onVaultKeeper—approveExecutable()andcheckExecutableTrust()— plus theExecutableTrustStatustype, which back this behavior. -
#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
-
#231
f863dfcThanks @mike-north! - Wrap config-directory CREATION failures in a typedFilesystemErrorinstead of leaking a raw Node error.vaultkeeper config init(and the firststore, which persists key state before writing any secret) against a config directory whose parent is read-only previously aborted with the raw, unwrappedError: EACCES: permission denied, mkdir '<path>'— no error class, no plain-English description, no fix hint. Only the config-directory READ paths had been wrapped previously.The directory-creation path now surfaces a typed
FilesystemError(with the path and the underlying errno code) rendered through the CLI's error formatter with directory-specific wording and a parent-directory fix hint (check that the parent directory is writable, or choose a writable location with--config-dir). The rawEACCES/mkdirerrno text no longer reaches the user, and the command still exits non-zero. The key-state write path is likewise wrapped. -
#140
94db84cThanks @mike-north! - Fix CLI error-experience papercuts:execanddeletenow report an identical "secret not found" message (Secret "<name>" not found in the "<backend>" backend.) with a recovery hint (Run \vaultkeeper store --name ` to create it.`) — previously each surfaced different wording and neither pointed at a fix.storewith empty stdin now exits 2 (usage error) instead of 1, consistent with a missing or empty--name— both mean "no usable input was given."- The library's
ConfigValidationErrormessage now separates the validation diagnosis from the remediation hint with a period instead of running them together with no punctuation.
-
#109
f24de45Thanks @mike-north! - Document the CLI's exit-code contract (0 success / 1 runtime error / 2 usage error, with an example
of each) and the--config-dirflag /VAULTKEEPER_CONFIG_DIRenv var in the CLI README. Note in
the library README that consumers need"type": "module"(vaultkeeper is ESM-only) and that
useLimitdefaults to unlimited (null) when omitted fromsetup()options.RotationInProgressError's message now includes a next step — runvaultkeeper revoke-key(or
callrevokeKey()) to invalidate the previous key immediately, or wait for the grace period to
elapse — matching the actionable-remediation style of the other domain errors. -
#226
eec6581Thanks @mike-north! - Fixed the CLI README's sign/verify walkthrough:signandverifynow both read the challenge viaprintf '%s', so each sees byte-identical stdin — the previous here-string form appended a trailing newline, making the documented example fail verification with exit 3. Shipped READMEs' runnable examples are now exercised by a CI example-fence check so a documented command sequence that stops working fails the build. -
#105
7f9da7aThanks @mike-north! - Add a supported recovery path for a corrupt or unreadableconfig.json.vaultkeeper config init --forcenow overwrites an existing config file, including one that's present-but-unparseable.config initwithout--forcekeeps its current non-destructive refusal, and now points atconfig init --forcein its refusal message.--forcecomposes with--backend(e.g.config init --force --backend file).ConfigParseError(and the otherloadConfigerrors sharing its remediation hint) now namesvaultkeeper config init --forceinstead ofvaultkeeper config init— the previous hint sent users to a command that provably failed in the exact state that produced the error.
-
#204
5c47f18Thanks @mike-north! - FixVaultKeeper.setup()recording TOFU (trust-on-first-use) trust for an executable before confirming the secret actually exists.#resolveExecutableIdentityran trust verification — which durably records a first-encounter or Sigstore hash in the trust manifest — beforebackend.retrieve(). Asetup()call for a nonexistent secret therefore still left the caller's hash permanently approved, letting an attacker (or a typo'd script) pre-seed TOFU trust without ever completing a legitimate first encounter; a later, real first encounter would then silently match the pre-seeded hash instead of being verified as new.Trust verification is now split into a verify phase (computes the hash and classifies it against the manifest, staging but not writing any first-encounter/Sigstore update) and a commit phase that only runs after
setup()'s secret retrieval and token minting succeed. Shape validation (missing/conflicting/legacy-'dev'-sentinel trust choices) still fails fast before any backend read, and a TOFU hash conflict still fails without ever recording the new hash — only the successful first-encounter write is deferred. -
#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
-
#206
88684f1Thanks @mike-north! - Surface the offending field indoctor's remediation for a schema-invalid config.For a config that parses as JSON but fails schema validation (e.g.
backends: []),doctor's Next-steps previously said only that the config was invalid, omitting the field-level reason it gives for JSON-parse errors (which name the line/column). ThePreflightCheckErrorstructured context now carries the offendingfieldfor aconfig-validationfailure — the validation analogue of a parse failure'slocation— sodoctorrenders it (e.g. "is invalid (backends)"), matching the wording every other command already used. Noreasonprose is parsed to do this. -
#221
2705b3aThanks @mike-north! - Validatebackends[].typeagainst the registered backends when loading a config, closing a gap wheredoctorreported a false "System ready." for a config naming a backend that does not exist.- Config validation (
loadConfig,validateConfig) now rejects abackends[].typethat names no registered backend. A config with an unknown type parses as valid JSON and is structurally valid, but the next real command would throwBackendUnavailableErrorat backend-creation time — sodoctorreporting all-clear undermined the diagnostic the CLI's own corrupted-config recovery points users at. doctor's config check now FAILS (red, exit non-zero) for an unknown backend type and names both the offending type and the valid options — the same guidance the runtimeBackendUnavailableErrorgives — instead of silently passing.- New public
UnknownBackendTypeError(aConfigValidationErrorsubclass) carries the offendingbackendTypeand theknownTypes. Thedoctorpreflight result gains aconfig-unknown-backenderror kind carryingbackendTypeandknownBackendTypes, so a consumer can render the valid-types guidance without parsing prose. - The valid set is read from the backend registry (including any custom backends a consumer registered), not a hardcoded list.
- Config validation (
-
#80
d511437Thanks @mike-north! - Fix the published.d.tsfailing to typecheck in strict consumer projects that scopecompilerOptions.typesexplicitly (TS2591: Cannot find name 'Buffer').SecretAccessor.read(),SignRequest.data, andVerifyRequest.datanow resolveBuffervia a realnode:bufferimport plus a/// <reference types="node" />directive in the published rollup, instead of relying on the ambient global.@types/nodeis now declared as an optional peer dependency. -
#82
2d848a7Thanks @mike-north! - Fix a security gap indelegatedExec:{{secret}}(or{{secret:name}}) in anyargselement was silently substituted with the raw secret value, exposing it on the process command line where it is visible to other processes viapsand often collected in logs and telemetry.exec()now throwsExecErrorif a placeholder appears inargs, matching the existingcommand-field guardrail. Inject secrets viaenvinstead.This is a breaking-in-practice fix: any caller that relied on placeholder substitution inside
argswill now getExecErrorand must move the secret intoenv. -
#107
9f06652Thanks @mike-north! - Fix thefilebackend's default storage directory diverging from the resolved config directory. With no explicitpathconfigured, secrets now land under<configDir>/file/— the same resolved config directory (honoring--config-dir/VAULTKEEPER_CONFIG_DIR,~/.config/vaultkeeperby default) that already holdsconfig.jsonand key material — instead of the hardcoded$HOME/.vaultkeeper/file. An explicitpathon the backend config still overrides this default unchanged.Back-compat:
retrieve/exists/delete/listtransparently fall back to the old$HOME/.vaultkeeper/filelocation when a secret isn't found under the new default, so secrets stored before this change remain reachable.storealways writes to the new location going forward — nothing at the old location is migrated automatically. -
#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. -
#78
414bb05Thanks @mike-north! - HonorBackendConfig.pathfor file-based backends. Previously the documentedpathoption was validated and then silently ignored: secrets always landed in the hardcoded$HOME/.vaultkeeper/<backend>location. Thefile,dpapi, andyubikeybackends now store, retrieve, and delete secrets under the configured directory (created on demand) whenpathis set, falling back to the default location when it is not. The CLIstoreanddeletecommands inherit the fix by routing throughVaultKeeper, which resolves the first enabled backend from config and forwards that backend's configuration.Config validation now rejects a whitespace-only
path(e.g." ") with the newConfigValidationError, instead of silently treating it as a real storage directory. -
#110
7f46237Thanks @mike-north! - Fix library error messages and public JSDoc that instructed users to run a barevaultkeeper config initas if the CLI shipped with thevaultkeeperpackage. The library has nobin— the CLI ships separately as@vaultkeeper/cli. Remediation text inConfigParseError,ConfigValidationError,FilesystemError(vialoadConfig), and JSDoc ondefaultBackendType,platformNativeBackendType,loadConfig, andVaultKeeperOptionsnow name@vaultkeeper/cliexplicitly, or point to the JS-API alternative of repairing/replacing the config directly (viaconfig/configDir). The README now states near the top that the CLI ships separately. -
#229
f6e692bThanks @mike-north! - Fix the README "Multiple secrets in one request" example so it runs verbatim. It
authorizedAPI_KEYandDB_PASSWORDwithout storing them first, so a
copy-pasted run threwSecretNotFoundErrorbefore reaching the network. The
example is now self-contained (importsVaultKeeper, initializes, and stores
each secret) and is executed against the built package in CI — not just
type-checked — so a runtime-throwing example fails the build. -
#77
26c876cThanks @mike-north! - Ship a package-specific README.md andrepository/homepage/bugsmetadata with every published package, so registry consumers get install instructions and a quick start without leaving npm. -
#111
ebfcd1dThanks @mike-north! - Make the packaged READMEs self-contained:vaultkeeperand@vaultkeeper/clinow include a
minimal, safe-by-default (filebackend) exampleVaultConfig/config JSON, plus inline
explanations of key rotation grace periods, thetrustTierpolicy label, and the
trust-on-first-use (TOFU) check thatexecreports on every run — so the golden path no longer
depends on fetching the unshipped repository README. -
#164
ce43052Thanks @mike-north! - Complete the plain-Erroraudit (issues #115/#126 coveredconfig.tsand the file backend): every remainingthrow new Error(...)in product source now throws a typed error instead.vaultkeeper:util/at-rest.tsandbackend/yubikey-backend.ts's encrypted-envelope decoding now throwDecryptionError(malformed envelope, unsupported/legacy file version, or a failed AES-GCM auth tag check) instead of a plainError.util/platform.ts'scurrentPlatform(),backend/one-password-constants.ts'sgetIntegrationVersion(), andyubikey-backend.ts's YubiKey HMAC response validation now throwSetupError.util/exec.ts'sexecCommand/execCommandFulland the YubiKeyykmanchallenge-response call now throwExecError.OnePasswordBackend's constructor validation (mutually exclusiveaccessMode/serviceAccountToken/accountoptions) now throwsConfigValidationError, and its per-access worker crash/spawn-failure paths now throwBackendUnavailableError. No new public error classes or fields were added — every site reuses an existingVaultErrorsubclass.@vaultkeeper/cli: the non-interactive-approval-required error (in bothapproval.ts'spromptApprovalandcommands/exec.ts's trust gate) now throws a new internalNonInteractiveApprovalError(not part of the public API, matching the existing internalConfigDirFlagErrorpattern) instead of a plainError.
A new repo-wide guard test (
no-plain-error.test.tsin both packages) scans every source file undersrc/and fails if a plainErrorconstruction (throw/reject) reappears. -
#175
f20ea5aThanks @mike-north! - Round out the shipped package docs so a reader offline (registry-only, air-gapped) can find everything without the GitHub URL:vaultkeeperREADME: new "Multiple secrets in one request" section documentingSecretTokenMapand the{{secret:name}}placeholder for injecting several secrets into onefetch()/exec()call; a runnable inlineexec()example (secret injected viaenv); a complete error-types table covering allVaultErrorsubclasses; a fullVaultConfig/BackendConfigfield reference; and a "Doctor / preflight checks" section explaining required-vs-informational checks and that a plugin checkmark means "binary detected on PATH", not "backend active".vaultkeeperREADME:verify()now notes that the disallowed-algorithm throw does not apply to Ed25519/Ed448 keys (the algorithm override is ignored). The "Testing against this library" section notes@vaultkeeper/test-helpersbelongs indevDependenciesand warns that the realVaultKeeper.setup()always requiresexecutablePathorskipTrust.@vaultkeeper/test-helpersREADME: strengthened the warning that the test-only zero-argsetup()default does not carry over to the realVaultKeeper.setup().@vaultkeeper/cliREADME: new "Doctor / preflight checks" section on checkmark semantics — plugin checks (op/ykman) are informational when their backend isn't enabled, but enabling the1password/yubikeybackend promotes its tool check to required; points at the now-self-contained library README for the full error hierarchy and config reference.
-
#119
0c1daefThanks @mike-north! - Close doc gaps left over from the shipped-README audit:vaultkeeper's and@vaultkeeper/cli's READMEexecexamples now mention the default[REDACTED]output redaction and the--no-redactescape hatch inline.- The
vaultkeeperpackage README now inlines a development-mode explanation, asign()/verify()example, and a brief error-hierarchy summary instead of deferring them solely to the unshipped repository README;@vaultkeeper/cli's README gets an inline development-mode explanation too. - States a supported TypeScript version (5.x) in both READMEs, and documents a
require()/CommonJS quick-start variant alongside the existing ESM one. verify()'s JSDoc now calls out that it is synchronous and throws immediately (not via a rejectedPromise) for a disallowed algorithm.- Adds a
./package.jsonsubpath tovaultkeeper'sexportsmap.
-
#220
e800683Thanks @mike-north! - Polish setup() editor guidance and CLI/README papercuts.setup()compile-error hint. BothVaultKeeper.setup()in thevaultkeeperlibrary and@vaultkeeper/wasmnow carry a TSDoc note that names the exact compile errors a missing trust choice produces (TS2554/TS2345) and the two remedies — add exactly one ofexecutablePathorskipTrust: true— so hovering the call in-editor explains the fix rather than leaving the bare compiler message. The WASMsetup()also gains a runnable@example.useLimit"use" semantics documented. The README now spells out thatuseLimitbounds calls tovault.authorize(jwe), not downstream delegatedfetch()/exec()/getSecret()calls: eachauthorize(jwe)consumes one use, and the resultingCapabilityTokencan be reused across many delegated calls; only a secondauthorize(jwe)throwsUsageLimitExceededError.verifyinline-PEM parsing.vaultkeeper verify --public-key/--signaturenow reject inline PEM material with a clear, actionable usage error (exit 2) instead of node's opaque "argument is ambiguous" — the flags are file-path-only, and the message says so and points at the--public-key=<path>escape for a path that legitimately begins with a dash.Unknown-command suggestion. An unrecognized subcommand now prints an npm/git/cargo-style
Did you mean '<closest>'?suggestion (e.g.doctro→doctor) plus a one-line pointer tovaultkeeper --helpand the docs, giving tarball-only users a discovery path.README Quick Start. The CLI Quick Start code block now includes an inline
--config-dir/VAULTKEEPER_CONFIG_DIRreminder so a copy-paster gets the isolated-config guidance that was previously only in prose. -
#224
6b6d11aThanks @mike-north! - Fix a late TOFU conflict window incommitTrust: if another process recorded a different executable hash for the same trust-manifest namespace between the verify and commit phases,commitTrustreloaded the manifest but then unconditionally merged the staged hash in — silently approving a second hash for that namespace and bypassing the TOFU-conflict record-nothing rule.commitTrustnow re-classifies the staged entry against the freshly reloaded manifest: an already-trusted hash stays a no-op, an empty namespace still merges, but a namespace whose approved hashes don't include the staged one now throwsIdentityMismatchErrorand writes nothing. Mirrors the RustPendingTrust::commitfix. -
#128
8ded257Thanks @mike-north! - Replace the guessed "TypeScript 5.x" README note with the actually-tested range: a CI matrix (packages/vaultkeeper/test/e2e/consumer-typecheck.test.ts) now typechecks the shipped.d.tsofvaultkeeper,@vaultkeeper/test-helpers, and@vaultkeeper/cli-test-helpersagainst pinned TypeScript 5.0.4, 5.9.3, 6.0.3, and 7.0.2 compilers — all pass, so both READMEs now state a tested 5.0.4–7.0.2 range instead of a narrower guess. -
#186
a4a6cb7Thanks @mike-north! - Validate the signing/verification algorithm before parsing key material inVaultKeeper.verify()(and the internal signing path). A disallowed algorithm (e.g.md5) now throwsInvalidAlgorithmErrorunconditionally and synchronously, as documented — even when the supplied key material is also malformed. Previously a malformed public key short-circuited tofalseand silently skipped the algorithm guard, so callers relying ontry/catchforInvalidAlgorithmErrorwere not protected when key material was attacker-controlled. -
#176
f2baa86Thanks @mike-north! - Close the@vaultkeeper/wasmgetting-started and API-reference documentation gaps.- The WASM quick start now leads with an ESM-setup callout.
@vaultkeeper/wasmis ESM-only (no CommonJS fallback), so a copy-paste of the snippet into a defaultnpm init -y(CommonJS) project previously failed withSyntaxError: Cannot use import statement outside a module. The callout documents adding"type": "module"first, so the documented steps now succeed from a fresh project. SetupOptions.executablePathJSDoc (and the generated API reference) now states positively that this WASM SDK records the path as a claim label and performs no trust-on-first-use (TOFU) verification — no hashing, manifest check, or throw on a changed/nonexistent path — unlike the TypeScriptvaultkeeperlibrary'sVaultKeeper.setup(). Cross-references the behavioral follow-up tracked separately.- The
vaultkeeperREADME Trust-tiers section now scopes its "requires an explicit executable-trust choice / never silently skips verification" guarantee to the TypeScript library, and notes that@vaultkeeper/wasmrecordsexecutablePathas a claim label without running TOFU verification. SetupOptions.backendTypeis now documented as a claim label only (recorded in the token'sbkdclaim) that does not select or route through a functional backend, mirroring the claim-label framing ofexecutablePath.
- The WASM quick start now leads with an ESM-setup callout.