Recoverable identities: IdentityManager engine (0243 Phase 1, step 2)#322
Merged
Conversation
Opt-in recoverable identities on the IdentityManager: born from a recovery phrase, gated by a passkey, recoverable on any device by typing the phrase. The default create() path is unchanged (stronger PRF-derived, nothing at rest). - createRecoverable(): mint from a fresh phrase, enroll a gating passkey, return the phrase to show once. - importRecoveryPhrase(phrase): adopt on a new device (lost passkey); reproduces the same DID + X25519 key. - exportRecoveryPhrase(): reveal behind a passkey gate (Settings); null for non-recoverable identities. - isRecoverable(): whether the stored identity has a saved phrase. The sealed phrase persists alongside the encrypted bundle (storeIdentity gains an optional recovery arg). Mirrored in the test-bypass manager. 4 new manager tests (same-DID round-trip across a simulated device wipe, invalid-phrase rejection, PRF-identity-is-not-recoverable). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: xNet Test <test@xnet.dev>
Contributor
|
✓ Changelog fragment found — thanks! |
Contributor
|
Preview removed for PR #322. |
crs48
added a commit
that referenced
this pull request
Jun 28, 2026
Third step of **Phase 1** of [exploration 0243](docs/explorations/0243_[_]_ACCOUNT_VALIDATION_AND_RECOVERY_BINDING_THE_PAYER_TO_THE_PASSKEY.md). Makes the recoverable-identity engine (#320, #322) user-reachable in onboarding — completing **P1.1** and **P1.3**. ## What this adds (opt-in, default unchanged) - **Create:** the welcome screen gains *"Set up a recovery phrase too"* → mints a recoverable identity and shows the 24-word phrase once on a [ShowRecoveryPhraseScreen](packages/react/src/onboarding/screens/ShowRecoveryPhraseScreen.tsx) with a copy button and an "I've saved it" gate before continuing. The plain passkey path is untouched. - **Recover:** *"Enter recovery phrase"* now opens a real [RecoveryPhraseScreen](packages/react/src/onboarding/screens/RecoveryPhraseScreen.tsx) that validates against the wordlist live (flags typos / short phrases), then reproduces the **same DID and X25519 key** on the new device and enrolls a gating passkey. New machine states (`creating-recoverable`, `show-recovery-phrase`) and events (`CREATE_RECOVERABLE`, `SUBMIT_PHRASE`, `RECOVERABLE_CREATED`, `PHRASE_SAVED`, `IMPORT_FAILED`); the `OnboardingProvider` runs the `IdentityManager` side-effects. 6 new machine-transition tests (24 onboarding tests pass); `@xnetjs/react` typechecks clean. ## Still ahead in Phase 1 - **P1.2** — Settings → Account "view recovery phrase" (uses `exportRecoveryPhrase()`) + Electron `secure-seed`. - **P1.4** — surface synced-passkey recovery (`discoverExistingPasskey`) in the import flow. Changeset: `@xnetjs/react` minor + a user-facing changelog entry. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
crs48
added a commit
that referenced
this pull request
Jun 28, 2026
#327) Phase 1 polish for [exploration 0243](docs/explorations/0243_[_]_ACCOUNT_VALIDATION_AND_RECOVERY_BINDING_THE_PAYER_TO_THE_PASSKEY.md) — completes **P1.2**. ## What this adds Settings → Account gains a **Recovery phrase** row: - For recoverable identities: a **View phrase** button reveals the phrase behind a passkey prompt (`identityManager.exportRecoveryPhrase()`), with consent copy that it restores your data but can't be vendor-recovered. A **Hide** toggle keeps it off-screen by default. - For plain (non-recoverable) identities: it says plainly there's no phrase, so the user knows a lost passkey means lost access. - In the desktop app, a **Back up to this device** button stores the phrase in the OS keychain via the existing `secure-seed` IPC (gated on `window.xnet`). `apps/web` typechecks clean; the reveal path is exercised by the engine's `exportRecoveryPhrase` tests from #322. ## Remaining in Phase 1 - **P1.4** — surfacing synced-passkey recovery (`discoverExistingPasskey`) in the import flow. Deliberately deferred: it's a convenience path on top of the phrase recovery already shipped, and it can't be meaningfully verified headlessly. Documented as a follow-up. Next: **Phase 2** — the 0149 account/device ledger. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Second step of Phase 1 of exploration 0243. Builds on the phrase primitives from #320 to make recovery real at the identity layer.
What this adds
Opt-in recoverable identities on the
IdentityManager. The defaultcreate()path is untouched — it stays the stronger PRF-derived identity that stores nothing key-related at rest. Recovery is a separate, explicit choice (per the agreed Phase-1 model):createRecoverable()— mint an identity from a fresh recovery phrase, enroll a gating passkey, return the phrase to show once.importRecoveryPhrase(phrase)— adopt an identity from a phrase on a new device (lost passkey); reproduces the same DID and X25519 key, so it regains access to the same E2E-encrypted data.exportRecoveryPhrase()— reveal the phrase behind a passkey gate (for the Settings "view phrase" panel); returns null for non-recoverable identities.isRecoverable()— whether the stored identity has a saved phrase.The sealed phrase is persisted alongside the encrypted bundle (storage.ts
recoveryfield;storeIdentitygains an optional arg), and anenrollRecoverableIdentityhelper (passkey/recoverable.ts) creates the gating passkey from a phrase-born bundle. Mirrored in the test-bypass manager.4 new manager tests (jsdom + mocked WebAuthn + fake-indexeddb): same-DID round-trip across a simulated device wipe, invalid-phrase rejection, and "a PRF identity is not recoverable / exports no phrase".
@xnetjs/identitytypechecks clean; 35 identity tests pass.Scope
This is the engine. The onboarding UI (a "Create a recoverable identity" option + the recovery-phrase entry screen), the Settings → Account "view recovery phrase" panel, and Electron
secure-seedwiring land in the next step (completing P1.1–P1.4). Changeset:@xnetjs/identityminor. No user-visible behavior ships in this PR, so it'sskip-changelog.🤖 Generated with Claude Code