Skip to content

feat(web): Devices & keys, a new-key alert on sign-in, and a persistent vault - #383

Merged
PastaPastaPasta merged 27 commits into
masterfrom
feat/e6-devices-keys
Oct 5, 2026
Merged

PastaPastaPasta merged 27 commits into
masterfrom
feat/e6-devices-keys

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Owner

Stacked on #375 (the dg → browser key handoff). Until #375 merges, its three commits show here too. This PR's own changes are 60e5ba4 and b7e48e9.

What

E6 Keys, devices, recovery, part 2 of 3 (trust and safety TS-07 and TS-17).

  • Settings → Devices & keys (/settings/keys/). It lists every key on the identity as Platform has it: what each key is for (master, a Forge key with a budget and an expiry, a key for one Forge contract, an unbounded signing key, encryption, transfer, another app's), the budget left (keysRemainingBudgets), the expiry, and the keys this browser holds. You can give keys local names ("work laptop"); names stay in localStorage. Disable turns off a lost device's key with one master-key update. The identity file or recovery phrase is used once and not stored, and the update costs about 0.00002 DASH.
    • Only Forge-bound keys (the dash-forge group, or one Forge contract) can be disabled here. The update re-checks the same rule against the identity it reads.
    • Refused, with where to go instead: the master key, any key this browser holds (signing key, wallet grants, held keys: use Revoke), the encryption key (replace it from Private repos), and unbounded signing keys, transfer keys and other apps' keys (dg auth keys disable <id> --force).
    • Platform keeps no "last used" per key. The page says so, and shows when this browser last wrote, taken from its spend ledger.
  • New-key alert. Each browser keeps a snapshot of the identity's key ids. On the next sign-in or unlock, a live key that this browser did not add raises a red bar naming it, with Review keys and It was me. A new master key gets its own wording. The check reuses the identity read that sign-in already makes, so it costs nothing. The first look is silent. Keys this browser registers itself (CI runner, encryption key, a wallet's encryption key registered beside its auth key) are counted as its own. An acknowledgement never starts a snapshot.
  • Persistent vault. storeInVault asks for navigator.storage.persist(). The page shows whether the browser agreed and lets you ask again.
  • Docs: docs/guides/identity-and-keys.md gains a Devices & keys section, and the lost-laptop steps point to it.

Verification

  • tsc, eslint, vitest: 5675 tests pass. One Argon2 test in key-safety.test.ts timed out at 60 s with the machine at load average 250; it passes in the earlier full run and is untouched here.

  • Live on devnet sakura, using a static build served locally and dg on the same identity (/tmp/claude-501/e6/devices-live.mjs):

  • Screenshots: /Users/pasta/workspace/dash-forge-qa/evidence/p0/keys-devices/pr2-*.png (alert, page, disable dialog, after disable, 390 px dark).

  • Review: a /code-review high pass produced 10 findings. These are fixed in b7e48e9:

    • unbounded wallet keys were offered Disable;
    • a contract group was not checked against Forge's;
    • wallet grants were not counted as this browser's;
    • a wallet's encryption key raised the alert;
    • an acknowledgement with no snapshot made the master key look new;
    • the page showed the alert twice.

    Not changed: three versions of the master-signed disable remain (merging them is a refactor for a later PR), and the alert does not sync across tabs (another tab updates on its next unlock). A security review of the diff found nothing at high confidence.

🤖 Generated with Claude Code

PastaPastaPasta and others added 8 commits October 4, 2026 12:34
…se prompt, keys up to a year

dg auth keys add --for-browser <request> registers a limited key with the master key on the terminal and prints it sealed (ECDH + HKDF + AES-256-GCM) to the one-time key the browser tab shows, so the recovery phrase never enters a web page. The web app gains Sign in -> Use dg and Renew -> With dg (the default for a key that came from dg), checks the key on chain before storing it, and can take the encryption key too. Every recovery-phrase prompt in the web app and dg shows the same fixed warning, and new browser keys can live 30 days to a year. Rust and TypeScript share the key_handoff_* and copy__* conformance vectors; forge-v2.md 6.4 documents the convention (TS-06).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review follow-ups: dg hands over a key that landed even when the update reported an error, refuses a key for another identity (--for-identity) or one that lives past a year before signing, and no longer asks for the recovery words when the signed-in key file fails to open. The browser names the key it replaces, refuses a reply that would orphan the key it holds or drop wallet keys, keeps the dropped-encryption-key notice when the carried key cannot be stored, and suggests the right network flag.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Anyone can show a dfkr1 request, so dg says to run the command only for a Forge page you opened yourself (security review of the key handoff).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sistent vault

