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:
flyemoji committed 2026-07-01 18:00:13 +09:00
1 parent c01f133cd8
commit fe2ece08cc
8 files changed
+585

No files matched your search

+56
View File
@@ -1338,6 +1338,62 @@ paths:
'403':
$ref: '#/components/responses/Forbidden'
/api/v1/auth/bind:
post:
tags: [auth]
operationId: bindRedeem
summary: Redeem a Bind Code into a player account + session (public console bootstrap, spec §10/§B).
description: >-
The one public, pre-account entrypoint of the player console
(console.<root_domain>): an account-less player redeems the one-time Bind
Code they generated in the in-game Login Lobby, and the platform creates their
player account (role=user), binds it to the verified in-game UUID, and mints a
host-only session cookie. Safe to expose unauthenticated because the code is
minted internal-face only, against an online-mode-verified UUID, with a short
TTL and single use — possession already proves control of a Minecraft identity.
An already-linked player UUID logs that player back in (idempotent); a UUID
that belongs to staff is refused (403) — operators authenticate at op.console
behind Zero Trust, so this never mints a session for an admin identity. Requires
local sessions to be enabled (same toggle as login).
x-felis-face: [external]
x-felis-tier: public
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [code]
properties:
code: { type: string }
responses:
'200':
description: Player account bootstrapped; the session cookie is set on the response.
content:
application/json:
schema:
type: object
required: [user_id, linked, mc_uuid, auth_source]
properties:
user_id: { type: string }
linked: { type: boolean, const: true }
mc_uuid: { type: string }
auth_source:
type: string
enum: [mojang, thirdparty]
description: The source captured at mint, copied onto the durable link.
'400':
description: Invalid or expired bind code.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'403':
description: Local sessions are disabled, or the code's UUID belongs to a staff account.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
/api/v1/me:
get:
tags: [servers]