Skip to content

feat(web): forge-v2 writes and limited-key sign-in - #24

Merged
PastaPastaPasta merged 9 commits into
masterfrom
feat/web-limited-key-auth
Sep 26, 2026
Merged

PastaPastaPasta merged 9 commits into
masterfrom
feat/web-limited-key-auth

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Owner

What

This is PR F part 2, stacked on #23 (base feat/web-v2-writes). It adds limited-key sign-in, the browser vault, and in-browser identity creation from ux-dx-spec.md §2 (P0 items 1 and 2). After this PR, the web app only ever keeps a PV14 limited key:

  • AUTHENTICATION / HIGH;
  • bound to the dash-forge contract group, so it can sign batches on forge-core and forge-collab only;
  • a budget of 0.05 DASH;
  • an expiry of 90 days.

The master key is used only in one-time ceremonies and is not retained. The plaintext localStorage keystore is gone, and any leftover forge_key_* entries are purged.

Sign-in sheet

Tile What it does
Unlock Shown when this device already holds a key. Unlocks with a passkey or a passphrase.
Use my Dash wallet App Connect. Shown only when the system contract exists (it does on moutai; testnet is still on protocol 13). Details below.
Create a new identity 12 words, a 3-word backup quiz, key protection, then a deposit QR. After that: an asset lock, a proof (InstantSend on testnet, chain lock on devnets), and one IdentityCreate that carries the limited key. The key is stored in the vault before it is registered. Creation is journaled, resumable and discardable.
Import an identity file or recovery phrase The master key signs one IdentityUpdate that registers the limited key. When renewing, the same update disables the previous key.
Advanced: paste a key HIGH or CRITICAL only. Kept in this tab only, with a red warning.

App Connect in detail:

  • The request names the contract group.
  • The app pages through every reply and verifies the granted key on chain: live, group-bound, with a budget and expiry.
  • If more than one identity replies, it refuses.
  • The user must confirm the full identity id and DPNS name before signing in.

On testnet, where there is no forge-v2 group, only the v1 identity-file sign-in is available, for the current tab only.

Vault (lib/auth/vault.ts)

  • Storage: AES-256-GCM in IndexedDB, bound to the network and identity through additional authenticated data.
  • Unlock: the data key is wrapped by a passkey PRF output (stretched with HKDF), by Argon2id of a passphrase (64 MiB, 3 passes, 16-byte salt), or by both. The Argon2 parameters are pinned in code, not read from the record.
  • In memory: the unlocked key lives only in this module's memory. It auto-locks after 12 hours, and the auto-lock also ends the session.
  • Shared origins: the vault refuses to run on shared origins (*.github.io project sites, IPFS path gateways). Pages serves forge.dashhq.org, as the repo's Pages settings confirm.

Header and Settings

  • The funds pill shows a budget bar and the expiry.
  • Settings → This browser's key has four actions:
    • Renew: register a new key and disable the old one.
    • Revoke on chain: uses the master key once.
    • Lock.
    • Forget: the UI says clearly that forgetting is not revoking.
  • A block-explorer override is also in Settings.

Live on moutai (all green: E2E_DEVNET=moutai E2E_WRITE=1)

  • a1: import once.
    • The spec reads the identity from Node, independently of the app. Exactly one new key is present: HIGH, bound to the group 23iVLZ…, budget 5,000,000,000 credits, remaining 5,000,000,000, expiry ~90 days.
    • localStorage holds no WIF.
    • The vault unlocks after a reload.
  • a2: in-browser identity creation.
    • Words, quiz and passphrase, then the deposit address. The harness funds the address from the devnet faucet key through mint-identity's fundFromKey.
    • Then the asset lock, the chain-lock proof, and the IdentityCreate with the limited key.
    • It passes end to end in about 4 minutes. For example, identity AypWjSj19Wpn… got 0.028 DASH of credits from a 0.03 DASH deposit, with a 0.05 DASH / 90-day browser key.
  • Writes and reads: all of feat(web): forge-v2 writes and cost UX #23's write specs (w1–w8) and read specs pass, signing with limited keys. 19/19 on the final code.
  • Testnet: the read specs and the live v1 issue write (auth-write) pass.

Screenshots: browser key in Settings, create: the 12 words, create: the deposit QR, identity created, key live.

The asset-lock transaction builder is byte-identical to tools/mint-identity. A unit test pins mint-identity's output as a vector.