Settings → Devices & keys lists every key on the identity as the chain has it (role, remaining budget, expiry, this browser's key), with local labels and Disable for a lost device's key, signed once by the master key. The master key, this browser's own key, the encryption key and other apps' keys are refused with the right place to go instead.

Each browser keeps a snapshot of the identity's key ids; on the next sign-in or unlock a live key it did not add raises a red bar (TS-07), reusing the identity read the sign-in already makes. Keys this browser registers itself (runner, encryption) are noted as its own.

The vault asks navigator.storage.persist() when it stores a key, and the page says whether the browser agreed (TS-17).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dded

Review of Devices & keys: unbounded signing keys (a wallet's) and keys bound to another app's contract group are no longer offered Disable; the master-signed disable re-checks the same rule against the identity it reads. Every key the browser holds (wallet grants, held keys) counts as its own and goes through Revoke. A wallet's encryption key, registered beside its auth key, no longer raises the new-key alert, and an acknowledgement never starts a snapshot (which would have named the master key as new). The page no longer repeats the app-wide alert.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 36 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5b1d07a2-36cb-4d53-a56c-999e2d724373
📥 Commits

Reviewing files that changed from the base of the PR and between ec2fd8a and 15adb37.

📒 Files selected for processing (44)
  • crates/dg/src/auth/keys.rs
  • crates/dg/src/auth/mod.rs
  • crates/forge-core/src/browser_key.rs
  • crates/forge-core/src/lib.rs
  • crates/forge-core/src/rules.rs
  • docs/contracts/forge-v2.md
  • docs/guides/identity-and-keys.md
  • forge-contracts/vectors/copy__recovery_phrase_warning.json
  • forge-contracts/vectors/key_handoff__limited_key.json
  • forge-contracts/vectors/key_handoff__with_encryption_key.json
  • forge-contracts/vectors/key_handoff_open__a_request_is_not_a_reply.json
  • forge-contracts/vectors/key_handoff_open__altered.json
  • forge-contracts/vectors/key_handoff_open__network_swapped.json
  • forge-contracts/vectors/key_handoff_open__opens.json
  • forge-contracts/vectors/key_handoff_open__other_network.json
  • forge-contracts/vectors/key_handoff_open__truncated.json
  • forge-contracts/vectors/key_handoff_open__wrong_browser_key.json
  • forge-web/app/settings/keys/page.tsx
  • forge-web/components/app-shell.tsx
  • forge-web/components/auth/create-identity-flow.tsx
  • forge-web/components/auth/dg-handoff-flow.tsx
  • forge-web/components/auth/key-lifetime.tsx
  • forge-web/components/auth/master-key-input.tsx
  • forge-web/components/auth/phrase-warning.tsx
  • forge-web/components/devices-keys.tsx
  • forge-web/components/encryption-key-panel.tsx
  • forge-web/components/identity-top-up-flow.tsx
  • forge-web/components/keys-panel.tsx
  • forge-web/components/login-modal.tsx
  • forge-web/components/new-key-alert.tsx
  • forge-web/contexts/auth-context.tsx
  • forge-web/hooks/use-ui-store.ts
  • forge-web/lib/auth/auth-v2.test.ts
  • forge-web/lib/auth/controller.ts
  • forge-web/lib/auth/devices.test.ts
  • forge-web/lib/auth/devices.ts
  • forge-web/lib/auth/index.ts
  • forge-web/lib/auth/key-handoff.test.ts
  • forge-web/lib/auth/key-handoff.ts
  • forge-web/lib/auth/key-watch.test.ts
  • forge-web/lib/auth/key-watch.ts
  • forge-web/lib/auth/limited-key.ts
  • forge-web/lib/auth/vault.ts
  • forge-web/lib/rules/conformance.test.ts
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

PastaPastaPasta and others added 15 commits October 4, 2026 16:23
# Conflicts:
#	docs/guides/identity-and-keys.md
# Conflicts:
#	forge-web/components/login-modal.tsx
…encryption key stays

"Revoke on chain" is now "Revoke on Platform", as the Settings button reads. The encryption key's row no longer points at a replace action the browser does not have: private repos open with it, so it is not disabled here.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
forge-v2.md: master's mirror back-links stay §6.4; key handoff becomes §6.5. rules.rs: both helpers (run_repo_name_case, run_role_oracle).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and check the held key in the vault write

The Unlock and pick-a-key paths now tell adoptHandoffKey which identity's key they replace, and a reply for another identity is refused before anything is stored. The held-key check runs again inside the vault write's IndexedDB transaction, so a key another tab stored meanwhile aborts the write instead of being overwritten.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d renewal

commitKey passes VaultChangedError through, so the dg handoff names the key to disable even when another tab left a renewal unfinished. The message covers a removed key as well as a stored one, and PendingRenewalError gets its doc comment back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings #390's contract snapshot (sakura runs UPDATE-1) and the latest master; conflicts resolved keeping both sides.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PastaPastaPasta and others added 2 commits October 5, 2026 05:24
limited-key.ts keeps the key lifetimes; its prefix-only shortId gives way to master's 7…5 shortId from lib/utils (#368).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@PastaPastaPasta
PastaPastaPasta merged commit d166370 into master Oct 5, 2026
32 checks passed
@PastaPastaPasta
PastaPastaPasta deleted the feat/e6-devices-keys branch October 5, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant