docs(api): record passkey login-handler deferral rationale

The passkey login/assertion HTTP handler stays deferred after its design
checkpoint; capture the reasoning in the handler header so the decision is
durable in the repo rather than only in task notes.

- RP boundary (resolved): felis-api is the app-login relying party (panel.*);
  the WebAuthn security gate lives at the Cloudflare Access edge. Spec §14 ties
  WebAuthn/posture to admin.* (Access) while panel.* is plain app login, so
  there is neither a spec-required assertion handler nor a backend step-up
  consumer for one.
- Identifier (blocking): a from-zero login needs a unique, human-typable handle
  to resolve an account, but users.email is nullable and non-unique and a
  player's username is their Minecraft uuid. Username-first assertion has
  nothing to key on; re-link stays the returning-player door.

Discoverable (usernameless) credentials are the future enabler; the adapter
crypto is already verified so that slice inherits correct crypto.
This commit is contained in:
flyemoji committed 2026-07-01 19:15:44 +09:00
1 parent e035142abc
commit f34711c174
1 file changed
+29 -7
+29 -7
View File
@@ -19,13 +19,35 @@ import (
// Scope of the HANDLERS in this file: ENROLLMENT only. Every ceremony here rides on a // Scope of the HANDLERS in this file: ENROLLMENT only. Every ceremony here rides on a
// known principal — the challenge is bound to the caller's user_id and the finish // known principal — the challenge is bound to the caller's user_id and the finish
// verifies against the server-stashed SessionData, never a client-echoed challenge. The // verifies against the server-stashed SessionData, never a client-echoed challenge. The
// login/assertion path (proving a passkey to mint or elevate a session from an // login/assertion path (proving a passkey to mint a session from an UNauthenticated
// UNauthenticated state) has its cryptographic half built and Oracle-verified in the // state) has its cryptographic half built and Oracle-verified in the adapter
// adapter (internal/passkey BeginLogin/FinishLogin, against a virtual authenticator), // (internal/passkey BeginLogin/FinishLogin, against a virtual authenticator) and its
// and its persist-ready output shape is VerifiedAssertion below — but the login HTTP // persist-ready output shape is VerifiedAssertion below — but the login HTTP handler is
// handlers, the session minting, and the panel.* passkey relying-party boundary/tier // a DELIBERATELY deferred slice. Its design checkpoint (task #36) resolved two questions
// decision (see migration 0007) are a deferred slice: this file adds no unauthenticated // and then deferred, for reasons that outlive this comment:
// login route. //
// - RP boundary (RESOLVED): felis-api is the app-login relying party (panel.*); the
// WebAuthn-as-security-gate lives at the Cloudflare Access EDGE, not here. Spec §14
// ties WebAuthn/posture to admin.* (Access), while panel.* is plain app login with
// no WebAuthn requirement — so there is neither a spec-required assertion handler
// nor a backend step-up consumer for one (the role-switcher step-up UX is frontend).
// - Identifier (BLOCKING): a from-zero login needs a unique, human-typable handle to
// resolve the account before its passkeys can be offered. users.email is nullable
// and NOT unique (0001_init.sql), and a player's users.username IS their Minecraft
// uuid (pgrepo.go RedeemPlayerBindCode mints a uuid-derived unique username) —
// opaque, never typed into a form. The username-first assertion the non-resident
// credentials + user-keyed challenge store support therefore has nothing to key on.
//
// The system's returning-player door is already re-link (control of the in-game identity
// is the root of trust — handlers_onboard.go re-mints a session through the bind-code
// flow even after passkey/OTP are bound); passkey and email-OTP are factors on an
// ALREADY-authenticated principal here, not from-zero login methods. The real enabler
// for a from-zero passkey login is discoverable ("usernameless") credentials, which
// sidestep the identifier gap but reshape enrollment (residentKey) and need a
// non-user-keyed challenge store — a future migration and its own checkpoint (that door
// partly bypasses the in-game-identity root of trust). The adapter crypto is verified
// now so that slice inherits correct crypto; this file adds no unauthenticated login
// route until then.
// //
// The cryptographic half is a seam (PasskeyVerifier) so this package never imports // The cryptographic half is a seam (PasskeyVerifier) so this package never imports
// go-webauthn: ceremony state crosses the boundary as opaque bytes, the attestation // go-webauthn: ceremony state crosses the boundary as opaque bytes, the attestation