Files
SeedPass/js/packages/cli
thePR0M3TH3ANandClaude Opus 5 c6cb85e840 feat(vault): port index0/atlas, and keep it current on every mutation
index0 is the per-vault activity ledger under `_system.index0`: an append-only
event stream per writer, daily checkpoints rolling those events up, and
derived canonical views (children_of, counts_by_kind, recent_activity).

It was the last piece carried through verbatim rather than computed — which
was safe for round-tripping but meant a TypeScript client never UPDATED it. A
vault written by this port went stale the moment it was touched, and a Python
client reading it afterwards saw an activity log missing everything the TS
client had done. Worse, `nostr restore` kept the local block verbatim, so a
restore silently discarded the remote's history.

Hash parity is the entire contract here: event ids, integrity hashes,
checkpoint summaries and view hashes are SHA-256 over Python's canonical JSON,
and the merge picks winners by comparing those hashes — one byte of encoding
disagreement means the two implementations permanently disagree about which
record wins. Everything routes through the existing canonicalJson (Python's
ensure_ascii escaping, code-point key ordering, CPython number formatting)
rather than JSON.stringify, and the tests compare whole structures via
canonical JSON rather than toEqual, so they pass only if the bytes that get
hashed are identical. Verified against Python fixtures covering the append
chain, compaction, manifest metadata, views and a two-replica merge —
including non-ASCII labels and tags, a non-numeric entry id, two writers
across two days, and a managed-account scope path.

Two Python quirks are replicated deliberately rather than "fixed", because
diverging would change which events exist rather than merely how they hash:
`str(None)` is "None" (so a JSON null in a required field is a non-empty
string there), and a stored integrity_hash is preserved even when it
disagrees — this normalizes, it does not verify, and silently rewriting a
mismatched hash would hide the tampering the hash exists to reveal.

Events are emitted by DIFFING the index at the single mutation funnel each
surface goes through, rather than from each call site as Python does. That is
deliberate: a call-site emitter is something a new code path can forget, and a
silently incomplete ledger is worse than an absent one because it reads as
"nothing happened". The event vocabulary matches Python's, so a stream from
either implementation reads the same. Emission is best-effort — the atlas is
derived state, and it must never fail a committed vault write.

mergeIndexPayloads now defaults to merging index0 with the same deterministic
rules; "preserve-current" and "reject" remain for callers that want them, and
`nostr restore` no longer discards the remote's history.

Also made gen_high_risk deterministic. Fernet embeds a random IV and the
current time, so that fixture regenerated differently every run and would have
failed the fixture-drift CI job each time — it now pins both, with a note that
production must never do this.

Verified end to end: the TS binary performed four mutations, and Python read
the resulting atlas, normalized it without dropping a record, and compacted it
into checkpoints and views.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-19 11:13:15 -04:00
..
2026-08-14 21:33:28 -04:00
2026-08-14 21:33:28 -04:00

seedpass-js

The SeedPass command-line interface, TypeScript port.

It reads and writes the same ~/.seedpass profiles as the Python implementation — same derivations, same vault format, same Nostr sync — and adds a session agent plus a scoped-token surface designed for AI agents and automation.

Install

Requires Node.js 22 or newer.

# from a checkout
cd js && pnpm install && pnpm --filter @seedpass/cli build
node packages/cli/dist/seedpass-js.mjs --version

The build produces one self-contained file (dist/seedpass-js.mjs) plus a .sha256 next to it. Copy it anywhere on your PATH; it has no runtime dependencies beyond Node itself.

Quick start

export SEEDPASS_APP_DIR=~/.seedpass          # omit to use the default

# Create a profile (mnemonic and password come from the environment,
# never from the command line)
export SEEDPASS_MNEMONIC="your twelve or twenty-four words"
export SEEDPASS_PASSWORD="your master password"
seedpass-js fingerprint add --name main
unset SEEDPASS_MNEMONIC                      # not needed again

# Unlock once per session through the agent
seedpass-js agent start &
seedpass-js vault unlock --ttl 900

seedpass-js entry add password github.com --length 20 --username you
seedpass-js entry reveal github.com
seedpass-js entry totp-codes
seedpass-js use github.com --clipboard
seedpass-js vault lock

Design: reference-first output

Default output never contains secret values. Entries are addressed by reference (sp://entry/<id>), and secret-bearing fields are reported as has_* flags:

$ seedpass-js entry get api-token
{ "id": "5", "ref": "sp://entry/5", "label": "api-token",
  "kind": "key_value", "has_value": true, ... }

Plaintext leaves the vault only through commands that say so (entry reveal, entry totp-codes, entry export-document, util generate-password), or through a sink, which delivers a secret to its destination without routing it through this process's output:

seedpass-js use api-token --exec ./deploy.sh    # SEEDPASS_SECRET in the child env
seedpass-js use api-token --stdin-to "wc -c"    # quote specs that contain flags
seedpass-js use api-token --clipboard

Scoped tokens for agents

An agent can be given a token that grants exactly what it needs and nothing else. Secrets are materialized inside the session agent, so the token holder never receives the seed and can never escalate to owner operations.

# As the owner:
seedpass-js agent token-issue --scope read use \
    --kind key_value --label-regex '^api-' --uses 2 --ttl 600

# In the agent's process — no mnemonic, no password:
export SEEDPASS_TOKEN=<token>
seedpass-js entry list                       # metadata only
seedpass-js use api-token --exec ./deploy.sh # permitted
seedpass-js entry reveal api-token           # denied: scope 'reveal' not granted

# Back as the owner — every grant, delivery and denial is recorded:
seedpass-js agent audit-verify
seedpass-js agent audit-tail -n 20

Environment

Variable Purpose
SEEDPASS_APP_DIR Profile directory (default ~/.seedpass)
SEEDPASS_MNEMONIC Parent seed; needed for profile creation, otherwise use the agent
SEEDPASS_PASSWORD Master password, for fingerprint add and vault unlock
SEEDPASS_TOKEN Scoped token; puts the CLI in token mode
SEEDPASS_AGENT_SOCK Override the agent socket path
SEEDPASS_SECRET Set by use --exec in the child process only

Compatibility

Interoperability with the Python implementation is verified on every change by scripts/cross_impl_check.py: each side opens the other's profiles, backups round-trip, edits are mutually visible, conflict merges agree, and a snapshot published by Python restores here with secrets intact. See docs/typescript_port_compatibility_matrix.md.

Not yet ported: PGP RSA keys (not byte-reproducible; ed25519 is at parity), and the semantic and api command groups.