Blocker 1: import_identity persisted-before-cleanup ordering. New
commit_imported_identity helper runs durable persistence FIRST; a failed
different-key import now leaves both the old in-memory identity and its
valid canonical identity.ncryptsec intact. Stale-backup cleanup runs after
the durable commit and is deliberately best-effort (logged, not surfaced
as a half-applied-import error). Regression tests cover the failure path
and prove cleanup cannot precede persist.
Blocker 2: the /events inventory tripwire is now site-granular. Each
inventoried file pairs an expected non-comment /events occurrence count
with an expected egress-guard call count, so an unguarded ninth site in an
already-listed file (or a deleted guard call) fails the scan. The scan
core is pure over (path, content) pairs, with mutation-style tests
demonstrating all three drift classes.
Hardening: bech32 permits an all-uppercase encoding, so the egress guard
now rejects NCRYPTSEC1... too (mixed case stays unblocked - it cannot
decode), and both import classifiers (Rust recover_keys_from_input, TS
classifyKeyImportInput/isPlausibleNcryptsec) route uppercase-valid blobs
to the encrypted path consistently.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Frontend half of PLANS/NIP49_LOCAL_BACKUP_PLAN.md Rev 3 (D3):
- BackupStep: encrypted by default. The default path never invokes
get_nsec — the webview only ever sees the finished ncryptsec1 blob
returned by create_ncryptsec_backup. The raw key path survives behind
an explicit 'Show raw key instead' click with its previous
loading/error/skip semantics.
- EncryptedBackupCreator (shared by onboarding + settings): generated
6-word passphrase default with cannot-be-recovered copy, 'choose my
own' = min 12 chars + confirmation, create -> masked display + copy +
'Save a copy…' via the native dialog.
- encryptedBackup.ts: pure reducer/validation model, unit-tested without
a DOM; keyImportInput.ts: HRP classification + submit gating.
- NsecMaskedDisplay: kind='nsec'|'ncryptsec' drives labels/aria/testids;
existing nsec call sites unchanged.
- NostrKeyImportForm: ncryptsec1 paste switches to encrypted mode
(passphrase field, no npub preview possible), decrypt errors surface;
raw nsec flow untouched. import_identity plumbs the optional password
through OnboardingFlow/MachineOnboardingFlow/KeyringLockedScreen.
- ProfileSettingsCard: 'Encrypted backup' row alongside the raw reveal.
- ncryptsecSourceScan.test.mjs: TS-side source-allowlist tripwire
mirroring the Rust scan (defense-in-depth per plan D4).
- e2e: mock bridge learns the three backup commands + ncryptsec import;
onboarding-backup.spec.ts covers the encrypted happy path (asserting
get_nsec is never called), custom-passphrase validation, raw fallback,
and error/retry; onboarding.spec.ts adds encrypted-import happy path
including a wrong-passphrase rejection.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>