Skip to content

Recoverable identities: IdentityManager engine (0243 Phase 1, step 2)#322

Merged
crs48 merged 1 commit into
mainfrom
claude/0243-recovery-phrase-wiring
Jun 28, 2026
Merged

Recoverable identities: IdentityManager engine (0243 Phase 1, step 2)#322
crs48 merged 1 commit into
mainfrom
claude/0243-recovery-phrase-wiring

Conversation

@crs48

@crs48 crs48 commented Jun 28, 2026

Copy link
Copy Markdown
Owner

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 default create() 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 recovery field; storeIdentity gains an optional arg), and an enrollRecoverableIdentity helper (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/identity typechecks 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-seed wiring land in the next step (completing P1.1–P1.4). Changeset: @xnetjs/identity minor. No user-visible behavior ships in this PR, so it's skip-changelog.

🤖 Generated with Claude Code

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>
@crs48 crs48 temporarily deployed to pr-322 June 28, 2026 21:50 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

✓ Changelog fragment found — thanks!

@crs48 crs48 added the skip-changelog Exclude this PR from the changelog label Jun 28, 2026
@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #322.

github-actions Bot added a commit that referenced this pull request Jun 28, 2026
@crs48 crs48 merged commit bd8ee17 into main Jun 28, 2026
15 of 16 checks passed
@crs48 crs48 deleted the claude/0243-recovery-phrase-wiring branch June 28, 2026 21:56
github-actions Bot added a commit that referenced this pull request Jun 28, 2026
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changelog Exclude this PR from the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant