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. 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}/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/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/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 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`, `not_found`, `conflict`, `method_not_allowed`, `internal_error`, `service_unavailable`. enum: - not_implemented - invalid_request - unauthorized - forbidden - not_found - conflict - method_not_allowed - internal_error - service_unavailable 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" 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