Continues the security-review fixes.
Durability and concurrency:
- Vault, config, fingerprint-registry and parent-seed writes are now atomic:
temp file in the same directory, fsync, rename, then an fsync of the
directory. Previously the target was truncated and rewritten in place, so
a crash could destroy the vault.
- The lock now spans the whole read-modify-write cycle, not just the write.
Serializing writes alone still lost changes -- two commands each read the
same index, mutated their copy, and wrote in turn. Every mutating command
goes through mutateVault(), which holds the lock from open to save. A test
runs five concurrent writers and asserts all five entries survive.
- 0600 is now enforced on existing files too: writeFile's mode argument only
applies on creation, so a loosened vault stayed loosened.
- The index is schema-validated before it can reach disk.
Path handling:
- Profile fingerprints are validated (16 uppercase hex) before being used in
any path, and removeProfile additionally checks the resolved directory
still sits directly under the app directory. A tampered registry could
otherwise have steered a recursive delete elsewhere.
- Malformed entries in fingerprints.json are dropped on read.
Restore semantics:
- `nostr restore` merges remote state into the local vault instead of
replacing it. Replacing silently discarded entries created since the last
sync; that now requires --replace --yes, and any overwritten vault is
copied aside first.
- Deltas are bound to the manifest they extend (filtered on the e tag, and
re-checked locally), so a relay cannot replay a validly-signed delta from
another snapshot lineage into a restore.
- Manifest selection breaks created_at ties by relay order, so a same-second
republish cannot leave a restore on a stale snapshot.
- mergeIndexPayloads gained an index0 option. The default still refuses to
merge populated Python atlas state rather than drop it; restore uses
preserve-current, keeping the local block verbatim, because Python
recomputes canonical views on load and the default would block every
restore into a Python-created profile.
193 core + 69 CLI tests green; 36/36 cross-implementation checks green.
Co-Authored-By: Claude Fable 5 <[email protected]>
Ran the full CLI surface against a freshly generated throwaway seed
(profile -> agent unlock -> all 8 entry kinds -> reveal/sinks -> tokens ->
audit -> backup -> relay sync -> seed-only recovery). Four defects that no
unit test had caught:
- clipboard sink hung the CLI forever. xclip/wl-copy fork a resident
process to own the X/Wayland selection; it inherited our stdio, so any
reader of the CLI's stdout blocked indefinitely, and waiting for process
exit never returned. Now detached with ignored stdio, success declared
once stdin is flushed.
- 'vault import' read the mnemonic straight from the env, so it failed
whenever the vault was unlocked through the agent -- the normal path.
- 'vault import' never wrote anything; it only printed a summary. It now
restores into the active vault, refuses to overwrite a non-empty vault
without --yes, and keeps the old behavior behind --inspect.
- A corrupt/foreign vault file surfaced the raw codec error "invalid token
encoding". It now names the actual likely causes.
Also documented the variadic-option limitation that a trailing flag is
lost (commander stops collecting at the next dash-prefixed token): a
quoted command spec now splits on whitespace, no shell involved.
Regression tests for all four. 151 core + 44 CLI tests green.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Portable backup (format_version 1): TS imports Python exports, re-exports
byte-identically under pinned nonce/timestamp, verifies the canonical
checksum, rejects tampering and unknown versions.
- .github/workflows/ts-parity.yml: typecheck + Node/jsdom parity suite on
js/ changes, plus a fixture-drift job that regenerates fixtures from
Python and fails if the committed ones don't match (manifest commit field
excluded by construction).
- packages/cli — seedpass-js MVP implementing plan section 9.3:
* reference-first output: entry list/get/search emit sp://entry/<id> refs
and metadata; secret-bearing fields become has_* flags
* 'entry reveal' is the only plaintext egress command
* 'use --clipboard|--exec|--stdin-to' delivers secrets to sinks; --exec
injects SEEDPASS_SECRET into the child env, never argv or stdout
* vault export/import over portable backups; capabilities --format json
* mnemonic comes from SEEDPASS_MNEMONIC env, never the command line
- CLI tests prove the blindness properties: derived password matches the v2
fixture, TOTP at pinned time, secrets absent from all default output and
from sink-delivery confirmations.
- 125 core parity tests (Node + jsdom) + 11 CLI tests green.
Co-Authored-By: Claude Fable 5 <[email protected]>