Reviews

Security review (independent, adversarial). All 12 must-fix items are fixed in ecbf04a:

  • Fund loss: a lying block explorer can no longer burn a deposit. Each input is now proven from its raw funding transaction: the transaction is hashed against the txid, and the value and script are parsed from its bytes. The fee is asserted. Unit tests cover a lying explorer.
  • Forget: Forget now acts on the key selected in the Unlock view.
  • App Connect: it can no longer silently sign you in as someone else's identity. There is a confirmation step, and multiple repliers are refused.
  • App Connect with a real wallet: the request now names the group, and replies are paged and retried.
  • Shared origins: the vault is refused on them.
  • Passkeys: each gets a random user.id.
  • Renewal: the old key is disabled, and Revoke on chain is available.
  • Secrets out of React state: these now live in refs, cleared after use: the identity-file text, the mnemonic, the PRF output and the granted WIF.
  • Single flight: creation runs once at a time, aborts on unmount, and saves the signed lock before broadcasting it.
  • Argon2 parameters are pinned.
  • Signed retry writes are cleared when you sign out.
  • verifyLimitedKey rejects expired or spent keys and compares against the request.

Also:

  • The group is checked on chain to contain forge-core and forge-collab before a key is bound to it.
  • The CSP no longer grants 'unsafe-eval' (only 'wasm-unsafe-eval'). Everything, including the live writes, runs without it.
  • The trust roots and the vault's limits (XSS, frame-ancestors on Pages) are documented in docs/guides/identity-and-keys.md.

Correctness review: all 5 must-fix, 7 should-fix and 8 simplification items are fixed. They include:

  • the identity-creation resume paths;
  • a discard button with a funds warning;
  • a single-flight guard;
  • the Unlock view's forget;
  • passkey user.id;
  • orphaned-key messaging;
  • auto-lock → Unlock;
  • Renew opening Import directly;
  • the view-reset race;
  • checking the phrase before showing a QR;
  • journaling the lock before broadcast;
  • App Connect retries;
  • a typed SDK facade.

Known gaps

  • App Connect: the reply only proves that someone registered the key. The v1 loginKeyResponse schema has no field for a wallet signature over the request. Until it does, the user's confirmation of the identity is the check. It has not been tested against a real wallet (none speaks the group-scoped request yet).
  • Broadcast: the asset lock is broadcast through the explorer, because the SDK has no DAPI Core broadcast. An explorer can delay, but not steal.
  • Stranded test DASH: 0.03 of it is on a deposit address from a pre-fix run of a2 on moutai. The creation was abandoned when the tab closed. It is recoverable only through the test's mnemonic, which was not kept.
  • IdentityKeyLimitsUpdate top-ups (P1) are not wired; renewing registers a new key instead.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 20 minutes.

Check out review usage here.

View limit details

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

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

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 5e99d06e-680f-4170-b245-fff591e02964

📥 Commits

Reviewing files that changed from the base of the PR and between fecc2b4 and 21174c0.

