The last unported derivation, and the one the plan flagged as highest risk.
It turned out to be fully reproducible: the key material is deterministic,
EdDSA signatures are deterministic (RFC 8032), and the packet layout is
fixed -- so the armored blocks match exactly, with no OpenPGP dependency.
core/derive/pgp.ts emits the packets directly: new-format headers, the
secret-key packet (unencrypted MPI plus 16-bit checksum), the user-id
packet, and the self-certification signature with PGPy's exact subpacket
set and order, wrapped in ASCII armor with a CRC-24 footer. Verified
against PGPy across two seeds, three indices and three user-id shapes --
armored private key, armored public key, and fingerprint.
RSA is deliberately unsupported: PyCryptodome's seeded prime search is not
reproducible, so the TS port refuses the key type rather than silently
deriving a different key.
Two things worth recording from the build: the v4 fingerprint reproduces
from first principles (SHA-1 over the public-key packet), which is what
made byte-parity look feasible before committing to it; and the only
mismatch in the first run was packet framing -- PGPy uses new-format
headers where I had emitted old-format.
CLI: `entry add pgp`, reveal (armored private key), `entry pgp-public`.
Cross-impl phases A/B now cover a pgp entry in both directions, completing
coverage of every creatable entry kind.
193 core + 47 CLI tests green; 30/30 cross-implementation checks green.
Co-Authored-By: Claude Fable 5 <[email protected]>
- core/derive/ssh.ts: BIP-85 app 32 entropy used directly as the Ed25519
private key, serialized as PKCS#8 and SubjectPublicKeyInfo PEM. Both DER
shapes are fixed for Ed25519, so they are emitted from literal prefixes
rather than adding an ASN.1 dependency to the crypto path.
- Output matches Python's `cryptography` byte-for-byte across two seeds and
three indices, including PEM line wrapping.
- CLI: `entry add ssh`, reveal (private key PEM), and `entry ssh-public`
which prints the public key without touching secret material. The
OpenSSH authorized_keys line is a TS-only convenience -- Python emits
only PEM, which ssh(1) cannot consume directly -- and is marked as such
in the compatibility matrix.
- Cross-impl phases A and B now include an ssh entry in both directions.
One harness fix: PEM values carry a trailing newline that the CLI capture
strips, so the cross-impl comparison normalizes trailing whitespace. The
unit tests still compare exactly.
170 core + 46 CLI tests green; 16/16 cross-implementation checks green.
Co-Authored-By: Claude Fable 5 <[email protected]>
This was the one remaining gap that could strand a real user: TS refused
any index below schema_version 4, which is every profile written before
tags were added.
- core/schema/migrations.ts ports seedpass.core.migrations exactly,
including the quirks: v1->v2 copies `website` to `label` and drops it
only for password entries; `kind` is NOT backfilled (Python leaves
pre-v2 entries `type`-only and falls back on read). Migration works on a
copy, so a failure can never leave a half-converted vault.
- parseVaultIndex now migrates on open and still refuses future versions;
pass {migrate:false} where silent upgrades are unwanted.
- Two bugs this surfaced: the version probe could not read a v0 index (no
schema_version field at all), and migrated legacy entries failed schema
validation because the discriminator `kind` was missing. The parser now
fills `kind` from `type` at validation time -- the same shape Python
writes for new entries, documented in the matrix.
Evidence: fixtures generated by running Python's own apply_migrations over
v0/v1/v2/v3 payloads, plus cross-impl phase E, which writes a real legacy
v2 index into a Python profile, opens it with the TS CLI, reveals a
migrated secret, and confirms Python still reads the profile afterwards.
162 core + 46 CLI tests green; 16/16 cross-implementation checks green.
Co-Authored-By: Claude Fable 5 <[email protected]>
- core entryMod: modify_entry port with the per-kind allowed-field matrix,
policy merging, blacklisted-field cleanup and timestamp touching;
archive/restore; typed links (add/remove/get with Python's normalization,
dedupe on target/relation/note, self-link and missing-target refusal).
- Parity fixture entry_mods.json: the real Python EntryManager applies a
10-op modification sequence at a later pinned clock; TS replays the same
ops from the creation state and matches byte-for-byte.
- CLI: entry modify/archive/unarchive/links/link-add/link-remove,
entry totp-codes (explicit egress, honors --at and archived state),
util generate-password (Python parity: index 0 + gen v1 defaults, full
policy flags — v1/v2 outputs verified against password fixtures).
- capabilities: egress command list updated.
- 148 core parity tests (Node + jsdom) + 22 CLI tests green.
Co-Authored-By: Claude Fable 5 <[email protected]>
- NIP-01 event serialization/id computation, BIP-340 signing and
verification via @noble/curves, SeedPass event builders (manifest 30070,
chunk 30071, delta 30072 with d/e tags), and REQ/EVENT/EOSE/OK/NOTICE/
CLOSED client-relay framing. Transport-agnostic; the WebSocket adapter
lands with the interface milestones.
- Cross-implementation fixtures generated through the Python side's
nostr_sdk (rust-nostr) with pinned created_at: TS ids match theirs
byte-for-byte, their signatures verify in TS, and TS-signed events
reproduce identical ids. The generator caches signatures across runs
(BIP-340 aux randomness) so fixture output stays byte-deterministic and
the CI drift job holds.
- 143 core parity tests (Node + jsdom) + 16 CLI tests green.
Co-Authored-By: Claude Fable 5 <[email protected]>
- core entryOps: add* functions for all creatable kinds rebuild the Python
EntryManager fixture index byte-for-byte under a pinned clock — field
shapes, id allocation, independent TOTP derivation-index allocation, and
Python's ISO timestamp format all verified.
- CLI 'entry add password/totp/key-value/document/seed/managed-account/
nostr': provision-blind — returns the reference and metadata, never the
secret the entry represents; tests assert the derived password / imported
secret / child mnemonic appear nowhere in provisioning output.
- Parity bug found and fixed by the new entry_secrets fixtures: nostr ENTRY
keys derive via the default BIP-85 app 39 path (EntryManager), not app
1237 (sync client identity); the CLI had used 1237. Reveal values for
password/TOTP/nostr/seed/managed-account now match Python retrieval.
- 129 core parity tests (Node + jsdom) + 16 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]>
- Snapshot model parity (kinds 30070/30071/30072): gzip chunking via
platform CompressionStream, chunk-hash verification, reassembly of
Python-produced chunks, manifest JSON schema, manifest id
HMAC-SHA256(key_index, 'manifest|'+nonce) with the key_index HKDF chain.
- Full merge_index_payloads port (modified_ts_hash_tombstone_v2):
Python-compatible canonical JSON (sort_keys + ensure_ascii) for hash
parity, equal-ts field-level union, tombstone create/prefer/apply/cap,
_sync_meta bookkeeping. Populated _system.index0 content refuses to merge
loudly (atlas-milestone work) instead of silently dropping data.
- Delta replay: encrypted V3 deltas decrypt and merge in order with
source_tag = sha256(payload)[:16]; final state matches Python.
- New fixtures: nostr_snapshot.json, sync_merge.json (8 scenarios),
delta_replay.json; generator remains byte-deterministic.
- 117 parity tests green in Node and jsdom.
Co-Authored-By: Claude Fable 5 <[email protected]>
- scripts/generate_ts_port_fixtures.py: deterministic fixtures from public
test mnemonics (passwords v1+v2, BIP-39/85, TOTP, Nostr keys, managed
seeds, fingerprints, index keys, entry schemas, V3 vault payload).
Byte-identical across runs; v1 values match the frozen entropy-integrity
vectors.
- js/: pnpm workspace with @seedpass/core and @seedpass/test-vectors.
Core implements BIP-85, password gen v1 (frozen) + v2, TOTP, Nostr keys,
fingerprints, vault index key, and V3 AES-GCM decrypt using only
noble/scure crypto + WebCrypto. Strict TS, no Node/DOM deps in core.
- 82 parity tests pass in Node and a jsdom browser-like environment.
- docs/typescript_port_compatibility_matrix.md tracks per-area status.
Co-Authored-By: Claude Fable 5 <[email protected]>