openapi: 3.0.3 info: title: Galaxy Backend REST API version: v1 description: | This specification documents the consolidated `galaxy/backend` REST surface consumed by `gateway` over the trusted internal network. It covers five route families: - `/api/v1/public/*` — unauthenticated public endpoints (only auth in MVP); - `/healthz`, `/readyz` — unauthenticated infrastructure probes; - `/api/v1/user/*` — authenticated end-user endpoints; the trusted `X-User-ID` header injected by gateway is the sole identity input; - `/api/v1/admin/*` — administrative endpoints gated by HTTP Basic Auth against the `admin_accounts` table; - `/api/v1/internal/*` — gateway-only server-to-server endpoints; trusted as part of the user surface in MVP (no extra auth). Every endpoint emits a JSON envelope of the shape `{"error":{"code":"...","message":"..."}}` on failure. The closed set of `code` values is enumerated under `components.schemas.ErrorBody`. JSON field names use `snake_case` everywhere on the wire. servers: - url: http://backend.internal description: | Backend internal listener reachable only from gateway. The actual address is configured by `BACKEND_HTTP_LISTEN_ADDR`. tags: - name: Public description: Unauthenticated public endpoints (registration and login). - name: Probes description: Liveness and readiness probes used by infrastructure tooling. - name: User description: Authenticated end-user endpoints; the trusted `X-User-ID` header carries identity. - name: Admin description: Administrator endpoints gated by HTTP Basic Auth. - name: Internal description: Gateway-only server-to-server endpoints used to lookup and revoke device sessions. paths: /healthz: get: tags: [Probes] operationId: getHealthz summary: Liveness probe description: Returns `200` for as long as the process is alive. security: [] responses: "200": description: Process is alive. content: application/json: schema: $ref: "#/components/schemas/HealthzResponse" "500": $ref: "#/components/responses/InternalError" /readyz: get: tags: [Probes] operationId: getReadyz summary: Readiness probe description: | Returns `200` once the Postgres pool is open, embedded migrations are applied, and the gRPC push listener is bound. Returns `503` until all of those hold. security: [] responses: "200": description: Process is ready to serve traffic. content: application/json: schema: $ref: "#/components/schemas/ReadyzResponse" "503": $ref: "#/components/responses/ServiceUnavailableError" "500": $ref: "#/components/responses/InternalError" /api/v1/public/auth/send-email-code: post: tags: [Public] operationId: publicAuthSendEmailCode summary: Issue an e-mail login challenge description: | Requests a six-digit login code be e-mailed to the supplied address. Returns the same opaque `challenge_id` shape regardless of whether the target account exists, so callers cannot use this endpoint to enumerate user accounts. Permanently blocked addresses are rejected with `400`. security: [] parameters: - $ref: "#/components/parameters/AcceptLanguage" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PublicAuthSendEmailCodeRequest" responses: "200": description: Challenge accepted; an e-mail will be delivered out-of-band. content: application/json: schema: $ref: "#/components/schemas/PublicAuthSendEmailCodeResponse" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/public/auth/confirm-email-code: post: tags: [Public] operationId: publicAuthConfirmEmailCode summary: Confirm an e-mail login challenge description: | Confirms a previously issued `challenge_id` by submitting the delivered verification `code` together with the client's Ed25519 public key and IANA time zone. On success the backend creates a device session and returns its identifier. security: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PublicAuthConfirmEmailCodeRequest" responses: "200": description: Device session created. content: application/json: schema: $ref: "#/components/schemas/PublicAuthConfirmEmailCodeResponse" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/account: get: tags: [User] operationId: userAccountGet summary: Get the current user account aggregate security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" responses: "200": description: Current account aggregate. content: application/json: schema: $ref: "#/components/schemas/AccountResponse" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/account/profile: patch: tags: [User] operationId: userAccountUpdateProfile summary: Update the caller's mutable profile fields security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateProfileRequest" responses: "200": description: Updated account aggregate. content: application/json: schema: $ref: "#/components/schemas/AccountResponse" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/account/settings: patch: tags: [User] operationId: userAccountUpdateSettings summary: Update the caller's settings fields security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateSettingsRequest" responses: "200": description: Updated account aggregate. content: application/json: schema: $ref: "#/components/schemas/AccountResponse" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/account/delete: post: tags: [User] operationId: userAccountDelete summary: Soft-delete the caller's account description: | Marks the caller's account `deleted_at` and triggers the documented in-process cascade across lobby, notification, and geo modules. security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" responses: "204": description: Account scheduled for soft delete. "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games: get: tags: [User] operationId: userLobbyGamesList summary: List public lobby games with paging security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: Page of public games. content: application/json: schema: $ref: "#/components/schemas/GameSummaryPage" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" post: tags: [User] operationId: userLobbyGamesCreate summary: Create a new private lobby game owned by the caller description: | Always emits a `private` game owned by `X-User-ID`. Public games are created via `POST /api/v1/admin/games`. security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/LobbyGameCreateRequest" responses: "201": description: Game created. content: application/json: schema: $ref: "#/components/schemas/LobbyGameDetail" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}: get: tags: [User] operationId: userLobbyGamesGet summary: Get the lobby game detail security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" responses: "200": description: Lobby game detail. content: application/json: schema: $ref: "#/components/schemas/LobbyGameDetail" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" patch: tags: [User] operationId: userLobbyGamesUpdate summary: Update mutable lobby game fields (owner only) security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/LobbyGameUpdateRequest" responses: "200": description: Updated lobby game detail. content: application/json: schema: $ref: "#/components/schemas/LobbyGameDetail" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/open-enrollment: post: tags: [User] operationId: userLobbyGamesOpenEnrollment summary: Move a draft game into `enrollment_open` security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" responses: "200": description: Enrollment opened. content: application/json: schema: $ref: "#/components/schemas/LobbyGameStateChange" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/ready-to-start: post: tags: [User] operationId: userLobbyGamesReadyToStart summary: Mark a game `ready_to_start` security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" responses: "200": description: Game transitioned to `ready_to_start`. content: application/json: schema: $ref: "#/components/schemas/LobbyGameStateChange" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/start: post: tags: [User] operationId: userLobbyGamesStart summary: Start the engine container for the game security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" responses: "202": description: Start request accepted; runtime job queued. content: application/json: schema: $ref: "#/components/schemas/LobbyGameStateChange" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/pause: post: tags: [User] operationId: userLobbyGamesPause summary: Pause a running game security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" responses: "200": description: Game paused. content: application/json: schema: $ref: "#/components/schemas/LobbyGameStateChange" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/resume: post: tags: [User] operationId: userLobbyGamesResume summary: Resume a paused game security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" responses: "200": description: Game resumed. content: application/json: schema: $ref: "#/components/schemas/LobbyGameStateChange" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/cancel: post: tags: [User] operationId: userLobbyGamesCancel summary: Cancel a game (owner) security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" responses: "200": description: Game cancelled. content: application/json: schema: $ref: "#/components/schemas/LobbyGameStateChange" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/retry-start: post: tags: [User] operationId: userLobbyGamesRetryStart summary: Retry a failed start security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" responses: "202": description: Retry queued. content: application/json: schema: $ref: "#/components/schemas/LobbyGameStateChange" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/applications: post: tags: [User] operationId: userLobbyApplicationsSubmit summary: Submit an application to join a game security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/LobbyApplicationSubmitRequest" responses: "201": description: Application created. content: application/json: schema: $ref: "#/components/schemas/LobbyApplicationDetail" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/applications/{application_id}/approve: post: tags: [User] operationId: userLobbyApplicationsApprove summary: Approve an application (owner or admin) security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" - $ref: "#/components/parameters/ApplicationID" responses: "200": description: Application approved. content: application/json: schema: $ref: "#/components/schemas/LobbyApplicationDetail" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/applications/{application_id}/reject: post: tags: [User] operationId: userLobbyApplicationsReject summary: Reject an application (owner or admin) security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" - $ref: "#/components/parameters/ApplicationID" responses: "200": description: Application rejected. content: application/json: schema: $ref: "#/components/schemas/LobbyApplicationDetail" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/invites: post: tags: [User] operationId: userLobbyInvitesIssue summary: Issue an invite to join a private game security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/LobbyInviteIssueRequest" responses: "201": description: Invite created. content: application/json: schema: $ref: "#/components/schemas/LobbyInviteDetail" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/invites/{invite_id}/redeem: post: tags: [User] operationId: userLobbyInvitesRedeem summary: Redeem an invite to create a membership security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" - $ref: "#/components/parameters/InviteID" responses: "200": description: Invite redeemed. content: application/json: schema: $ref: "#/components/schemas/LobbyInviteDetail" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/invites/{invite_id}/decline: post: tags: [User] operationId: userLobbyInvitesDecline summary: Decline an invite (recipient) security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" - $ref: "#/components/parameters/InviteID" responses: "200": description: Invite declined. content: application/json: schema: $ref: "#/components/schemas/LobbyInviteDetail" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/invites/{invite_id}/revoke: post: tags: [User] operationId: userLobbyInvitesRevoke summary: Revoke an invite (issuer) security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" - $ref: "#/components/parameters/InviteID" responses: "200": description: Invite revoked. content: application/json: schema: $ref: "#/components/schemas/LobbyInviteDetail" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/memberships: get: tags: [User] operationId: userLobbyMembershipsList summary: List memberships for a game security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" responses: "200": description: Memberships for the game. content: application/json: schema: $ref: "#/components/schemas/LobbyMembershipList" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/memberships/{membership_id}/remove: post: tags: [User] operationId: userLobbyMembershipsRemove summary: Remove a membership (owner or self) security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" - $ref: "#/components/parameters/MembershipID" responses: "200": description: Membership removed. content: application/json: schema: $ref: "#/components/schemas/LobbyMembershipDetail" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/games/{game_id}/memberships/{membership_id}/block: post: tags: [User] operationId: userLobbyMembershipsBlock summary: Block a membership (owner) security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" - $ref: "#/components/parameters/MembershipID" responses: "200": description: Membership blocked. content: application/json: schema: $ref: "#/components/schemas/LobbyMembershipDetail" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/my/games: get: tags: [User] operationId: userLobbyMyGames summary: List games the caller participates in security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" responses: "200": description: Caller's games. content: application/json: schema: $ref: "#/components/schemas/MyGamesListResponse" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/my/applications: get: tags: [User] operationId: userLobbyMyApplications summary: List the caller's applications security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" responses: "200": description: Caller's applications. content: application/json: schema: $ref: "#/components/schemas/LobbyApplicationList" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/my/invites: get: tags: [User] operationId: userLobbyMyInvites summary: List the caller's invites security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" responses: "200": description: Caller's invites. content: application/json: schema: $ref: "#/components/schemas/LobbyInviteList" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/my/race-names: get: tags: [User] operationId: userLobbyMyRaceNames summary: List the caller's race names security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" responses: "200": description: Caller's race-name records. content: application/json: schema: $ref: "#/components/schemas/RaceNameList" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/race-names/register: post: tags: [User] operationId: userLobbyRaceNamesRegister summary: Promote a `pending_registration` to `registered` security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RaceNameRegisterRequest" responses: "200": description: Race name promoted. content: application/json: schema: $ref: "#/components/schemas/RaceNameDetail" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/games/{game_id}/commands: post: tags: [User] operationId: userGamesCommands summary: Forward an engine command batch security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EngineCommand" responses: "200": description: Engine command result passed through. content: application/json: schema: $ref: "#/components/schemas/PassthroughObject" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/games/{game_id}/orders: post: tags: [User] operationId: userGamesOrders summary: Forward an engine order batch security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EngineOrder" responses: "200": description: | Engine order validation result passed through. Body is the engine's `UserGamesOrder` shape — game_id, updatedAt, and the per-command `cmd[]` list with `cmdApplied` / `cmdErrorCode` populated by the engine. content: application/json: schema: $ref: "#/components/schemas/PassthroughObject" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" get: tags: [User] operationId: userGamesGetOrders summary: Read the player's stored order for a turn description: | Forwards `GET /api/v1/order` against the engine container. The caller always knows the current turn from the lobby record at game boot, so `turn` is required. security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" - name: turn in: query required: true description: Turn number whose stored order to fetch. Non-negative. schema: type: integer format: int32 minimum: 0 responses: "200": description: | Engine returned the stored order for this player + turn. Body is the engine's `UserGamesOrder` shape. content: application/json: schema: $ref: "#/components/schemas/PassthroughObject" "204": description: No order has been stored for this player on this turn. "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/games/{game_id}/reports/{turn}: get: tags: [User] operationId: userGamesReport summary: Read an engine turn report security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" - $ref: "#/components/parameters/Turn" responses: "200": description: Engine report passed through. content: application/json: schema: $ref: "#/components/schemas/PassthroughObject" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/games/{game_id}/battles/{turn}/{battle_id}: get: tags: [User] operationId: userGamesBattle summary: Read one engine battle report description: | Forwards to the engine's `GET /api/v1/battle/:turn/:uuid`. The engine response body is passed through verbatim. `404 Not Found` is returned when the battle does not exist for the supplied `turn` / `battle_id` pair. security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" - $ref: "#/components/parameters/Turn" - name: battle_id in: path required: true description: Battle identifier (RFC 4122 UUID). schema: type: string format: uuid responses: "200": description: Engine battle report passed through. content: application/json: schema: $ref: "#/components/schemas/PassthroughObject" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/games/{game_id}/mail/messages: post: tags: [User] operationId: userMailSendPersonal summary: Send a personal diplomatic mail message description: | Sends a replyable personal message from the authenticated user to another active member of the same game. Both sender and recipient must be active members. Body is plain UTF-8 text (no HTML processing on the server); `subject` is optional. Body length is capped at `BACKEND_DIPLOMAIL_MAX_BODY_BYTES` (default 4096) and subject length at `BACKEND_DIPLOMAIL_MAX_SUBJECT_BYTES` (default 256). security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UserMailSendRequest" responses: "201": description: Personal message accepted and persisted. content: application/json: schema: $ref: "#/components/schemas/UserMailMessageDetail" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/games/{game_id}/mail/broadcast: post: tags: [User] operationId: userMailSendBroadcast summary: Send a paid-tier personal broadcast to a game's active members description: | Paid-tier players (`entitlement.is_paid == true`) may send one personal message that fans out to every other active member of the game. Free-tier callers receive 403. The resulting rows carry `kind="personal"`, `sender_kind="player"`, `broadcast_scope="game_broadcast"`. Recipients reply through the regular personal-send endpoint; the reply targets the broadcaster only. security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UserMailSendBroadcastRequest" responses: "201": description: Personal broadcast accepted; receipt carries the recipient count. content: application/json: schema: $ref: "#/components/schemas/UserMailBroadcastReceipt" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/games/{game_id}/mail/admin: post: tags: [User] operationId: userMailSendAdmin summary: Send a non-replyable admin notification (owner only) description: | Owner-only: the caller must be the owner of the private game. `target="user"` requires `recipient_user_id`; `target="all"` accepts an optional `recipients` scope (`active` by default, plus `active_and_removed` and `all_members`). The message carries `kind="admin"` and is therefore non-replyable. security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UserMailSendAdminRequest" responses: "201": description: Admin message persisted; broadcasts return a fan-out receipt. content: application/json: schema: oneOf: - $ref: "#/components/schemas/UserMailMessageDetail" - $ref: "#/components/schemas/UserMailBroadcastReceipt" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/games/{game_id}/mail/messages/{message_id}: get: tags: [User] operationId: userMailGet summary: Read one diplomatic mail message security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" - $ref: "#/components/parameters/MessageID" responses: "200": description: Message addressed to the caller. content: application/json: schema: $ref: "#/components/schemas/UserMailMessageDetail" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" delete: tags: [User] operationId: userMailDelete summary: Soft-delete a previously-read message description: | Marks the caller's recipient row for the message as deleted. The underlying message stays persisted (admin / system mail is retained for the lifetime of the game). The recipient row must have `read_at` set first; otherwise the call returns 409. security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" - $ref: "#/components/parameters/MessageID" responses: "200": description: Message soft-deleted for the caller. content: application/json: schema: $ref: "#/components/schemas/UserMailRecipientState" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/games/{game_id}/mail/messages/{message_id}/read: post: tags: [User] operationId: userMailMarkRead summary: Mark a diplomatic mail message as read description: | Idempotent. Sets `read_at` on the caller's recipient row when it is still unread; a second call on an already-read row is a no-op and the existing state is returned. security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" - $ref: "#/components/parameters/MessageID" responses: "200": description: Recipient state after the mark-read. content: application/json: schema: $ref: "#/components/schemas/UserMailRecipientState" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/games/{game_id}/mail/inbox: get: tags: [User] operationId: userMailInbox summary: List the caller's inbox for a game description: | Returns every non-soft-deleted mail row addressed to the caller in the given game, newest first. Includes the per-recipient read state. Soft access: the caller may not be an active member if every visible row carries `kind="admin"`. security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" responses: "200": description: Inbox entries for the caller in the given game. content: application/json: schema: $ref: "#/components/schemas/UserMailInboxList" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/games/{game_id}/mail/sent: get: tags: [User] operationId: userMailSent summary: List the caller's sent personal messages in a game description: | Returns personal messages authored by the caller in the given game, newest first. Admin / system messages are not listed (they have no `sender_user_id`). security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/GameID" responses: "200": description: Sent personal messages by the caller. content: application/json: schema: $ref: "#/components/schemas/UserMailSentList" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/lobby/mail/unread-counts: get: tags: [User] operationId: userMailUnreadCounts summary: Per-game and total unread mail counts for the caller description: | Drives the lobby badge: returns one entry per game the caller has any unread mail in, plus the global total. The response is empty (and `total == 0`) when there is nothing unread. security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" responses: "200": description: Per-game unread counts addressed to the caller. content: application/json: schema: $ref: "#/components/schemas/UserMailUnreadCountsResponse" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/sessions: get: tags: [User] operationId: userSessionsList summary: List the caller's active device sessions security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" responses: "200": description: Caller's active device sessions. content: application/json: schema: $ref: "#/components/schemas/UserSessionList" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/sessions/revoke-all: post: tags: [User] operationId: userSessionsRevokeAll summary: Revoke every device session belonging to the caller description: | Logout from every device. Subsequent authenticated requests on any of the caller's sessions are rejected. Each revocation is recorded in `session_revocations` with `actor_kind=user_self`. security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" responses: "200": description: Caller's sessions revoked. content: application/json: schema: $ref: "#/components/schemas/DeviceSessionRevocationSummary" "400": $ref: "#/components/responses/InvalidRequestError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/user/sessions/{device_session_id}/revoke: post: tags: [User] operationId: userSessionsRevoke summary: Revoke one of the caller's device sessions description: | Logout from a single device. The target `device_session_id` must belong to the caller; otherwise the endpoint returns `404 not_found` (the same shape as a missing session) so the endpoint cannot be used to probe foreign session ids. The revocation is recorded in `session_revocations` with `actor_kind=user_self`. security: - UserHeader: [] parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/DeviceSessionID" responses: "200": description: Device session revoked. content: application/json: schema: $ref: "#/components/schemas/DeviceSession" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/admin-accounts: get: tags: [Admin] operationId: adminAdminAccountsList summary: List admin accounts security: - AdminBasicAuth: [] responses: "200": description: Admin accounts. content: application/json: schema: $ref: "#/components/schemas/AdminAccountList" "401": $ref: "#/components/responses/UnauthorizedError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" post: tags: [Admin] operationId: adminAdminAccountsCreate summary: Create an admin account security: - AdminBasicAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AdminAccountCreateRequest" responses: "201": description: Admin account created. content: application/json: schema: $ref: "#/components/schemas/AdminAccount" "400": $ref: "#/components/responses/InvalidRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/admin-accounts/{username}: get: tags: [Admin] operationId: adminAdminAccountsGet summary: Get an admin account security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/Username" responses: "200": description: Admin account. content: application/json: schema: $ref: "#/components/schemas/AdminAccount" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/admin-accounts/{username}/disable: post: tags: [Admin] operationId: adminAdminAccountsDisable summary: Disable an admin account security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/Username" responses: "200": description: Admin account disabled. content: application/json: schema: $ref: "#/components/schemas/AdminAccount" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/admin-accounts/{username}/enable: post: tags: [Admin] operationId: adminAdminAccountsEnable summary: Enable an admin account security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/Username" responses: "200": description: Admin account enabled. content: application/json: schema: $ref: "#/components/schemas/AdminAccount" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/admin-accounts/{username}/reset-password: post: tags: [Admin] operationId: adminAdminAccountsResetPassword summary: Reset an admin account password security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/Username" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AdminAccountResetPasswordRequest" responses: "200": description: Password reset; the new value is delivered out-of-band. content: application/json: schema: $ref: "#/components/schemas/AdminAccount" "400": $ref: "#/components/responses/InvalidRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/users: get: tags: [Admin] operationId: adminUsersList summary: List users security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: Page of users. content: application/json: schema: $ref: "#/components/schemas/AdminUserList" "401": $ref: "#/components/responses/UnauthorizedError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/users/{user_id}: get: tags: [Admin] operationId: adminUsersGet summary: Get a user account aggregate security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/UserID" responses: "200": description: Account aggregate. content: application/json: schema: $ref: "#/components/schemas/AccountResponse" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/users/{user_id}/sanctions: post: tags: [Admin] operationId: adminUsersAddSanction summary: Apply a sanction to a user security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/UserID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AdminUserSanctionRequest" responses: "200": description: Sanction applied. content: application/json: schema: $ref: "#/components/schemas/AccountResponse" "400": $ref: "#/components/responses/InvalidRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/users/{user_id}/limits: post: tags: [Admin] operationId: adminUsersAddLimit summary: Apply a per-user limit override security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/UserID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AdminUserLimitRequest" responses: "200": description: Limit applied. content: application/json: schema: $ref: "#/components/schemas/AccountResponse" "400": $ref: "#/components/responses/InvalidRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/users/{user_id}/entitlements: post: tags: [Admin] operationId: adminUsersAddEntitlement summary: Update a user's entitlement security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/UserID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AdminUserEntitlementRequest" responses: "200": description: Entitlement updated. content: application/json: schema: $ref: "#/components/schemas/AccountResponse" "400": $ref: "#/components/responses/InvalidRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/users/{user_id}/soft-delete: post: tags: [Admin] operationId: adminUsersSoftDelete summary: Soft-delete a user (admin) security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/UserID" responses: "204": description: User scheduled for soft delete. "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/games: get: tags: [Admin] operationId: adminGamesList summary: List games for administration security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: Page of games. content: application/json: schema: $ref: "#/components/schemas/AdminGameList" "401": $ref: "#/components/responses/UnauthorizedError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" post: tags: [Admin] operationId: adminGamesCreate summary: Create a public lobby game (admin-only) description: | Creates a public game owned collectively by administrators (`visibility=public`, `owner_user_id=NULL`). The user-facing `POST /api/v1/user/lobby/games` only creates private games. security: - AdminBasicAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AdminGameCreateRequest" responses: "201": description: Public game created. content: application/json: schema: $ref: "#/components/schemas/LobbyGameDetail" "400": $ref: "#/components/responses/InvalidRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/games/{game_id}: get: tags: [Admin] operationId: adminGamesGet summary: Get an admin-side game detail security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/GameID" responses: "200": description: Game detail. content: application/json: schema: $ref: "#/components/schemas/LobbyGameDetail" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/games/{game_id}/force-start: post: tags: [Admin] operationId: adminGamesForceStart summary: Force-start a game security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/GameID" responses: "202": description: Force-start queued. content: application/json: schema: $ref: "#/components/schemas/LobbyGameStateChange" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/games/{game_id}/force-stop: post: tags: [Admin] operationId: adminGamesForceStop summary: Force-stop a game security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/GameID" responses: "200": description: Force-stop accepted. content: application/json: schema: $ref: "#/components/schemas/LobbyGameStateChange" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/games/{game_id}/ban-member: post: tags: [Admin] operationId: adminGamesBanMember summary: Ban a member from a game security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/GameID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AdminGameBanMemberRequest" responses: "200": description: Member banned. content: application/json: schema: $ref: "#/components/schemas/LobbyMembershipDetail" "400": $ref: "#/components/responses/InvalidRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/mail/broadcast: post: tags: [Admin] operationId: adminDiplomailBroadcast summary: Multi-game admin broadcast description: | Fans out one admin-kind broadcast across the games selected by `scope`. `scope="selected"` requires `game_ids`; `scope="all_running"` enumerates every game whose status is non-terminal. Recipients are resolved per-game via the same scope vocabulary as the per-game admin send. A recipient appearing in multiple addressed games receives one independently-deletable inbox entry per game. security: - AdminBasicAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AdminDiplomailBroadcastRequest" responses: "201": description: Broadcast accepted; per-game message ids and total recipient count. content: application/json: schema: $ref: "#/components/schemas/AdminDiplomailBroadcastResponse" "400": $ref: "#/components/responses/InvalidRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/mail/cleanup: post: tags: [Admin] operationId: adminDiplomailCleanup summary: Bulk-purge diplomail messages from old finished games description: | Removes every `diplomail_messages` row whose game finished more than `older_than_years` years ago. Cascading FKs prune the recipient and translation tables in the same transaction. `older_than_years` must be >= 1. security: - AdminBasicAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AdminDiplomailCleanupRequest" responses: "200": description: Cleanup result. content: application/json: schema: $ref: "#/components/schemas/AdminDiplomailCleanupResponse" "400": $ref: "#/components/responses/InvalidRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/mail/messages: get: tags: [Admin] operationId: adminDiplomailList summary: Paginated admin view of diplomail messages description: | Returns the canonical message rows for admin observability. Optional filters: `game_id`, `kind` (personal / admin), `sender_kind` (player / admin / system). Pagination via `page` and `page_size`. security: - AdminBasicAuth: [] parameters: - name: page in: query required: false schema: type: integer minimum: 1 - name: page_size in: query required: false schema: type: integer minimum: 1 - name: game_id in: query required: false schema: type: string format: uuid - name: kind in: query required: false schema: type: string enum: [personal, admin] - name: sender_kind in: query required: false schema: type: string enum: [player, admin, system] responses: "200": description: Paginated diplomail messages. content: application/json: schema: $ref: "#/components/schemas/AdminDiplomailListResponse" "400": $ref: "#/components/responses/InvalidRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/games/{game_id}/mail: post: tags: [Admin] operationId: adminDiplomailSend summary: Send a diplomatic-mail admin notification to one game description: | Site-admin send for the diplomatic-mail subsystem. Body shape mirrors the owner-only `POST /api/v1/user/games/{game_id}/mail/admin` endpoint. `target="user"` requires `recipient_user_id`; `target="all"` accepts an optional `recipients` scope (`active` / `active_and_removed` / `all_members`). The authenticated admin username is persisted as `sender_username`. security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/GameID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UserMailSendAdminRequest" responses: "201": description: Admin message persisted; broadcasts return a fan-out receipt. content: application/json: schema: oneOf: - $ref: "#/components/schemas/UserMailMessageDetail" - $ref: "#/components/schemas/UserMailBroadcastReceipt" "400": $ref: "#/components/responses/InvalidRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/runtimes/{game_id}: get: tags: [Admin] operationId: adminRuntimesGet summary: Read the runtime record for a game security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/GameID" responses: "200": description: Runtime record. content: application/json: schema: $ref: "#/components/schemas/RuntimeRecord" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/runtimes/{game_id}/restart: post: tags: [Admin] operationId: adminRuntimesRestart summary: Restart the engine container for a game security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/GameID" responses: "202": description: Restart queued. content: application/json: schema: $ref: "#/components/schemas/RuntimeOperation" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/runtimes/{game_id}/patch: post: tags: [Admin] operationId: adminRuntimesPatch summary: Patch the engine version (semver-patch only) security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/GameID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RuntimePatchRequest" responses: "202": description: Patch queued. content: application/json: schema: $ref: "#/components/schemas/RuntimeOperation" "400": $ref: "#/components/responses/InvalidRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/runtimes/{game_id}/force-next-turn: post: tags: [Admin] operationId: adminRuntimesForceNextTurn summary: Schedule a one-shot extra turn tick security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/GameID" responses: "200": description: One-shot tick scheduled. content: application/json: schema: $ref: "#/components/schemas/RuntimeOperation" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/engine-versions: get: tags: [Admin] operationId: adminEngineVersionsList summary: List engine versions security: - AdminBasicAuth: [] responses: "200": description: Engine versions. content: application/json: schema: $ref: "#/components/schemas/EngineVersionList" "401": $ref: "#/components/responses/UnauthorizedError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" post: tags: [Admin] operationId: adminEngineVersionsCreate summary: Register a new engine version security: - AdminBasicAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EngineVersionCreateRequest" responses: "201": description: Engine version registered. content: application/json: schema: $ref: "#/components/schemas/EngineVersion" "400": $ref: "#/components/responses/InvalidRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/engine-versions/{id}: patch: tags: [Admin] operationId: adminEngineVersionsUpdate summary: Update an engine version record security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/EngineVersionID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EngineVersionUpdateRequest" responses: "200": description: Engine version updated. content: application/json: schema: $ref: "#/components/schemas/EngineVersion" "400": $ref: "#/components/responses/InvalidRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/engine-versions/{id}/disable: post: tags: [Admin] operationId: adminEngineVersionsDisable summary: Disable an engine version security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/EngineVersionID" responses: "200": description: Engine version disabled. content: application/json: schema: $ref: "#/components/schemas/EngineVersion" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/mail/deliveries: get: tags: [Admin] operationId: adminMailListDeliveries summary: List mail deliveries security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: Page of mail deliveries. content: application/json: schema: $ref: "#/components/schemas/MailDeliveryList" "401": $ref: "#/components/responses/UnauthorizedError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/mail/deliveries/{delivery_id}: get: tags: [Admin] operationId: adminMailGetDelivery summary: Get a mail delivery security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/DeliveryID" responses: "200": description: Mail delivery. content: application/json: schema: $ref: "#/components/schemas/MailDelivery" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/mail/deliveries/{delivery_id}/attempts: get: tags: [Admin] operationId: adminMailListDeliveryAttempts summary: List mail delivery attempts security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/DeliveryID" responses: "200": description: Mail delivery attempts. content: application/json: schema: $ref: "#/components/schemas/MailAttemptList" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/mail/deliveries/{delivery_id}/resend: post: tags: [Admin] operationId: adminMailResendDelivery summary: Resend a non-sent mail delivery description: | Re-arms a delivery for another attempt cycle. Allowed states are `pending`, `retrying`, and `dead_lettered`. Resend on a `sent` delivery returns `409 Conflict` to prevent operators from accidentally dispatching a duplicate copy of an already-delivered mail. security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/DeliveryID" responses: "202": description: Resend scheduled. content: application/json: schema: $ref: "#/components/schemas/MailDelivery" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/mail/dead-letters: get: tags: [Admin] operationId: adminMailListDeadLetters summary: List mail dead-letters security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: Page of dead-letters. content: application/json: schema: $ref: "#/components/schemas/MailDeadLetterList" "401": $ref: "#/components/responses/UnauthorizedError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/notifications: get: tags: [Admin] operationId: adminNotificationsList summary: List notifications security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: Page of notifications. content: application/json: schema: $ref: "#/components/schemas/NotificationList" "401": $ref: "#/components/responses/UnauthorizedError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/notifications/{notification_id}: get: tags: [Admin] operationId: adminNotificationsGet summary: Get a notification security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/NotificationID" responses: "200": description: Notification. content: application/json: schema: $ref: "#/components/schemas/NotificationDetail" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/notifications/dead-letters: get: tags: [Admin] operationId: adminNotificationsListDeadLetters summary: List notification dead-letters security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: Page of notification dead-letters. content: application/json: schema: $ref: "#/components/schemas/NotificationDeadLetterList" "401": $ref: "#/components/responses/UnauthorizedError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/notifications/malformed: get: tags: [Admin] operationId: adminNotificationsListMalformed summary: List malformed notification intents security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PageSize" responses: "200": description: Page of malformed intents. content: application/json: schema: $ref: "#/components/schemas/NotificationMalformedList" "401": $ref: "#/components/responses/UnauthorizedError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/admin/geo/users/{user_id}/countries: get: tags: [Admin] operationId: adminGeoListUserCountries summary: List per-country counters for a user security: - AdminBasicAuth: [] parameters: - $ref: "#/components/parameters/UserID" responses: "200": description: Per-country counters for the user. content: application/json: schema: $ref: "#/components/schemas/GeoCountryCounterList" "401": $ref: "#/components/responses/UnauthorizedError" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/internal/sessions/{device_session_id}: get: tags: [Internal] operationId: internalSessionsGet summary: Look up a device session (gateway-only) security: [] parameters: - $ref: "#/components/parameters/DeviceSessionID" responses: "200": description: Device session record. content: application/json: schema: $ref: "#/components/schemas/DeviceSession" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" /api/v1/internal/users/{user_id}/account-internal: get: tags: [Internal] operationId: internalUsersGetAccountInternal summary: Server-to-server fetch of an account aggregate (gateway-only) security: [] parameters: - $ref: "#/components/parameters/UserID" responses: "200": description: Account aggregate enriched for gateway flows. content: application/json: schema: $ref: "#/components/schemas/AccountResponse" "404": $ref: "#/components/responses/NotFoundError" "501": $ref: "#/components/responses/NotImplementedError" "500": $ref: "#/components/responses/InternalError" components: parameters: AcceptLanguage: name: Accept-Language in: header required: false description: | Optional RFC 9110 Accept-Language header forwarded by gateway. Backend uses it as a fallback locale source when the request body does not carry an explicit `locale` field. schema: type: string XUserID: name: X-User-ID in: header required: true description: | Trusted UUID identifying the calling user. Injected by gateway after request signature verification. Backend never re-derives identity from the request body on the user surface. schema: type: string format: uuid GameID: name: game_id in: path required: true schema: type: string format: uuid ApplicationID: name: application_id in: path required: true schema: type: string format: uuid InviteID: name: invite_id in: path required: true schema: type: string format: uuid MembershipID: name: membership_id in: path required: true schema: type: string format: uuid MessageID: name: message_id in: path required: true schema: type: string format: uuid NotificationID: name: notification_id in: path required: true schema: type: string format: uuid DeliveryID: name: delivery_id in: path required: true schema: type: string format: uuid UserID: name: user_id in: path required: true schema: type: string format: uuid DeviceSessionID: name: device_session_id in: path required: true schema: type: string format: uuid EngineVersionID: name: id in: path required: true schema: type: string Username: name: username in: path required: true schema: type: string Turn: name: turn in: path required: true schema: type: integer minimum: 0 Page: name: page in: query required: false schema: type: integer minimum: 1 default: 1 PageSize: name: page_size in: query required: false schema: type: integer minimum: 1 maximum: 200 default: 50 securitySchemes: UserHeader: type: apiKey in: header name: X-User-ID description: | The trusted UUID forwarded by gateway after the device-session signature has been verified. AdminBasicAuth: type: http scheme: basic description: | Basic Auth credentials checked against `admin_accounts` with bcrypt cost 12. Failed authentication returns `401` with `WWW-Authenticate: Basic realm="galaxy-admin"`. schemas: HealthzResponse: type: object additionalProperties: false required: [status] properties: status: type: string enum: [ok] ReadyzResponse: type: object additionalProperties: false required: [status] properties: status: type: string enum: [ready, starting] ErrorBody: type: object additionalProperties: false required: [code, message] properties: code: type: string description: | Stable machine-readable failure marker. The closed set is `not_implemented`, `invalid_request`, `unauthorized`, `forbidden`, `not_found`, `conflict`, `method_not_allowed`, `internal_error`, `service_unavailable`, `turn_already_closed`, `game_paused`. enum: - not_implemented - invalid_request - unauthorized - forbidden - not_found - conflict - method_not_allowed - internal_error - service_unavailable - turn_already_closed - game_paused message: type: string description: Human-readable client-safe failure description. ErrorResponse: type: object additionalProperties: false required: [error] properties: error: $ref: "#/components/schemas/ErrorBody" PublicAuthSendEmailCodeRequest: type: object additionalProperties: false required: [email] properties: email: type: string format: email locale: type: string description: | Optional BCP 47 locale tag preferred for the delivered code. Read by the gateway in preference to the request `Accept-Language` header so Safari clients (which silently drop JS-set `Accept-Language`) can still pick a non-system mail language. Empty / malformed values fall back to the header, which in turn falls back to `en`. PublicAuthSendEmailCodeResponse: type: object additionalProperties: false required: [challenge_id] properties: challenge_id: type: string description: Opaque identifier of the issued challenge. PublicAuthConfirmEmailCodeRequest: type: object additionalProperties: false required: [challenge_id, code, client_public_key, time_zone] properties: challenge_id: type: string code: type: string description: Verification code delivered by mail. client_public_key: type: string description: Standard base64-encoded raw 32-byte Ed25519 public key. time_zone: type: string description: IANA time-zone identifier provided by the client. PublicAuthConfirmEmailCodeResponse: type: object additionalProperties: false required: [device_session_id] properties: device_session_id: type: string format: uuid ActorRef: type: object additionalProperties: false required: [type] properties: type: type: string id: type: string EntitlementSnapshot: type: object additionalProperties: false required: - plan_code - is_paid - source - actor - reason_code - starts_at - max_registered_race_names - updated_at properties: plan_code: type: string enum: [free, monthly, yearly, permanent] description: | Closed tier vocabulary. The wire field name is `plan_code`; the storage column is `entitlement_snapshots.tier`. is_paid: type: boolean source: type: string actor: $ref: "#/components/schemas/ActorRef" reason_code: type: string starts_at: type: string format: date-time ends_at: type: string format: date-time nullable: true max_registered_race_names: type: integer description: | Derived from the tier policy table. `free` accounts get 1; `monthly`, `yearly`, and `permanent` accounts get 5 in MVP. updated_at: type: string format: date-time ActiveSanction: type: object additionalProperties: false required: [sanction_code, scope, reason_code, actor, applied_at] properties: sanction_code: type: string scope: type: string reason_code: type: string actor: $ref: "#/components/schemas/ActorRef" applied_at: type: string format: date-time expires_at: type: string format: date-time nullable: true ActiveLimit: type: object additionalProperties: false required: [limit_code, value, reason_code, actor, applied_at] properties: limit_code: type: string value: type: integer reason_code: type: string actor: $ref: "#/components/schemas/ActorRef" applied_at: type: string format: date-time expires_at: type: string format: date-time nullable: true Account: type: object additionalProperties: false required: - user_id - email - user_name - preferred_language - time_zone - entitlement - active_sanctions - active_limits - created_at - updated_at properties: user_id: type: string format: uuid email: type: string format: email user_name: type: string display_name: type: string preferred_language: type: string time_zone: type: string declared_country: type: string entitlement: $ref: "#/components/schemas/EntitlementSnapshot" active_sanctions: type: array items: $ref: "#/components/schemas/ActiveSanction" active_limits: type: array items: $ref: "#/components/schemas/ActiveLimit" created_at: type: string format: date-time updated_at: type: string format: date-time AccountResponse: type: object additionalProperties: false required: [account] properties: account: $ref: "#/components/schemas/Account" UpdateProfileRequest: type: object additionalProperties: false properties: display_name: type: string description: Replacement display name; an empty value clears the field. UpdateSettingsRequest: type: object additionalProperties: false properties: preferred_language: type: string time_zone: type: string GameSummary: type: object additionalProperties: false required: - game_id - game_name - game_type - status - min_players - max_players - enrollment_ends_at - created_at - updated_at - current_turn properties: game_id: type: string format: uuid game_name: type: string game_type: type: string enum: [public, private] description: | Wire alias for `visibility`; values match the storage column `games.visibility`. `public` games are admin-created and carry `owner_user_id IS NULL`; `private` games are owned by the calling user. status: type: string enum: - draft - enrollment_open - ready_to_start - starting - start_failed - running - paused - finished - cancelled owner_user_id: type: string format: uuid nullable: true description: | Owner user_id for private games; `null` for public games whose ownership is collective and managed by administrators. min_players: type: integer minimum: 1 max_players: type: integer minimum: 1 enrollment_ends_at: type: string format: date-time created_at: type: string format: date-time updated_at: type: string format: date-time current_turn: type: integer description: | Most recent turn number observed by backend's runtime projection. Zero before the engine produces its first snapshot. The user surface uses it to fetch the matching `user.games.report` without a separate state query. GameSummaryPage: type: object additionalProperties: false required: [items, page, page_size, total] properties: items: type: array items: $ref: "#/components/schemas/GameSummary" page: type: integer page_size: type: integer total: type: integer MyGamesListResponse: type: object additionalProperties: false required: [items] properties: items: type: array items: $ref: "#/components/schemas/GameSummary" LobbyGameCreateRequest: type: object additionalProperties: false required: - game_name - visibility - min_players - max_players - start_gap_hours - start_gap_players - enrollment_ends_at - turn_schedule - target_engine_version properties: game_name: type: string minLength: 1 visibility: type: string enum: [private] description: | User-facing game creation always emits `private` games. Public games are created by admins via `POST /api/v1/admin/games`. description: type: string min_players: type: integer minimum: 1 max_players: type: integer minimum: 1 start_gap_hours: type: integer minimum: 0 start_gap_players: type: integer minimum: 0 enrollment_ends_at: type: string format: date-time turn_schedule: type: string description: Five-field cron expression accepted by `pkg/cronutil.Parse`. target_engine_version: type: string description: | Engine version label (semver). Cross-checked against `engine_versions` at start time; rejected if no enabled row matches. LobbyGameUpdateRequest: type: object additionalProperties: false description: | Mutable lobby game fields (owner-only patch). Status transitions are driven through dedicated endpoints (`open-enrollment`, `ready-to-start`, `start`, `pause`, `resume`, `cancel`, `retry-start`). properties: game_name: type: string minLength: 1 description: type: string enrollment_ends_at: type: string format: date-time turn_schedule: type: string target_engine_version: type: string min_players: type: integer minimum: 1 max_players: type: integer minimum: 1 start_gap_hours: type: integer minimum: 0 start_gap_players: type: integer minimum: 0 AdminGameCreateRequest: type: object additionalProperties: false required: - game_name - min_players - max_players - start_gap_hours - start_gap_players - enrollment_ends_at - turn_schedule - target_engine_version description: | Admin-side public-game creation. The `visibility` of the created record is hard-coded to `public` and `owner_user_id` is `NULL`. properties: game_name: type: string minLength: 1 description: type: string min_players: type: integer minimum: 1 max_players: type: integer minimum: 1 start_gap_hours: type: integer minimum: 0 start_gap_players: type: integer minimum: 0 enrollment_ends_at: type: string format: date-time turn_schedule: type: string target_engine_version: type: string LobbyGameDetail: allOf: - $ref: "#/components/schemas/GameSummary" - type: object additionalProperties: false required: - visibility - turn_schedule - target_engine_version - start_gap_hours - start_gap_players - runtime_status properties: visibility: type: string enum: [public, private] description: type: string turn_schedule: type: string target_engine_version: type: string start_gap_hours: type: integer start_gap_players: type: integer runtime_status: type: string engine_health: type: string started_at: type: string format: date-time nullable: true finished_at: type: string format: date-time nullable: true LobbyGameStateChange: type: object additionalProperties: false required: [game_id, status] properties: game_id: type: string format: uuid status: type: string runtime_status: type: string LobbyApplicationSubmitRequest: type: object additionalProperties: false required: [race_name] properties: race_name: type: string minLength: 1 LobbyApplicationDetail: type: object additionalProperties: false required: - application_id - game_id - applicant_user_id - race_name - status - created_at properties: application_id: type: string format: uuid game_id: type: string format: uuid applicant_user_id: type: string format: uuid race_name: type: string status: type: string enum: [pending, approved, rejected] created_at: type: string format: date-time decided_at: type: string format: date-time nullable: true LobbyApplicationList: type: object additionalProperties: false required: [items] properties: items: type: array items: $ref: "#/components/schemas/LobbyApplicationDetail" LobbyInviteIssueRequest: type: object additionalProperties: false description: | Issues either a user-bound invite (when `invited_user_id` is set) or a one-shot code-based invite (when omitted). The server generates the redemption `code` for the latter. properties: invited_user_id: type: string format: uuid race_name: type: string minLength: 1 expires_at: type: string format: date-time LobbyInviteDetail: type: object additionalProperties: false required: - invite_id - game_id - inviter_user_id - status - race_name - created_at - expires_at properties: invite_id: type: string format: uuid game_id: type: string format: uuid inviter_user_id: type: string format: uuid invited_user_id: type: string format: uuid nullable: true code: type: string nullable: true race_name: type: string status: type: string enum: [pending, redeemed, declined, revoked, expired] created_at: type: string format: date-time expires_at: type: string format: date-time decided_at: type: string format: date-time nullable: true LobbyInviteList: type: object additionalProperties: false required: [items] properties: items: type: array items: $ref: "#/components/schemas/LobbyInviteDetail" LobbyMembershipDetail: type: object additionalProperties: false required: - membership_id - game_id - user_id - race_name - canonical_key - status - joined_at properties: membership_id: type: string format: uuid game_id: type: string format: uuid user_id: type: string format: uuid race_name: type: string canonical_key: type: string status: type: string enum: [active, removed, blocked] joined_at: type: string format: date-time removed_at: type: string format: date-time nullable: true LobbyMembershipList: type: object additionalProperties: false required: [items] properties: items: type: array items: $ref: "#/components/schemas/LobbyMembershipDetail" RaceNameDetail: type: object additionalProperties: false required: [name, canonical, status, owner_user_id] properties: name: type: string canonical: type: string status: type: string enum: [registered, reservation, pending_registration] owner_user_id: type: string format: uuid game_id: type: string format: uuid nullable: true source_game_id: type: string format: uuid nullable: true reserved_at: type: string format: date-time nullable: true expires_at: type: string format: date-time nullable: true registered_at: type: string format: date-time nullable: true RaceNameList: type: object additionalProperties: false required: [items] properties: items: type: array items: $ref: "#/components/schemas/RaceNameDetail" RaceNameRegisterRequest: type: object additionalProperties: false required: [name] properties: name: type: string EngineCommand: type: object additionalProperties: true description: | Engine command request body. The schema is permissive because the engine proxy passes the body through verbatim; the typed shape lives in `pkg/model/rest.Command` and is enforced by `internal/engineclient` before the engine call leaves backend. EngineOrder: type: object additionalProperties: true description: | Engine order request body. Permissive on the wire; typed shape lives in `pkg/model/order.Order`. PassthroughObject: type: object additionalProperties: true description: | Permissive placeholder used for engine pass-through responses (`pkg/model/{rest,report}` types are the authoritative shape). AdminAccount: type: object additionalProperties: false required: [username, created_at] properties: username: type: string created_at: type: string format: date-time last_used_at: type: string format: date-time nullable: true disabled_at: type: string format: date-time nullable: true AdminAccountList: type: object additionalProperties: false required: [items] properties: items: type: array items: $ref: "#/components/schemas/AdminAccount" AdminAccountCreateRequest: type: object additionalProperties: false required: [username, password] properties: username: type: string password: type: string format: password AdminAccountResetPasswordRequest: type: object additionalProperties: false required: [password] properties: password: type: string format: password AdminUserList: type: object additionalProperties: false required: [items, page, page_size, total] properties: items: type: array items: $ref: "#/components/schemas/Account" page: type: integer page_size: type: integer total: type: integer AdminUserSanctionRequest: type: object additionalProperties: false required: [sanction_code, scope, reason_code, actor] properties: sanction_code: type: string enum: [permanent_block] description: | Closed MVP set; only `permanent_block` is supported. Applying it triggers the in-process cascade (revoke all sessions, release lobby memberships and Race Name Directory entries). scope: type: string reason_code: type: string actor: $ref: "#/components/schemas/ActorRef" expires_at: type: string format: date-time nullable: true AdminUserLimitRequest: type: object additionalProperties: false required: [limit_code, value, reason_code, actor] properties: limit_code: type: string value: type: integer reason_code: type: string actor: $ref: "#/components/schemas/ActorRef" expires_at: type: string format: date-time nullable: true AdminUserEntitlementRequest: type: object additionalProperties: false required: [tier, source, actor] properties: tier: type: string enum: [free, monthly, yearly, permanent] source: type: string actor: $ref: "#/components/schemas/ActorRef" reason_code: type: string starts_at: type: string format: date-time ends_at: type: string format: date-time nullable: true AdminGameList: type: object additionalProperties: false required: [items, page, page_size, total] properties: items: type: array items: $ref: "#/components/schemas/LobbyGameDetail" page: type: integer page_size: type: integer total: type: integer AdminGameBanMemberRequest: type: object additionalProperties: false required: [user_id, reason] properties: user_id: type: string format: uuid reason: type: string minLength: 1 RuntimeRecord: type: object additionalProperties: true required: [game_id, status] properties: game_id: type: string format: uuid status: type: string current_container_id: type: string image_ref: type: string started_at: type: string format: date-time nullable: true last_observed_at: type: string format: date-time nullable: true RuntimeOperation: type: object additionalProperties: true required: [operation_id, game_id, op, status, started_at] properties: operation_id: type: string format: uuid game_id: type: string format: uuid op: type: string status: type: string started_at: type: string format: date-time finished_at: type: string format: date-time nullable: true error: type: string RuntimePatchRequest: type: object additionalProperties: false required: [target_version] properties: target_version: type: string description: Semver-patch target inside the same major/minor line. EngineVersion: type: object additionalProperties: false required: [version, image_ref, enabled, created_at] properties: version: type: string image_ref: type: string enabled: type: boolean created_at: type: string format: date-time EngineVersionList: type: object additionalProperties: false required: [items] properties: items: type: array items: $ref: "#/components/schemas/EngineVersion" EngineVersionCreateRequest: type: object additionalProperties: false required: [version, image_ref] properties: version: type: string image_ref: type: string enabled: type: boolean EngineVersionUpdateRequest: type: object additionalProperties: false properties: image_ref: type: string enabled: type: boolean MailDelivery: type: object additionalProperties: true required: [delivery_id, template_id, status, attempts, created_at] properties: delivery_id: type: string format: uuid template_id: type: string idempotency_key: type: string status: type: string attempts: type: integer next_attempt_at: type: string format: date-time nullable: true created_at: type: string format: date-time MailDeliveryList: type: object additionalProperties: false required: [items, page, page_size, total] properties: items: type: array items: $ref: "#/components/schemas/MailDelivery" page: type: integer page_size: type: integer total: type: integer MailAttempt: type: object additionalProperties: true required: [attempt_id, delivery_id, attempt_no, started_at] properties: attempt_id: type: string format: uuid delivery_id: type: string format: uuid attempt_no: type: integer started_at: type: string format: date-time finished_at: type: string format: date-time nullable: true outcome: type: string error: type: string MailAttemptList: type: object additionalProperties: false required: [items] properties: items: type: array items: $ref: "#/components/schemas/MailAttempt" MailDeadLetter: type: object additionalProperties: true required: [dead_letter_id, delivery_id, archived_at] properties: dead_letter_id: type: string format: uuid delivery_id: type: string format: uuid archived_at: type: string format: date-time reason: type: string MailDeadLetterList: type: object additionalProperties: false required: [items, page, page_size, total] properties: items: type: array items: $ref: "#/components/schemas/MailDeadLetter" page: type: integer page_size: type: integer total: type: integer NotificationDetail: type: object additionalProperties: true required: [notification_id, kind, idempotency_key, created_at] properties: notification_id: type: string format: uuid kind: type: string idempotency_key: type: string user_id: type: string format: uuid payload: type: object additionalProperties: true created_at: type: string format: date-time NotificationList: type: object additionalProperties: false required: [items, page, page_size, total] properties: items: type: array items: $ref: "#/components/schemas/NotificationDetail" page: type: integer page_size: type: integer total: type: integer NotificationDeadLetter: type: object additionalProperties: true required: [dead_letter_id, notification_id, archived_at] properties: dead_letter_id: type: string format: uuid notification_id: type: string format: uuid archived_at: type: string format: date-time reason: type: string NotificationDeadLetterList: type: object additionalProperties: false required: [items, page, page_size, total] properties: items: type: array items: $ref: "#/components/schemas/NotificationDeadLetter" page: type: integer page_size: type: integer total: type: integer NotificationMalformed: type: object additionalProperties: true required: [id, received_at] properties: id: type: string format: uuid received_at: type: string format: date-time payload: type: object additionalProperties: true reason: type: string NotificationMalformedList: type: object additionalProperties: false required: [items, page, page_size, total] properties: items: type: array items: $ref: "#/components/schemas/NotificationMalformed" page: type: integer page_size: type: integer total: type: integer GeoCountryCounter: type: object additionalProperties: false required: [country, count] properties: country: type: string description: ISO 3166-1 alpha-2 country code. count: type: integer last_seen_at: type: string format: date-time nullable: true GeoCountryCounterList: type: object additionalProperties: false required: [user_id, items] properties: user_id: type: string format: uuid items: type: array items: $ref: "#/components/schemas/GeoCountryCounter" DeviceSession: type: object additionalProperties: false required: [device_session_id, user_id, status, created_at] properties: device_session_id: type: string format: uuid user_id: type: string format: uuid status: type: string client_public_key: type: string description: Standard base64-encoded raw 32-byte Ed25519 public key. created_at: type: string format: date-time revoked_at: type: string format: date-time nullable: true last_seen_at: type: string format: date-time nullable: true DeviceSessionRevocationSummary: type: object additionalProperties: false required: [user_id, revoked_count] properties: user_id: type: string format: uuid revoked_count: type: integer UserSessionList: type: object additionalProperties: false required: [items] properties: items: type: array items: $ref: "#/components/schemas/DeviceSession" UserMailSendRequest: type: object additionalProperties: false required: [recipient_user_id, body] properties: recipient_user_id: type: string format: uuid subject: type: string description: | Optional subject. Empty string and missing field are treated the same. body: type: string description: Plain UTF-8 body. HTML is not parsed on the server. UserMailSendAdminRequest: type: object additionalProperties: false required: [target, body] properties: target: type: string enum: [user, all] recipient_user_id: type: string format: uuid description: | Required when `target="user"`. Identifies the recipient of the personal admin message; the recipient may be in any membership status (admin notifications can reach kicked players). recipients: type: string enum: [active, active_and_removed, all_members] description: | Optional scope when `target="all"`. Defaults to `active`. subject: type: string body: type: string UserMailBroadcastReceipt: type: object additionalProperties: false required: - message_id - game_id - kind - sender_kind - body - body_lang - broadcast_scope - created_at - recipient_count properties: message_id: type: string format: uuid game_id: type: string format: uuid game_name: type: string kind: type: string enum: [personal, admin] sender_kind: type: string enum: [player, admin, system] subject: type: string body: type: string body_lang: type: string broadcast_scope: type: string enum: [single, game_broadcast, multi_game_broadcast] created_at: type: string format: date-time recipient_count: type: integer minimum: 0 UserMailSendBroadcastRequest: type: object additionalProperties: false required: [body] properties: subject: type: string body: type: string AdminDiplomailBroadcastRequest: type: object additionalProperties: false required: [scope, body] properties: scope: type: string enum: [selected, all_running] game_ids: type: array items: type: string format: uuid recipients: type: string enum: [active, active_and_removed, all_members] subject: type: string body: type: string AdminDiplomailBroadcastResponse: type: object additionalProperties: false required: [recipient_count, messages] properties: recipient_count: type: integer minimum: 0 messages: type: array items: type: object additionalProperties: false required: [message_id, game_id] properties: message_id: type: string format: uuid game_id: type: string format: uuid game_name: type: string AdminDiplomailCleanupRequest: type: object additionalProperties: false required: [older_than_years] properties: older_than_years: type: integer minimum: 1 AdminDiplomailCleanupResponse: type: object additionalProperties: false required: [messages_deleted, game_ids] properties: messages_deleted: type: integer minimum: 0 game_ids: type: array items: type: string format: uuid AdminDiplomailMessage: type: object additionalProperties: false required: - message_id - game_id - kind - sender_kind - body - body_lang - broadcast_scope - created_at properties: message_id: type: string format: uuid game_id: type: string format: uuid game_name: type: string kind: type: string enum: [personal, admin] sender_kind: type: string enum: [player, admin, system] sender_user_id: type: string format: uuid nullable: true sender_username: type: string nullable: true sender_ip: type: string subject: type: string body: type: string body_lang: type: string broadcast_scope: type: string enum: [single, game_broadcast, multi_game_broadcast] created_at: type: string format: date-time AdminDiplomailListResponse: type: object additionalProperties: false required: [total, page, page_size, items] properties: total: type: integer minimum: 0 page: type: integer minimum: 1 page_size: type: integer minimum: 1 items: type: array items: $ref: "#/components/schemas/AdminDiplomailMessage" UserMailMessageDetail: type: object additionalProperties: false required: - message_id - game_id - kind - sender_kind - body - body_lang - broadcast_scope - created_at - recipient_user_id properties: message_id: type: string format: uuid game_id: type: string format: uuid game_name: type: string kind: type: string enum: [personal, admin] sender_kind: type: string enum: [player, admin, system] sender_user_id: type: string format: uuid nullable: true sender_username: type: string nullable: true subject: type: string body: type: string body_lang: type: string description: BCP 47 tag. `und` until Stage D adds detection. broadcast_scope: type: string enum: [single, game_broadcast, multi_game_broadcast] created_at: type: string format: date-time recipient_user_id: type: string format: uuid recipient_user_name: type: string recipient_race_name: type: string nullable: true read_at: type: string format: date-time nullable: true deleted_at: type: string format: date-time nullable: true UserMailSentSummary: type: object additionalProperties: false required: - message_id - game_id - kind - body - body_lang - broadcast_scope - created_at properties: message_id: type: string format: uuid game_id: type: string format: uuid game_name: type: string kind: type: string enum: [personal, admin] subject: type: string body: type: string body_lang: type: string broadcast_scope: type: string enum: [single, game_broadcast, multi_game_broadcast] created_at: type: string format: date-time UserMailInboxList: type: object additionalProperties: false required: [items] properties: items: type: array items: $ref: "#/components/schemas/UserMailMessageDetail" UserMailSentList: type: object additionalProperties: false required: [items] properties: items: type: array items: $ref: "#/components/schemas/UserMailSentSummary" UserMailUnreadCount: type: object additionalProperties: false required: [game_id, unread] properties: game_id: type: string format: uuid game_name: type: string unread: type: integer minimum: 0 UserMailUnreadCountsResponse: type: object additionalProperties: false required: [total, items] properties: total: type: integer minimum: 0 items: type: array items: $ref: "#/components/schemas/UserMailUnreadCount" UserMailRecipientState: type: object additionalProperties: false required: [message_id] properties: message_id: type: string format: uuid read_at: type: string format: date-time nullable: true deleted_at: type: string format: date-time nullable: true responses: NotImplementedError: description: Endpoint is documented but not implemented yet. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: placeholder: value: error: code: not_implemented message: endpoint is not implemented yet InvalidRequestError: description: Request body or field values are invalid. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: invalidRequest: value: error: code: invalid_request message: request payload is invalid UnauthorizedError: description: Basic authentication credentials are missing or rejected. headers: WWW-Authenticate: description: Basic challenge advertised on rejected admin requests. schema: type: string example: Basic realm="galaxy-admin" content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: unauthorized: value: error: code: unauthorized message: basic authentication is required ForbiddenError: description: Caller is authenticated but not allowed to perform the action. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: forbidden: value: error: code: forbidden message: caller is not authorized for this action NotFoundError: description: The requested resource was not found. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: notFound: value: error: code: not_found message: resource was not found ConflictError: description: The request conflicts with the current state of the target resource. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: conflict: value: error: code: conflict message: resource already exists MethodNotAllowedError: description: Request method is not allowed for the target route. headers: Allow: description: Comma-separated list of accepted methods. schema: type: string example: GET content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: methodNotAllowed: value: error: code: method_not_allowed message: request method is not allowed for this route InternalError: description: Internal backend error while processing the request. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: internalError: value: error: code: internal_error message: internal server error ServiceUnavailableError: description: Backend is starting up or temporarily cannot serve the request. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: unavailable: value: error: code: service_unavailable message: backend is not ready