⛔ Files ignored due to path filters (1)
  • forge-web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (34)
  • docs/guides/identity-and-keys.md
  • forge-web/app/layout.tsx
  • forge-web/app/login/page.tsx
  • forge-web/app/new/page.tsx
  • forge-web/app/settings/page.tsx
  • forge-web/components/app-header.tsx
  • forge-web/components/auth/create-identity-flow.tsx
  • forge-web/components/auth/protection-fields.tsx
  • forge-web/components/auth/wallet-connect-flow.tsx
  • forge-web/components/keys-panel.tsx
  • forge-web/components/login-modal.tsx
  • forge-web/components/top-up-sheet.tsx
  • forge-web/components/ui/qr.tsx
  • forge-web/contexts/auth-context.tsx
  • forge-web/e2e/auth-write.spec.ts
  • forge-web/e2e/helpers.ts
  • forge-web/e2e/v2-auth.spec.ts
  • forge-web/e2e/v2-writes.spec.ts
  • forge-web/hooks/use-ui-store.ts
  • forge-web/lib/auth/app-connect.ts
  • forge-web/lib/auth/asset-lock.ts
  • forge-web/lib/auth/auth-v2.test.ts
  • forge-web/lib/auth/controller.ts
  • forge-web/lib/auth/create-identity.ts
  • forge-web/lib/auth/hd.ts
  • forge-web/lib/auth/identity-file.ts
  • forge-web/lib/auth/index.ts
  • forge-web/lib/auth/keystore.ts
  • forge-web/lib/auth/limited-key.ts
  • forge-web/lib/auth/vault.ts
  • forge-web/lib/sdk/facade.ts
  • forge-web/lib/sdk/index.ts
  • forge-web/lib/sdk/service.ts
  • forge-web/package.json

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 9 commits September 26, 2026 13:33
Vault: AES-256-GCM under a passkey PRF (HKDF) or Argon2id (64 MiB, t=3) in IndexedDB, AAD-bound to network+identity, unlocked only in module memory with a 12 h auto-lock; the plaintext localStorage keystore is removed and legacy entries purged. Sign-in sheet: unlock, App Connect (verified on chain), create identity (mnemonic, 3-word quiz, deposit QR, asset lock ported from mint-identity and byte-identical to it, IdentityCreate carrying the limited key), import file or phrase (master key signs one IdentityUpdate registering a HIGH key bound to the dash-forge group with budget and expiry), Advanced raw key for this tab only. Settings shows the key's budget and expiry, renew, lock and forget.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
v2-auth.spec.ts: a1 imports an identity file and checks from Node that the new key is live, HIGH, bound to the dash-forge group with a 0.05 DASH budget and a ~90-day expiry, that localStorage holds no key, and that the vault unlocks after a reload. a2 creates an identity in the browser, funding the deposit from the devnet faucet key through mint-identity's fundFromKey. The write specs now sign in through the import ceremony.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The create-identity and wallet flows can start before any page connected (from the landing page); they used getSdk() and failed with 'EvoSDK not initialized' after the deposit arrived. ensureSdk(network) connects on demand. Live on moutai the whole create flow now passes: words, quiz, passphrase, deposit funded from the devnet key, chain-lock proof, IdentityCreate with the limited key, in 4.3 minutes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Identity creation stores the browser key in the vault before IdentityCreate, saves the signed asset lock before broadcasting it, re-registers a key from the mnemonic when the identity already exists, and clears the journal only after the session opens; unfinished creations can be discarded (with a warning if the deposit holds funds); one run at a time, aborted on unmount; a wrong phrase is caught before any QR. Passkeys get a random user id. Forget acts on the selected key with a confirmation; the auto-lock ends the session; Renew opens Import directly; a key stored but not opened says so; wallet responses that fail verification are retried. Renewal disables the previous key in the same update; the sheet offers Unlock for an identity already stored; the file network check is back. One typed SDK facade and shared sheet pieces. e2e reuses one stored key per identity instead of registering a key per test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Asset-lock inputs are proven from their raw funding transactions (hash checked against the txid, value and script parsed from the bytes), so a lying explorer can no longer burn a deposit; the fee is asserted. App Connect asks for the dash-forge group, pages through every reply, retries a reply whose key is not visible yet, refuses when two identities answer, and makes the user confirm the full identity id and DPNS name. The vault refuses to run on shared origins (github.io project sites, IPFS path gateways), pins the Argon2 parameters instead of reading them from the record, and clears signed retry writes on sign-out and forget. Keys: random passkey user ids, renewal disables the previous key, Revoke on chain in Settings (forgetting is not revoking), verifyLimitedKey rejects expired or spent keys and checks the requested limits, the group is checked on chain to hold forge-core and forge-collab before a key binds to it, the raw-key path accepts only HIGH or CRITICAL. Secrets (identity file text, mnemonic, PRF output, granted WIF) live in refs, not React state. CSP drops 'unsafe-eval'. docs/guides/identity-and-keys.md documents the trust roots and the vault's limits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A contract re-registration leaves limited keys bound to the old group: they still validate but every write is refused. Unlock now reports them as needing renewal; renew and revoke accept any group-bound budgeted HIGH key so old keys can be disabled. The e2e reads the group from the deployment file.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A lagging node answered 'key N is not on identity' right after the update landed (live a1).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@PastaPastaPasta
PastaPastaPasta force-pushed the feat/web-limited-key-auth branch from ca256d6 to 21174c0 Compare September 26, 2026 18:33
@PastaPastaPasta
PastaPastaPasta changed the base branch from feat/web-v2-writes to master September 26, 2026 18:38
@PastaPastaPasta
PastaPastaPasta merged commit 9b53013 into master Sep 26, 2026
4 of 7 checks passed
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