Skip to content

feat(identity): handles Wave A2 — console claim UI (/console/security) - #185

Merged
unforced merged 1 commit into
mainfrom
handles-a2
Jul 17, 2026
Merged

feat(identity): handles Wave A2 — console claim UI (/console/security)#185
unforced merged 1 commit into
mainfrom
handles-a2

Conversation

@unforced

Copy link
Copy Markdown
Contributor

PR A2 — the console claim UI (Handles, Wave A)

Second PR of the handles wave sequence, building directly on A1 (#184, migration 0022 + src/handles.ts + the Bearer claim API). No migration here — UI + one console door.

What ships

A "Your handle" block on /console/security (renderSecurity, alongside email/password/2FA):

  • Unclaimed → a plain-form claim (no JS, the console house style) prefilled with the email-derived suggestion (suggestHandleFromEmail), the policy stated in full (lowercase letters/numbers/hyphens, 3–30, one per account, permanent for now).
  • Claimed → the handle shown read-only ("renaming comes later").

POST /console/handle — the console write boundary (session + CSRF + same-origin, via checkForm, mirroring POST /console/security), calling the same claimHandle core as the Bearer door (POST /account/handle) — the checkoutCore/portalCore two-door precedent from billing. No claim logic duplicated.

Error mapping (all re-render /console/security):

  • invalid / reserved → message inline in the form, attempted value preserved (and esc()'d — a crafted handle can't break the value="…" attribute, the same CSP injection-safety posture as every other form).
  • handle_taken → friendly inline "already taken".
  • handle_already_set (a raced/stale double-submit) → a gentle notice over the now-read-only handle (no form left to inline into).
  • Success → re-render showing the claimed handle read-only.

Server-rendered HTML only, ui.ts style, CSP-compliant (no new inline scripts/handlers). Additive + inert on the wire — nothing serves a /u/<handle> URL until later waves.

Tests (test/auth.test.ts, synthetic data only)

Both render states (unclaimed shows suggestion + form; claimed shows read-only, no form), plus the POST handler: happy path, mixed-case canonicalization, invalid, reserved, taken, already-set, attribute-escape (a"ba&quot;b), CSRF-missing, cross-origin, no-session.

Gates (literal counts)

  • identity bun run typecheck — clean
  • identity bun x vitest run888 pass across 35 files (auth.test.ts 60, +14 new). Run per-file: the local workerd pool exhausts sockets (EADDRNOTAVAIL) running all files at once — the documented cloud gotcha; every file passes individually.
  • root bun run typecheck — clean
  • root bun run test153 pass, 0 fail (16 files)

Version → 0.0.8-rc.102.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XLZtmuSs1RirWGMGyCB1QB

…ty (#A2)

A "Your handle" block on /console/security: an unclaimed account sees a
plain-form claim (no JS — the console house style) prefilled with the
email-derived suggestion (suggestHandleFromEmail) and the policy stated in
full; a claimed account sees its handle read-only ("renaming comes later").

POST /console/handle is the console door — session + CSRF + same-origin (the
console write boundary, checkForm) — calling the SAME claimHandle core as the
Bearer door (POST /account/handle), the checkoutCore/portalCore two-door
precedent from billing. No claim logic is duplicated. Typed failures map to a
re-rendered page: invalid/reserved inline in the form (attempted value
preserved, esc'd so a crafted handle can't break the attribute), handle_taken a
friendly inline "already taken", handle_already_set a gentle notice over the
read-only handle. Success re-renders showing the claimed handle.

No migration (A1 shipped 0022). Additive + inert on the wire — nothing serves a
/u/<handle> URL until later waves.

Tests (auth.test.ts, synthetic data only): both render states, and the POST
handler's happy path, mixed-case canonicalization, invalid, reserved, taken,
already-set, attribute-escape, CSRF-missing, cross-origin, and no-session.

Gates: identity typecheck clean + vitest 888 pass across 35 files (auth.test.ts
60, +14 new; run per-file — the local workerd pool exhausts sockets running all
files at once); root typecheck clean + root bun test 153 pass. Version → rc.102.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XLZtmuSs1RirWGMGyCB1QB
@unforced
unforced merged commit cda7335 into main Jul 17, 2026
3 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