Skip to content

A person's approved key opens a fragment's pages: POST /api/f/{name}/session - #1032

Merged
futurepaul merged 2 commits into
masterfrom
claude/ios-key-sessions
Nov 4, 2026
Merged

futurepaul merged 2 commits into
masterfrom
claude/ios-key-sessions

Conversation

@futurepaul

Copy link
Copy Markdown
Collaborator

The server half of #1026 (split from that draft, which stays as it is): a person's approved key opens a fragment's pages, so a native app with no browser session can open private apps and brains in a web view.

What. POST /api/f/{name}/session, NIP-98 signed by a person's approved key, takes {returnTo?, consent?}. It answers {consentRequired: false, url}, the fragment's one-use, 60-second __signin redemption, or {consentRequired: true, canonical, email} with no credential.

  • It uses the same consent as /auth/fragment: silent on the person's own and shared fragments, asks first on anyone else's, and remembers the yes.
  • It goes through the same mint_for and the same registry Mint call. Mint now names a token (a browser's platform session) or a key, exactly one; a key's mint is top-level only.
  • An agent's key is 403. An unknown or revoked key is 401. A returnTo off the fragment's origin, or an unknown field, is 400.

The key's parent session. The registry mints from a parent session of the key's own: one per key, sessions.device_key, schema step 9 key_sessions.

  • Its token is never kept, so it is no bearer session.
  • Each site session redeemed from it lives only while the key is still its person's. live_session and redeem join the live key, so revoking the key ends its site sessions and its unspent redemptions, and nothing else of the person's.

Rollback (asked in review). During a rollback, an older release wouldn't apply the key check to children minted from a key parent. This can't happen, for two reasons:

  • Step 9 is a schema step. The rollout refuses to roll back beneath a schema change ("forward only: fix it forward", xtask/src/rollout.rs eligible).
  • An older release's registry that meets a newer schema refuses every call (SchemaError::Newer → schema_refused, crates/core/src/migrate.rs).

So no older code ever reads these rows, and every site-session lookup goes to the registry live, with no cache. Nothing extra is written for it.

Idempotent posts (also asked). The chat channel already treats a repeated post id as the same post: same id and body → the original record with replayed: true; a different body → 409 (cell/src/channels.rs post and posted, docs/chat-records.md).

Tests. A new e2e section, native, with 18 checks:

  • refusals: unsigned, unknown key, agent key, returnTo, unknown field;
  • one use, a wrong fragment leaving the redemption unspent, and private pages opening;
  • consent asked, given and remembered;
  • revocation ending the key's cookies, unspent redemptions and mints, while the person's other key and browser stay signed in.

restart checks that a key's site session outlives a restart.

Checks

  • cargo xtask check passes on the head merged with master bcc50fcd8.
  • On that merged head, e2e identities, signin, native, frames and restart: 296 passed, 0 failed.
  • Before that merge, e2e identities, signin, native, restart, plus every lane that reaches the changed sign-in paths (1,908 passed, 0 failed): frames, share, isolation, members, reach, secrets, faces, brain, brain-release, doc, chat, shell, shell-ui, templates, browser, routes, live, channels, schemas, cli, computers.

Wasm: +24,404 bytes against master bcc50fcd8, the exact sha merged (6,520,856 → 6,545,260; 54,740 left of the budget).

  • My first version was +29.8 KB. Folding the key path into the existing Mint call and mint_for, instead of a second call type and handler, cut that.
  • The migration alone is +520 bytes.
  • Of the rest, about 12 KB is this change's own code: the route handler, the registry's dispatch of key_parent, the joins in live_session, and strings. The remainder is LTO inlining shifts spread across unrelated functions, measured by name with twiggy on the pre-wasm-opt build.

🤖 Generated with Claude Code

futurepaul and others added 2 commits November 4, 2026 13:36
…session

A native app has no browser session. Its approved NIP-98 key now asks for
the same one-use, one-fragment __signin redemption /auth/fragment mints for
a browser, with the same consent: silent on the person's own and shared
fragments, asked first on anyone else's, the yes remembered.

The registry mints it from a parent session of the key's own (one a key,
sessions.device_key, schema step 9). Its token is never kept, so it is no
bearer session. Each site session redeemed from it, and each unspent
redemption, lives only while the key is still its person's: revoking the
key ends them and nothing else. An agent's key is refused (403).

The new `native` e2e section covers refusals, one use, wrong fragment,
consent, and revocation isolation; restart checks a key's site session
outlives a restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@futurepaul futurepaul left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review @ 6b68df5: PASS

Checked signed URL/method/body binding, person-only key resolution, confined returnTo, and the existing consent path: no credential reaches a stranger's app before consent, and the internal key parent's random bearer token is discarded rather than exposed. Both live_session and redemption validate the active key/identity binding; parent creation awaits only storage operations under Cloudflare's storage concurrency guarantees, and schema step 9 plus the release/schema checks prevent older registry code from accepting these rows. Local cargo xtask check passed, as did direct probes using the actual migrations and SQL for active/revoked/missing/reassigned keys, missing parents and browser-session isolation; I found no obvious large Wasm saving beyond the shared Mint/mint_for path, did not rerun e2e or measure Wasm, and CI's check is still running.

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass within the reviewed scope. No actionable regression found at 6b68df5cf280be4f6df8cf8727dcf83b9cd29a5c against bcc50fcd87addc3bd56a9cc06995d7cb0ba048cd.

Reviewed signed method/URL/body binding, person-only key resolution, consent reuse, return-path confinement, key-parent lifetime, revocation isolation, wipe behavior, and schema/release compatibility. An independent second source review found no concrete defect.

Validation: cargo test --locked -p fragment-core migrate:: (24 passed), cargo test --locked -p fragment-core site:: (11 passed), and git diff --check. Additional disposable SQLite probes using the actual migrations and session queries passed browser-session preservation, revocation isolation, expiry, ownership mismatch, absent-parent, and unique-key-parent checks. Those probes mirror the Rust rejection predicate; they do not execute the Worker handlers.

Coverage limits: the managed Docker daemon responds, but a disposable container's DNS lookup of index.crates.io fails with Connection refused / no reachable nameserver. The standard container-backed native, signin, identities, frames, and restart e2e lanes were not run. No workerd, live Cloudflare/vendor, or browser/WebView execution is claimed; fixtures were synthetic local SQLite data. No UI files changed, so no screenshots were required. CI check passed; the green e2e job is the PR pass-through and its shards were skipped. This is a COMMENT review, not an approval.

@futurepaul
futurepaul added this pull request to the merge queue Nov 4, 2026
Merged via the queue into master with commit b559244 Nov 4, 2026
16 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.

2 participants