feat(api): reclaim squatted usernames for Mojang-priority players (spec §B3)

When the configured third-party Yggdrasil and the official Mojang service
issue the same username under different UUIDs, the non-genuine squatter is
displaced in favour of the real Mojang owner (正版优先). This adds the
Go-verifiable data layer of that flow on the internal (velocity) face.

- migration 0006: username_blacklist (barred squatter UUIDs) and
  player_data_holds (the displaced account's 30-day data stash), both keyed
  by mc_uuid so the genuine Mojang player — identical username, different
  UUID — is never caught by the bar.
- POST /api/v1/internal/player/reclaim bars the squatter UUID and stashes
  its data in one transaction (all-or-nothing). It is idempotent on a
  retried callback and returns the hold's effective expiry — the first
  reclaim's window, never a fresh now()+30d — so the rejected player is told
  the truth about how long their data is kept.
- GET /api/v1/internal/player/blacklist/{mc_uuid} is the login-gate check
  velocity calls to reject a barred squatter before admitting them.

Scope: velocity collision-routing, the limbo prompt, the authlib
dual-backend and the data-inherit flow are code-only (Java plus a QR-bound
device session a row cannot express) and are not part of this slice. Unit
tests cover the handlers and the in-memory repo contract; the Postgres SQL
path is exercised by integration only.
This commit is contained in:
flyemoji committed 2026-06-27 13:41:19 +09:00
1 parent 1f8b9bb5d0
commit a29571de39
8 files changed
+611

No files matched your search

+74
View File
@@ -668,6 +668,80 @@ paths:
'401':
$ref: '#/components/responses/Unauthorized'
/api/v1/internal/player/reclaim:
post:
tags: [account-internal]
operationId: reclaimUsername
summary: Record a Mojang-priority username reclaim — bar the squatter UUID and stash its data (spec §B3).
description: >
Internal-only. Velocity records a username-collision reclaim: the
non-genuine squatter UUID is barred and its world/player data stashed for
a 30-day window so a new account can inherit it. Idempotent — a repeat
reclaim of an already-barred UUID is a no-op. The bar is keyed by UUID,
never the contested name, so the genuine Mojang player always passes.
x-felis-face: [internal]
x-felis-tier: service
security: [{ serviceToken: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [squatter_uuid, username]
properties:
squatter_uuid: { type: string, format: uuid }
username: { type: string }
data_ref:
type: string
description: >
Optional opaque handle to the data already archived for the
hold (server-side only, never returned). Archival may be
deferred, in which case this is omitted.
responses:
'200':
description: Reclaim recorded.
content:
application/json:
schema:
type: object
required: [blacklisted, username, hold_expires_at]
properties:
blacklisted: { type: boolean, const: true }
username: { type: string }
hold_expires_at: { type: string, format: date-time }
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
/api/v1/internal/player/blacklist/{mc_uuid}:
get:
tags: [account-internal]
operationId: checkUsernameBlacklist
summary: Report whether an in-game UUID was barred by a prior reclaim (spec §B3).
description: >
Internal-only. The velocity login gate calls it to reject a barred
squatter before letting them in; the genuine Mojang UUID — same username,
different UUID — is never on the list and always passes.
x-felis-face: [internal]
x-felis-tier: service
security: [{ serviceToken: [] }]
parameters:
- { name: mc_uuid, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
description: Blacklist status.
content:
application/json:
schema:
type: object
required: [blacklisted]
properties:
blacklisted: { type: boolean }
'401':
$ref: '#/components/responses/Unauthorized'
# ----------------------------------------------------- external: servers ---
/api/v1/servers/{name}/wake:
post: