Files
galaxy-game/lobby/api/public-openapi.yaml
T
2026-04-25 23:20:55 +02:00

1866 lines
62 KiB
YAML

openapi: 3.0.3
info:
title: Galaxy Game Lobby Service Public REST API
version: v1
description: |
This specification documents the public authenticated REST contract of
`galaxy/lobby` served on `LOBBY_PUBLIC_HTTP_ADDR` (default `:8094`).
This port is reached exclusively through `Edge Gateway`. Gateway verifies
the authenticated session and injects the `X-User-ID` header before
forwarding every request. `Lobby` derives the acting user identity from
`X-User-ID` only and must never accept identity claims from request bodies.
Scope:
- game lifecycle management (create, update, get, list)
- enrollment management (open, close, ready-to-start)
- start lifecycle (start, pause, resume, cancel, retry-start)
- application flow for public games
- invite flow for private games
- membership operations
- user-facing lists (my games, my applications, my invitations)
This specification intentionally does not describe:
- the internal trusted REST contract (see `api/internal-openapi.yaml`)
- Redis Stream event contracts (see `README.md`)
- notification intent contracts (see `../notification/README.md`)
Transport rules:
- request bodies are strict JSON only; unknown fields are rejected
- all authenticated routes require `X-User-ID` injected by `Edge Gateway`
- error responses use `{ "error": { "code", "message" } }`
- stable error codes are `invalid_request`, `conflict`, `subject_not_found`,
`eligibility_denied`, `name_taken`,
`race_name_pending_window_expired`,
`race_name_registration_quota_exceeded`, `forbidden`,
`internal_error`, and `service_unavailable`
- `eligibility_denied`, `name_taken`,
`race_name_pending_window_expired`, and
`race_name_registration_quota_exceeded` are returned as `422`
servers:
- url: http://localhost:8094
description: Default local public listener for Game Lobby Service.
tags:
- name: Games
description: Game record CRUD and lifecycle queries.
- name: Enrollment
description: Enrollment management commands.
- name: Lifecycle
description: Start, pause, resume, cancel, and retry-start commands.
- name: Applications
description: Application flow for public games.
- name: Invites
description: Invite flow for private games.
- name: Memberships
description: Membership roster operations.
- name: MyLists
description: Authenticated-user personal list queries.
- name: RaceNames
description: Race Name Directory user-facing operations.
- name: Probes
description: Health and readiness probes.
paths:
/healthz:
get:
tags:
- Probes
operationId: publicHealthz
summary: Public listener health probe
responses:
"200":
description: Service is alive.
content:
application/json:
schema:
$ref: "#/components/schemas/ProbeResponse"
examples:
ok:
value:
status: ok
/readyz:
get:
tags:
- Probes
operationId: publicReadyz
summary: Public listener readiness probe
responses:
"200":
description: Service is ready to serve traffic.
content:
application/json:
schema:
$ref: "#/components/schemas/ProbeResponse"
examples:
ready:
value:
status: ready
/api/v1/lobby/games:
post:
tags:
- Games
operationId: createGame
summary: Create a new game record in draft status
description: |
Creates a new game record in `draft` status.
Authorization:
- `game_type=public`: requires system-admin role enforced upstream by
`Admin Service`; public games created on the internal port only in
normal operation
- `game_type=private`: requires the acting user's eligibility snapshot
from `User Service` to carry `can_create_private_game=true`
parameters:
- $ref: "#/components/parameters/XUserID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateGameRequest"
responses:
"201":
description: Game record created in draft status.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"400":
$ref: "#/components/responses/InvalidRequestError"
"403":
$ref: "#/components/responses/ForbiddenError"
"422":
$ref: "#/components/responses/DomainPreconditionError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
get:
tags:
- Games
operationId: listGames
summary: List public games with deterministic pagination
description: |
Returns a paginated list of public games with status in
`enrollment_open`, `ready_to_start`, `running`, or `finished`.
Games in `draft` or `cancelled` status are excluded from the public
list. Authenticated users also see private games where they hold an
active membership.
Default order: `enrollment_open` and `ready_to_start` first, then
`running`, then `finished` (most recent first within each group).
parameters:
- $ref: "#/components/parameters/XUserID"
- $ref: "#/components/parameters/PageSize"
- $ref: "#/components/parameters/PageToken"
responses:
"200":
description: One deterministic page of game summaries.
content:
application/json:
schema:
$ref: "#/components/schemas/GameListResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}:
get:
tags:
- Games
operationId: getGame
summary: Get one game record
description: |
Returns the full game record for the requested `game_id`.
Visibility rules:
- private `draft` games: visible only to the owner
- private non-draft games: visible to the owner and users with an
active membership or a non-expired invite
- public `draft` games: visible only to system administrators
- public non-draft games: visible to any authenticated user
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Full game record.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
patch:
tags:
- Games
operationId: updateGame
summary: Update mutable fields of a game record
description: |
Partially updates a game record.
Only fields present in the request body are modified; absent fields
retain their current values.
Editable in `draft` status: all fields in the request schema.
Editable in `enrollment_open` status: `description` only.
All fields are immutable in all other statuses.
Authorization: system administrator or private-game owner.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/XUserID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateGameRequest"
responses:
"200":
description: Updated game record.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"400":
$ref: "#/components/responses/InvalidRequestError"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/open-enrollment:
post:
tags:
- Enrollment
operationId: openEnrollment
summary: Transition a draft game to enrollment_open
description: |
Transitions the game from `draft` to `enrollment_open`.
Authorization: system administrator or private-game owner.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Updated game record with status enrollment_open.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/ready-to-start:
post:
tags:
- Enrollment
operationId: manualReadyToStart
summary: Manually close enrollment and transition to ready_to_start
description: |
Manually closes enrollment and transitions the game from
`enrollment_open` to `ready_to_start`.
Pre-condition: `approved_count >= min_players`.
Side effects: all invites in `created` status are transitioned to
`expired`; `lobby.invite.expired` notification intents are published
for each expired invite.
Authorization: system administrator or private-game owner.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Updated game record with status ready_to_start.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/start:
post:
tags:
- Lifecycle
operationId: startGame
summary: Initiate the game start sequence
description: |
Transitions the game from `ready_to_start` to `starting` and publishes
a start job to `Runtime Manager`.
The final outcome (`running`, `paused`, or `start_failed`) is determined
asynchronously by the `Runtime Manager` result consumer.
Authorization: system administrator or private-game owner.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Updated game record with status starting.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/pause:
post:
tags:
- Lifecycle
operationId: pauseGame
summary: Apply a platform-level pause to a running game
description: |
Transitions the game from `running` to `paused`.
This is a platform-level pause distinct from `Game Master` runtime
failure states. The engine container may remain alive.
Authorization: system administrator or private-game owner.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Updated game record with status paused.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/resume:
post:
tags:
- Lifecycle
operationId: resumeGame
summary: Resume a paused game
description: |
Transitions the game from `paused` to `running`.
A synchronous `Game Master` liveness check is performed before the
transition. If `Game Master` is unreachable, the game remains `paused`
and `503 service_unavailable` is returned.
Authorization: system administrator or private-game owner.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Updated game record with status running.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/cancel:
post:
tags:
- Lifecycle
operationId: cancelGame
summary: Cancel a game that has not yet started running
description: |
Cancels the game. Allowed source statuses: `draft`, `enrollment_open`,
`ready_to_start`, `start_failed`. Not allowed from `starting`,
`running`, or `paused`.
Authorization: system administrator or private-game owner.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Updated game record with status cancelled.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/retry-start:
post:
tags:
- Lifecycle
operationId: retryStart
summary: Retry a failed start attempt
description: |
Transitions the game from `start_failed` back to `ready_to_start`,
enabling a new start attempt.
Authorization: system administrator or private-game owner.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Updated game record with status ready_to_start.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/applications:
post:
tags:
- Applications
operationId: submitApplication
summary: Submit a join application for a public game
description: |
Creates a new application in `submitted` status for a public game.
Pre-conditions checked synchronously:
- game status is `enrollment_open` and game type is `public`
- acting user has no existing non-rejected application to the same game
- `User Service` eligibility confirms `can_join_game=true`
- roster capacity allows additional applicants
- Race Name Directory confirms `race_name` is available for the acting user
On success, `lobby.application.submitted` notification intent is
published to the configured admin email list.
Authorization: any authenticated user.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/XUserID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SubmitApplicationRequest"
responses:
"201":
description: Application created in submitted status.
content:
application/json:
schema:
$ref: "#/components/schemas/ApplicationRecord"
"400":
$ref: "#/components/responses/InvalidRequestError"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"422":
$ref: "#/components/responses/DomainPreconditionError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/applications/{application_id}/approve:
post:
tags:
- Applications
operationId: approveApplication
summary: Approve a submitted application
description: |
Approves a submitted application, reserves the race name, and creates
an active membership for the applicant.
Pre-conditions: game is `enrollment_open`; application is `submitted`;
roster capacity allows additional approved participants.
On success, `lobby.membership.approved` notification intent is published
to the applicant.
Authorization: system administrator.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/ApplicationIDPath"
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Active membership created for the approved applicant.
content:
application/json:
schema:
$ref: "#/components/schemas/MembershipRecord"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/applications/{application_id}/reject:
post:
tags:
- Applications
operationId: rejectApplication
summary: Reject a submitted application
description: |
Rejects a submitted application and releases any pending race name
reservation held for the applicant.
On success, `lobby.membership.rejected` notification intent is published
to the applicant.
Authorization: system administrator.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/ApplicationIDPath"
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Application record with status rejected.
content:
application/json:
schema:
$ref: "#/components/schemas/ApplicationRecord"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/invites:
post:
tags:
- Invites
operationId: createInvite
summary: Create an invite for a private game
description: |
Creates a new invite in `created` status for the specified invitee.
Pre-conditions: game is `enrollment_open` and `private`; the invitee
has no active invite or active membership in the game; roster capacity
allows additional participants.
`expires_at` is set to `enrollment_ends_at` of the game.
On success, `lobby.invite.created` notification intent is published
to the invitee.
Authorization: private-game owner.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/XUserID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateInviteRequest"
responses:
"201":
description: Invite record created in created status.
content:
application/json:
schema:
$ref: "#/components/schemas/InviteRecord"
"400":
$ref: "#/components/responses/InvalidRequestError"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/invites/{invite_id}/redeem:
post:
tags:
- Invites
operationId: redeemInvite
summary: Redeem an invite and join a private game
description: |
Redeems a `created` invite, reserves the chosen race name, and creates
an active membership immediately without a separate owner-approval step.
Pre-conditions: invite status is `created`; game is `enrollment_open`;
roster capacity allows additional participants; Race Name Directory
confirms `race_name` is available for the acting user.
On success, `lobby.invite.redeemed` notification intent is published
to the private-game owner.
Authorization: the invited user (invitee_user_id must match X-User-ID).
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/InviteIDPath"
- $ref: "#/components/parameters/XUserID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RedeemInviteRequest"
responses:
"200":
description: Active membership created for the redeeming user.
content:
application/json:
schema:
$ref: "#/components/schemas/MembershipRecord"
"400":
$ref: "#/components/responses/InvalidRequestError"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"422":
$ref: "#/components/responses/DomainPreconditionError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/invites/{invite_id}/decline:
post:
tags:
- Invites
operationId: declineInvite
summary: Decline a received invite
description: |
Transitions a `created` invite to `declined`. No notification is
published in v1.
Declined users may receive a new invite from the owner while enrollment
is open.
Authorization: the invited user (invitee_user_id must match X-User-ID).
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/InviteIDPath"
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Invite record with status declined.
content:
application/json:
schema:
$ref: "#/components/schemas/InviteRecord"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/invites/{invite_id}/revoke:
post:
tags:
- Invites
operationId: revokeInvite
summary: Revoke a sent invite
description: |
Transitions a `created` invite to `revoked`. No notification is
published in v1.
Authorization: private-game owner.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/InviteIDPath"
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Invite record with status revoked.
content:
application/json:
schema:
$ref: "#/components/schemas/InviteRecord"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/memberships:
get:
tags:
- Memberships
operationId: listMemberships
summary: List memberships of a game
description: |
Returns a paginated list of memberships for the game.
Authorization: system administrator, game owner, or active member.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/XUserID"
- $ref: "#/components/parameters/PageSize"
- $ref: "#/components/parameters/PageToken"
responses:
"200":
description: One deterministic page of membership records.
content:
application/json:
schema:
$ref: "#/components/schemas/MembershipListResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/memberships/{membership_id}/remove:
post:
tags:
- Memberships
operationId: removeMember
summary: Remove a member from a game
description: |
Removes an active member.
Before game start: drops the membership and releases the race name
reservation.
After game start: marks membership `removed`; `Game Master` must
deactivate the player slot; race name reservation is retained until
the game finishes.
Authorization: system administrator or private-game owner.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/MembershipIDPath"
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Updated membership record with status removed.
content:
application/json:
schema:
$ref: "#/components/schemas/MembershipRecord"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games/{game_id}/memberships/{membership_id}/block:
post:
tags:
- Memberships
operationId: blockMember
summary: Apply a platform-level block to a member
description: |
Blocks an active member. The engine slot is retained but the member
cannot send commands through `Game Master`. Race name reservation is
preserved.
Authorization: system administrator or private-game owner.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/MembershipIDPath"
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Updated membership record with status blocked.
content:
application/json:
schema:
$ref: "#/components/schemas/MembershipRecord"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/my/games:
get:
tags:
- MyLists
operationId: listMyGames
summary: List active games for the authenticated user
description: |
Returns games where the authenticated user holds an active membership
and the game status is `running` or `paused`. Response includes the
denormalized runtime snapshot for each game.
parameters:
- $ref: "#/components/parameters/XUserID"
- $ref: "#/components/parameters/PageSize"
- $ref: "#/components/parameters/PageToken"
responses:
"200":
description: One page of active game records including runtime snapshot.
content:
application/json:
schema:
$ref: "#/components/schemas/GameListResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/my/applications:
get:
tags:
- MyLists
operationId: listMyApplications
summary: List pending applications for the authenticated user
description: |
Returns applications submitted by the authenticated user with status
`submitted`. Each item includes game name and type for display.
parameters:
- $ref: "#/components/parameters/XUserID"
- $ref: "#/components/parameters/PageSize"
- $ref: "#/components/parameters/PageToken"
responses:
"200":
description: One page of submitted application items.
content:
application/json:
schema:
$ref: "#/components/schemas/MyApplicationListResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/my/invites:
get:
tags:
- MyLists
operationId: listMyInvites
summary: List open invites addressed to the authenticated user
description: |
Returns invites addressed to the authenticated user with status
`created`. Each item includes game name, inviter name, and `expires_at`.
parameters:
- $ref: "#/components/parameters/XUserID"
- $ref: "#/components/parameters/PageSize"
- $ref: "#/components/parameters/PageToken"
responses:
"200":
description: One page of open invite items.
content:
application/json:
schema:
$ref: "#/components/schemas/MyInviteListResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/my/race-names:
get:
tags:
- RaceNames
operationId: listMyRaceNames
summary: List the acting user's race-name directory entries
description: |
Returns the acting user's view of the Race Name Directory across
all three levels of binding: permanent registered names,
`pending_registration` entries waiting for the 30-day window to
elapse, and active per-game reservations. Each reservation
carries the current `game_status` of its hosting game so the UI
can render it next to the game state. The endpoint reads only
the `user_registered` and `user_reservations` indexes; it never
scans the full directory.
The response is exclusively scoped to the caller. There is no
`?user_id=` parameter; admin-side cross-user reads are not
exposed by this route.
parameters:
- $ref: "#/components/parameters/XUserID"
responses:
"200":
description: Snapshot of the acting user's race-name bindings.
content:
application/json:
schema:
$ref: "#/components/schemas/MyRaceNamesResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"403":
$ref: "#/components/responses/ForbiddenError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/race-names/register:
post:
tags:
- RaceNames
operationId: registerRaceName
summary: Convert a pending race-name registration into a permanent one
description: |
Converts the caller's `pending_registration` for
`(source_game_id, race_name)` into a permanent registered race
name. The pending entry must still be inside its 30-day window,
the caller must not carry an active `permanent_block`, and the
caller's `max_registered_race_names` allowance from the User
Service eligibility snapshot must permit the new registration
(a value of `0` denotes the unlimited lifetime tariff).
The call is idempotent: a repeated request with the same body
returns the previously registered record without consuming any
additional quota slot. The notification intent
`lobby.race_name.registered` is emitted on every successful
return; consumers deduplicate using the stable idempotency key
`lobby.race_name.registered:<source_game_id>:<user_id>`.
parameters:
- $ref: "#/components/parameters/XUserID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RegisterRaceNameRequest"
responses:
"200":
description: Race name successfully registered.
content:
application/json:
schema:
$ref: "#/components/schemas/RegisteredRaceName"
"400":
$ref: "#/components/responses/InvalidRequestError"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError"
"422":
$ref: "#/components/responses/DomainPreconditionError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
components:
parameters:
XUserID:
name: X-User-ID
in: header
required: true
description: |
Authenticated platform user identifier injected by `Edge Gateway`.
`Lobby` derives the acting user identity exclusively from this header.
schema:
type: string
GameIDPath:
name: game_id
in: path
required: true
description: Opaque stable game identifier.
schema:
type: string
ApplicationIDPath:
name: application_id
in: path
required: true
description: Opaque stable application identifier.
schema:
type: string
InviteIDPath:
name: invite_id
in: path
required: true
description: Opaque stable invite identifier.
schema:
type: string
MembershipIDPath:
name: membership_id
in: path
required: true
description: Opaque stable membership identifier.
schema:
type: string
PageSize:
name: page_size
in: query
required: false
description: |
Maximum number of items to return. Default is `50`; maximum is `200`.
schema:
type: integer
minimum: 1
maximum: 200
default: 50
PageToken:
name: page_token
in: query
required: false
description: Opaque continuation token returned as `next_page_token` in a previous response.
schema:
type: string
schemas:
GameRecord:
type: object
additionalProperties: false
required:
- game_id
- game_name
- game_type
- owner_user_id
- status
- min_players
- max_players
- start_gap_hours
- start_gap_players
- enrollment_ends_at
- turn_schedule
- target_engine_version
- created_at
- updated_at
- current_turn
- runtime_status
- engine_health_summary
properties:
game_id:
type: string
description: Opaque stable game identifier in game-* form.
game_name:
type: string
description: Human-readable game name; mutable in draft status.
description:
type: string
description: Optional game description; mutable in draft and enrollment_open.
game_type:
type: string
enum:
- public
- private
description: Game visibility and enrollment model.
owner_user_id:
type: string
description: Platform user identifier of the private-game owner; empty for public games.
status:
type: string
enum:
- draft
- enrollment_open
- ready_to_start
- starting
- start_failed
- running
- paused
- finished
- cancelled
description: Current platform-level lifecycle status.
min_players:
type: integer
description: Minimum approved participants required to proceed to start.
max_players:
type: integer
description: Target roster size that activates the gap window.
start_gap_hours:
type: integer
description: Hours of gap window after max_players is reached.
start_gap_players:
type: integer
description: Additional participants admitted during the gap window.
enrollment_ends_at:
type: integer
format: int64
description: UTC Unix seconds; deadline for automatic enrollment close.
turn_schedule:
type: string
description: Five-field cron expression for scheduled turn generation; passed to Game Master at registration.
target_engine_version:
type: string
description: Semver of the game engine to launch; passed to Game Master at registration.
created_at:
type: integer
format: int64
description: UTC Unix milliseconds; record creation timestamp.
updated_at:
type: integer
format: int64
description: UTC Unix milliseconds; last mutation timestamp.
started_at:
type: integer
format: int64
description: UTC Unix milliseconds; set when status becomes running.
finished_at:
type: integer
format: int64
description: UTC Unix milliseconds; set when status becomes finished.
current_turn:
type: integer
description: Denormalized from Game Master; zero until the game is running.
runtime_status:
type: string
description: Denormalized from Game Master; empty until the game is running.
engine_health_summary:
type: string
description: Denormalized from Game Master; empty until the game is running.
runtime_binding:
$ref: "#/components/schemas/RuntimeBinding"
RuntimeBinding:
type: object
additionalProperties: false
description: |
Runtime binding metadata persisted on the game record after a
successful container start. Absent before the start sequence
completes.
required:
- container_id
- engine_endpoint
- runtime_job_id
- bound_at
properties:
container_id:
type: string
description: Engine container identifier assigned by Runtime Manager.
engine_endpoint:
type: string
description: Network address Game Master uses to reach the engine container.
runtime_job_id:
type: string
description: |
Source `runtime:job_results` Redis Stream message id (in `<ms>-<seq>`
form) that produced this binding.
bound_at:
type: integer
format: int64
description: UTC Unix milliseconds when the binding was persisted.
ApplicationRecord:
type: object
additionalProperties: false
required:
- application_id
- game_id
- applicant_user_id
- race_name
- status
- created_at
properties:
application_id:
type: string
description: Opaque stable application identifier.
game_id:
type: string
description: Identifier of the game this application belongs to.
applicant_user_id:
type: string
description: Platform user identifier of the applicant.
race_name:
type: string
description: Desired in-game name submitted with the application.
status:
type: string
enum:
- submitted
- approved
- rejected
description: Current application lifecycle status.
created_at:
type: integer
format: int64
description: UTC Unix milliseconds; application submission timestamp.
decided_at:
type: integer
format: int64
description: UTC Unix milliseconds; set when application is approved or rejected.
InviteRecord:
type: object
additionalProperties: false
required:
- invite_id
- game_id
- inviter_user_id
- invitee_user_id
- status
- created_at
- expires_at
properties:
invite_id:
type: string
description: Opaque stable invite identifier.
game_id:
type: string
description: Identifier of the game this invite belongs to.
inviter_user_id:
type: string
description: Platform user identifier of the game owner who created the invite.
invitee_user_id:
type: string
description: Platform user identifier of the invited user.
race_name:
type: string
description: In-game name chosen by the invitee at redeem time; absent until the invite is redeemed.
status:
type: string
enum:
- created
- redeemed
- declined
- revoked
- expired
description: Current invite lifecycle status.
created_at:
type: integer
format: int64
description: UTC Unix milliseconds; invite creation timestamp.
expires_at:
type: integer
format: int64
description: UTC Unix milliseconds; equals enrollment_ends_at of the game at creation time.
decided_at:
type: integer
format: int64
description: UTC Unix milliseconds; set when invite is redeemed, declined, revoked, or expired.
MembershipRecord:
type: object
additionalProperties: false
required:
- membership_id
- game_id
- user_id
- race_name
- status
- joined_at
properties:
membership_id:
type: string
description: Opaque stable membership identifier.
game_id:
type: string
description: Identifier of the game this membership belongs to.
user_id:
type: string
description: Platform user identifier of the member.
race_name:
type: string
description: Confirmed in-game name; reserved in Race Name Directory.
status:
type: string
enum:
- active
- removed
- blocked
description: Current membership status.
joined_at:
type: integer
format: int64
description: UTC Unix milliseconds; membership activation timestamp.
removed_at:
type: integer
format: int64
description: UTC Unix milliseconds; set when membership is removed or blocked.
MyApplicationItem:
type: object
additionalProperties: false
required:
- application_id
- game_id
- applicant_user_id
- race_name
- status
- created_at
- game_name
- game_type
properties:
application_id:
type: string
game_id:
type: string
applicant_user_id:
type: string
race_name:
type: string
status:
type: string
enum:
- submitted
- approved
- rejected
created_at:
type: integer
format: int64
decided_at:
type: integer
format: int64
game_name:
type: string
description: Human-readable game name for display purposes.
game_type:
type: string
enum:
- public
- private
description: Game type for display purposes.
MyInviteItem:
type: object
additionalProperties: false
required:
- invite_id
- game_id
- inviter_user_id
- invitee_user_id
- status
- created_at
- expires_at
- game_name
- inviter_name
properties:
invite_id:
type: string
game_id:
type: string
inviter_user_id:
type: string
invitee_user_id:
type: string
race_name:
type: string
status:
type: string
enum:
- created
- redeemed
- declined
- revoked
- expired
created_at:
type: integer
format: int64
expires_at:
type: integer
format: int64
decided_at:
type: integer
format: int64
game_name:
type: string
description: Human-readable game name for display purposes.
inviter_name:
type: string
description: Owner's race name if already a member of the game; otherwise the owner's user_id.
CreateGameRequest:
type: object
additionalProperties: false
required:
- game_name
- game_type
- min_players
- max_players
- start_gap_hours
- start_gap_players
- enrollment_ends_at
- turn_schedule
- target_engine_version
properties:
game_name:
type: string
description: Human-readable game name; must be non-empty after trim.
description:
type: string
description: Optional game description.
game_type:
type: string
enum:
- public
- private
description: Game visibility and enrollment model.
min_players:
type: integer
minimum: 1
description: Minimum approved participants required to proceed to start; must be <= max_players.
max_players:
type: integer
minimum: 1
description: Target roster size that activates the gap window; must be >= min_players.
start_gap_hours:
type: integer
minimum: 0
description: Hours of gap window after max_players is reached.
start_gap_players:
type: integer
minimum: 0
description: Additional participants admitted during the gap window.
enrollment_ends_at:
type: integer
format: int64
description: UTC Unix seconds; deadline for automatic enrollment close; must be a positive integer.
turn_schedule:
type: string
description: Valid five-field cron expression for scheduled turn generation.
target_engine_version:
type: string
description: Non-empty semver string of the game engine to launch.
UpdateGameRequest:
type: object
additionalProperties: false
description: |
Partial update of a game record. Only fields present in the request body
are modified. `game_name`, `min_players`, `max_players`,
`start_gap_hours`, `start_gap_players`, `enrollment_ends_at`,
`turn_schedule`, and `target_engine_version` are mutable in `draft`
status only. `description` is additionally mutable in `enrollment_open`
status.
properties:
game_name:
type: string
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: integer
format: int64
turn_schedule:
type: string
target_engine_version:
type: string
SubmitApplicationRequest:
type: object
additionalProperties: false
required:
- race_name
properties:
race_name:
type: string
description: Desired in-game name; must be available in the Race Name Directory.
CreateInviteRequest:
type: object
additionalProperties: false
required:
- invitee_user_id
properties:
invitee_user_id:
type: string
description: Platform user identifier of the user to invite.
RedeemInviteRequest:
type: object
additionalProperties: false
required:
- race_name
properties:
race_name:
type: string
description: Desired in-game name; must be available in the Race Name Directory.
RegisterRaceNameRequest:
type: object
additionalProperties: false
required:
- race_name
- source_game_id
properties:
race_name:
type: string
description: |
Original-casing race name to register. Must match the
canonical key of an existing `pending_registration` owned by
the caller in `source_game_id`.
source_game_id:
type: string
description: |
Identifier of the finished game whose capable finish
produced the pending registration to convert.
RegisteredRaceName:
type: object
additionalProperties: false
required:
- canonical_key
- race_name
- source_game_id
- registered_at_ms
properties:
canonical_key:
type: string
description: |
Race Name Directory canonical key derived from the policy
(lowercase + frozen confusable-pair map).
race_name:
type: string
description: Original-casing display value owned by the caller.
source_game_id:
type: string
description: |
Game whose capable finish produced the pending registration
converted by this call.
registered_at_ms:
type: integer
format: int64
description: |
UTC Unix milliseconds timestamp recorded by the directory
on the original commit. Idempotent retries return the same
value.
PendingRaceName:
type: object
additionalProperties: false
required:
- canonical_key
- race_name
- source_game_id
- eligible_until_ms
properties:
canonical_key:
type: string
description: |
Race Name Directory canonical key derived from the policy
(lowercase + frozen confusable-pair map).
race_name:
type: string
description: Original-casing display value held by the caller.
source_game_id:
type: string
description: |
Game whose capable finish produced this pending entry.
Use this value as `source_game_id` when calling
`lobby.race_name.register`.
reserved_at_ms:
type: integer
format: int64
description: |
UTC Unix milliseconds timestamp of the original `Reserve`
call that became this pending entry.
eligible_until_ms:
type: integer
format: int64
description: |
UTC Unix milliseconds deadline for converting the pending
entry into a registered race name. After this moment the
pending-registration expiration worker releases it.
RaceNameReservation:
type: object
additionalProperties: false
required:
- canonical_key
- race_name
- game_id
- game_status
properties:
canonical_key:
type: string
description: |
Race Name Directory canonical key derived from the policy
(lowercase + frozen confusable-pair map).
race_name:
type: string
description: Original-casing display value held by the caller.
game_id:
type: string
description: Game hosting the reservation.
reserved_at_ms:
type: integer
format: int64
description: |
UTC Unix milliseconds timestamp of the `Reserve` call.
game_status:
type: string
description: |
Current `game.Status` of the hosting game. Empty when the
game record cannot be loaded (defensive only — this should
not occur in normal operation).
MyRaceNamesResponse:
type: object
additionalProperties: false
required:
- registered
- pending
- reservations
properties:
registered:
type: array
items:
$ref: "#/components/schemas/RegisteredRaceName"
pending:
type: array
items:
$ref: "#/components/schemas/PendingRaceName"
reservations:
type: array
items:
$ref: "#/components/schemas/RaceNameReservation"
GameListResponse:
type: object
additionalProperties: false
required:
- items
properties:
items:
type: array
items:
$ref: "#/components/schemas/GameRecord"
next_page_token:
type: string
description: Opaque continuation token; absent when no further pages exist.
MembershipListResponse:
type: object
additionalProperties: false
required:
- items
properties:
items:
type: array
items:
$ref: "#/components/schemas/MembershipRecord"
next_page_token:
type: string
description: Opaque continuation token; absent when no further pages exist.
MyApplicationListResponse:
type: object
additionalProperties: false
required:
- items
properties:
items:
type: array
items:
$ref: "#/components/schemas/MyApplicationItem"
next_page_token:
type: string
description: Opaque continuation token; absent when no further pages exist.
MyInviteListResponse:
type: object
additionalProperties: false
required:
- items
properties:
items:
type: array
items:
$ref: "#/components/schemas/MyInviteItem"
next_page_token:
type: string
description: Opaque continuation token; absent when no further pages exist.
ProbeResponse:
type: object
additionalProperties: false
required:
- status
properties:
status:
type: string
description: Stable probe outcome string.
ErrorResponse:
type: object
additionalProperties: false
required:
- error
properties:
error:
$ref: "#/components/schemas/ErrorBody"
ErrorBody:
type: object
additionalProperties: false
required:
- code
- message
properties:
code:
type: string
description: Stable internal API error code.
message:
type: string
description: Human-readable trusted error message.
responses:
InvalidRequestError:
description: Request validation failed.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
invalidRequest:
value:
error:
code: invalid_request
message: request is invalid
ForbiddenError:
description: Caller is not authorized for this operation on this resource.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
forbidden:
value:
error:
code: forbidden
message: access denied
NotFoundError:
description: The requested game, application, invite, or membership does not exist.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
notFound:
value:
error:
code: subject_not_found
message: resource not found
ConflictError:
description: The requested state transition is not allowed from the current status.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
conflict:
value:
error:
code: conflict
message: operation not allowed in current status
DomainPreconditionError:
description: |
A domain-level precondition was not met. Stable codes returned under
this response:
- `eligibility_denied` — user not eligible per User Service
- `name_taken` — race_name is already reserved by another user
- `race_name_pending_window_expired` — the 30-day pending
registration window has lapsed
- `race_name_registration_quota_exceeded` — caller exhausted their
tariff `max_registered_race_names` allowance
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
eligibilityDenied:
value:
error:
code: eligibility_denied
message: user is not eligible to join games
nameTaken:
value:
error:
code: name_taken
message: race name is already taken
raceNamePendingWindowExpired:
value:
error:
code: race_name_pending_window_expired
message: pending race-name registration window has expired
raceNameRegistrationQuotaExceeded:
value:
error:
code: race_name_registration_quota_exceeded
message: race name registration quota exceeded
InternalError:
description: Unexpected internal service error.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
internal:
value:
error:
code: internal_error
message: internal server error
ServiceUnavailableError:
description: An upstream dependency is unavailable.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
unavailable:
value:
error:
code: service_unavailable
message: service is unavailable