feat(api): add public Bind-Code onboarding for the player console
Adds POST /api/v1/auth/bind, the one public pre-account entrypoint of the player console (console.<root_domain>). An account-less player redeems the one-time Bind Code minted in the in-game Login Lobby; in a single step the platform creates a role=user player, links it to the verified in-game UUID, and mints a host-only felis_session. Login is thus not forced at the edge while operations stay app-authenticated. The operator console (op.console.<root_domain>) is unaffected and stays behind Zero Trust: a code whose UUID resolves to a staff (role=admin) account is refused with 403 (ErrPlayerBindForbidden) without consuming the code, so the public door provably never yields an admin principal — the session it mints carries ViaAdminAccess=false and is host-only to console, never sent to op.console. Repo layer: new RedeemPlayerBindCode on the Repo interface, implemented on PGRepo (single tx: resolve code, create-or-fetch the player, consume) and the test fake. The returning-player branch is idempotent and is a deliberate standing "log in via the game" door, not just first-time onboarding. Honest labeling: - ORACLE-VERIFIED (Go): account/session logic — role=user, refuse-staff, idempotent create-or-fetch, single-use code, and the op.console redline (player session rejected on admin routes). Covered by handlers_onboard_test and the OpenAPI parity gate. - INTEGRATION-dependent: the endpoint's security rests on the Bind Code having been minted against an online-mode-Yggdrasil-authenticated UUID, a precondition that lives in velocity/Java and is not verifiable from this repo (CODE-ONLY). The Go layer proves the logic, not that identity guarantee. - No app-level attempt cap: rate-limiting is deferred to the edge as for the public /auth/login; the ~1e12 keyspace, single use and short TTL make a blind app-level cap non-critical.
This commit is contained in:
8 files changed
+585
No files matched your search
@@ -42,6 +42,13 @@ var (
|
||||
// finish endpoint exists; the ceremony state is gone (never begun, already
|
||||
// consumed, or expired) — so handlers map it to 400, not 404.
|
||||
ErrPasskeyChallengeInvalid = errors.New("passkey challenge invalid or expired")
|
||||
// ErrPlayerBindForbidden means a public Bind-Code redemption resolved to a STAFF
|
||||
// account (role=admin), which the player-console bootstrap refuses (console-tier
|
||||
// access model). Operators authenticate at op.console behind Zero Trust, never via
|
||||
// the account-less console.<root_domain> door, so the public bootstrap provably
|
||||
// never mints a session for an admin identity. It is distinct from ErrConflict so
|
||||
// the handler answers 403 (wrong door) rather than 409 (already linked).
|
||||
ErrPlayerBindForbidden = errors.New("bind code belongs to a staff account")
|
||||
)
|
||||
|
||||
// apiError is a handler-level error carrying an HTTP status and a stable,
|
||||
|
||||
Reference in new issue
Block a user