fitme·story
v7.8.1 · 4 min read
Summary card · 60-second read

UCC Passkey Auth — Replacing basic-auth on the operator dashboard with WebAuthn

Version
v7.8.1
Date
2026-05-07
Tier
light

Two-PR cross-repo ship that replaces shared HTTP basic-auth on /control-room/* with WebAuthn passkeys. Per-operator identity, per-device credentials, server-side session allowlist (instant revoke), append-only audit log synced daily into FT2 framework-health. Migration is reversible at every step via UCC_AUTH_MODE env switch (basic / passkey / both). 5 screens · 5 API routes · 22 new files in fitme-story · 19/19 unit tests pass · tsc clean · next build clean. Second feature shipped via the v7.8.1 protocol (Mechanism C session attribution + isolated worktree from Phase 1 + Tier 2.2 logging + Mechanism A coverage telemetry). Structurally closes the asymmetry where iOS users had passkeys but operators had a shared phishable password.

How to read this case studyT1/T2/T3 · ledger · kill criterion
T1Instrumented
Numbers come from a machine-generated ledger or commit. Reproducible. Highest reader trust.
T2Declared
Numbers stated by a structured declaration (PRD, plan, frontmatter) but not directly measured.
T3Narrative
Estimates and observations from session memory. Useful for context; not citable as evidence.
Ledger
Where to verify the claim — a file path, GitHub issue, or backlog entry. Anything labelled ledger: is the audit trail.
Kill criterion
The pre-registered threshold under which this work would have been killed mid-flight. Not fired = work shipped without hitting the threshold.
Deferred
Items intentionally not closed in this version. Each cites the ledger that tracks remaining work.

Visual aid · key numbers at a glance

Default · no specialised visual declared
PRs mergedT1
fitme-story #55 + FT2 #248
Implementation tasks doneT1
28 / 28
Unit tests passT1
19 / 19
Phase transitions in one sessionT1
9
Measured wall timeT1
~115 min
Files added in fitme-storyT1
22
Files added in FT2T1
2 + 1 dir of artifacts
High-risk Swift files touchedT1
0
Lines added (fitme-story)T1
~2,650
Kill criterion · not fired
  • K1 — Registration ceremony fails on > 5% of attempted devices in week 1 → fall back to UCC_AUTH_MODE=basic.
  • K2 — any counter_replay event = hard stop. Investigate before any further sign-ins.
  • K3 — Vercel function p50 on /api/auth/* > 500 ms sustained 24 h → fall back.

Why this exists

The Unified Control Center shipped on 2026-05-06 [T1] gating /control-room/* behind shared HTTP basic-auth. T2.5 — "passkey replacement for basic-auth" — was explicitly deferred because the Astro→Next.js migration was already a 22-PR effort across two repos.

That deferral surfaced a structural asymmetry: the iOS app shipped passkeys for end users on 2026-05-01 via auth-polish-v2 [T1]. The operator surface — which has access to every framework readout, ship event, ledger, and case study — was gated by a phishable shared credential. Operator security weaker than user security is exactly the wrong ratio.

This feature closes that asymmetry one day after the v7.8.1 framework feature that introduced the protocol it followed.

What landed

fitme-story side — the actual gate (PR #55)

Five screens, five API routes (Node.js runtime), four reusable components, one Vercel Cron route, and one extended proxy.ts switch. The gate matrix:

UCC_AUTH_MODEBehavior
basic (default; pre-cutover)Existing basic-auth path; no behavior change
both (cutover window)Accept either basic-auth OR passkey session cookie; log which path was used
passkey (steady state)Require iron-session cookie; redirect-to-sign-in on miss (NOT 401)

The cookie is iron-session AES-GCM-sealed (HttpOnly + Secure + SameSite=Strict, scoped to /control-room, 24h hard cap with 12h sliding refresh). Server-side ucc:session:<sid> allowlist in Upstash Redis enables instant revoke — flipping revokedAt on a credential bulk-clears every session for that operator on the next request.

FT2 side — cross-repo audit-log sync (PR #248)

Daily GitHub Actions cron (05:17 UTC) pulls a Vercel Blob populated by the fitme-story Vercel Cron (05:13 UTC) and commits it to .claude/logs/ucc-auth-events.jsonl. The cross-repo workflow is repo-variable gated — absence of UCC_AUDIT_BLOB_URL is a no-op, not a failure. This matches the migration plan's reversibility property at the infrastructure layer.

Defenses

  • Phishing → WebAuthn binds assertion to RP-ID (fitme-story.vercel.app); browser refuses to sign for any other origin.
  • Replay → Per-ceremony challenge in Redis (60s TTL, single-use) + CAS counter check. Hardware authenticators that always report counter=0 explicitly handled.
  • Session theft → HttpOnly + Secure + SameSite=Strict + server-side allowlist. Any compromised cookie is one revoke away from invalidation.
  • PII in audit log → SHA-256 hashes for credential IDs + session IDs; IPv4 truncated to /24, IPv6 to /48; UA stripped to family. Tests assert no raw value appears in the JSONL output.

How this got shipped — the v7.8.1 protocol

Second feature shipped via the v7.8 protocol (after framework-v7-8.1 itself, the same-day morning ship). The protocol mandates:

  1. Isolated worktree from Phase 1 onward (Mode C of BRANCH_ISOLATION_VIOLATION)
  2. Mechanism C session attribution via .claude/active-feature lockfile
  3. Tier 2.2 contemporaneous logging on every phase transition
  4. v4.X preflight gates (/ux preflight + /design preflight) before Phase 4
  5. v4.X pre-merge-review gates (/ux pre-merge-review + /design pre-merge-review) before Phase 7
  6. FEATURE_CLOSURE_COMPLETENESS gate at Phase 8 (7 frontmatter fields + Q7 kill_criteria_resolution + Q6 PR parity)

Compliance ledger on this ship: ✓ ✓ ✓ ✓ ✓ ✓ — the kill_criteria_resolution field is set to Pending — week-1 telemetry gate per the documented forward-only path.

Honest disclosures

  1. One bug caught + fixed during Phase 5. iron-session.unsealData returns {} on garbage input rather than throwing. My unsealSession wrapper would have returned an empty SessionPayload-shaped object (truthy, defensive checks pass). Caught by the "returns null on garbage input" test. Fix: explicit field-presence check on all 4 payload fields. The test asserts the bug stays dead.
  2. 4 modules refactored to lazy env reads. Static reads at module-load time caused test isolation issues (env-var override after import had no effect because imports hoist). Refactored to read at call-time. This is also the right production behavior.
  3. Figma node IDs are absent. Per state.json.figma_build_status: "deferred_to_prompt". The fitme-story dashboard has no Figma-file mapping by design — built code-first during the UCC migration. Portable build prompt at docs/prompts/ui/2026-05-07-ucc-passkey-auth-design-build.md is the v4.X documented escape hatch.
  4. kill_criteria_resolution is "pending". Time-deferred resolution (T+7d post-cutover). Frontmatter records the criteria + a placeholder; actual update lands in §99 after the cutover window.
  5. Patterns ported, not extracted. auth-polish-v2's capability-detection + consent-sheet + inline-error-banner + mock-fixture-test patterns are mirrored web-side rather than extracted into a shared lib. Consolidation can happen if a third auth surface emerges.

Cross-references