feat: game lobby service

This commit is contained in:
Ilia Denisov
2026-04-25 23:20:55 +02:00
committed by GitHub
parent 32dc29359a
commit 48b0056b49
336 changed files with 57074 additions and 1418 deletions
+946
View File
@@ -0,0 +1,946 @@
openapi: 3.0.3
info:
title: Galaxy Game Lobby Service Internal REST API
version: v1
description: |
This specification documents the internal trusted REST contract of
`galaxy/lobby` served on `LOBBY_INTERNAL_HTTP_ADDR` (default `:8095`).
This port is not reachable from the public internet. Two caller classes
use it:
**Game Master integration paths** (`/api/v1/internal/…`):
- `GET /api/v1/internal/games/{game_id}` — game detail read for
`Game Master` and internal tooling
- `GET /api/v1/internal/games/{game_id}/memberships` — full membership
list for `Game Master` authorization checks
Note: Lobby calls Game Master synchronously after a successful
container start (outgoing). The `register-runtime` endpoint lives on
Game Master's surface, not on Lobby's. Lobby does not accept inbound
`register-runtime` requests.
**Admin Service paths** (same `/api/v1/lobby/…` paths as the public port):
- `Admin Service` enforces the system-admin role check at the gateway
boundary before calling these endpoints
- `X-User-ID` is NOT present on calls from `Admin Service`; Lobby treats
all callers on this port as trusted and performs no user-level auth
Transport rules:
- request bodies are strict JSON only; unknown fields are rejected
- error responses use `{ "error": { "code", "message" } }`
- stable error codes match the public contract: `invalid_request`,
`conflict`, `subject_not_found`, `forbidden`, `internal_error`,
and `service_unavailable`
servers:
- url: http://localhost:8095
description: Default local internal listener for Game Lobby Service.
tags:
- name: GMIntegration
description: Game Master integration paths for runtime binding and membership reads.
- name: AdminGames
description: Admin-mirrored game lifecycle paths called by Admin Service.
- name: AdminApplications
description: Admin-mirrored application approval paths called by Admin Service.
- name: AdminMemberships
description: Admin-mirrored membership operation paths called by Admin Service.
- name: Probes
description: Health and readiness probes.
paths:
/healthz:
get:
tags:
- Probes
operationId: internalHealthz
summary: Internal 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: internalReadyz
summary: Internal 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/internal/games/{game_id}:
get:
tags:
- GMIntegration
operationId: internalGetGame
summary: Get one game record for Game Master or internal tooling
description: |
Returns the full game record without visibility restrictions. Intended
for use by `Game Master` and internal administrative tooling.
parameters:
- $ref: "#/components/parameters/GameIDPath"
responses:
"200":
description: Full game record.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"404":
$ref: "#/components/responses/NotFoundError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/games/{game_id}/memberships:
get:
tags:
- GMIntegration
operationId: internalListMemberships
summary: List all memberships of a game for Game Master
description: |
Returns all memberships of the game without visibility restrictions.
Intended for `Game Master` authorization checks during command routing.
Pagination applies.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/PageSize"
- $ref: "#/components/parameters/PageToken"
responses:
"200":
description: One page of membership records.
content:
application/json:
schema:
$ref: "#/components/schemas/MembershipListResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/NotFoundError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/lobby/games:
post:
tags:
- AdminGames
operationId: adminCreateGame
summary: Create a new game record (admin)
description: |
Creates a new game record in `draft` status. Used by `Admin Service`
for public game creation. Lobby trusts the caller and does not enforce
a user-level eligibility check on this port.
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"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
get:
tags:
- AdminGames
operationId: adminListGames
summary: List games (admin, unrestricted)
description: |
Returns a paginated list of games without visibility restrictions.
Used by `Admin Service` for administrative oversight.
parameters:
- $ref: "#/components/parameters/PageSize"
- $ref: "#/components/parameters/PageToken"
responses:
"200":
description: One page of game records.
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:
- AdminGames
operationId: adminGetGame
summary: Get one game record (admin, unrestricted)
parameters:
- $ref: "#/components/parameters/GameIDPath"
responses:
"200":
description: Full game record without visibility restrictions.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"404":
$ref: "#/components/responses/NotFoundError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
patch:
tags:
- AdminGames
operationId: adminUpdateGame
summary: Update mutable fields of a game record (admin)
parameters:
- $ref: "#/components/parameters/GameIDPath"
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"
"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:
- AdminGames
operationId: adminOpenEnrollment
summary: Transition a draft game to enrollment_open (admin)
parameters:
- $ref: "#/components/parameters/GameIDPath"
responses:
"200":
description: Updated game record with status enrollment_open.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"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:
- AdminGames
operationId: adminManualReadyToStart
summary: Manually close enrollment and transition to ready_to_start (admin)
parameters:
- $ref: "#/components/parameters/GameIDPath"
responses:
"200":
description: Updated game record with status ready_to_start.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"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:
- AdminGames
operationId: adminStartGame
summary: Initiate the game start sequence (admin)
parameters:
- $ref: "#/components/parameters/GameIDPath"
responses:
"200":
description: Updated game record with status starting.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"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:
- AdminGames
operationId: adminPauseGame
summary: Apply a platform-level pause to a running game (admin)
parameters:
- $ref: "#/components/parameters/GameIDPath"
responses:
"200":
description: Updated game record with status paused.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"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:
- AdminGames
operationId: adminResumeGame
summary: Resume a paused game (admin)
parameters:
- $ref: "#/components/parameters/GameIDPath"
responses:
"200":
description: Updated game record with status running.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"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:
- AdminGames
operationId: adminCancelGame
summary: Cancel a game that has not yet started running (admin)
parameters:
- $ref: "#/components/parameters/GameIDPath"
responses:
"200":
description: Updated game record with status cancelled.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"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:
- AdminGames
operationId: adminRetryStart
summary: Retry a failed start attempt (admin)
parameters:
- $ref: "#/components/parameters/GameIDPath"
responses:
"200":
description: Updated game record with status ready_to_start.
content:
application/json:
schema:
$ref: "#/components/schemas/GameRecord"
"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}/approve:
post:
tags:
- AdminApplications
operationId: adminApproveApplication
summary: Approve a submitted application (admin)
description: |
Approves a submitted application, reserves the race name, and creates
an active membership. On success, `lobby.membership.approved`
notification intent is published to the applicant.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/ApplicationIDPath"
responses:
"200":
description: Active membership created for the approved applicant.
content:
application/json:
schema:
$ref: "#/components/schemas/MembershipRecord"
"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:
- AdminApplications
operationId: adminRejectApplication
summary: Reject a submitted application (admin)
description: |
Rejects a submitted application and releases any pending race name
reservation. On success, `lobby.membership.rejected` notification
intent is published to the applicant.
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/ApplicationIDPath"
responses:
"200":
description: Application record with status rejected.
content:
application/json:
schema:
$ref: "#/components/schemas/ApplicationRecord"
"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:
- AdminMemberships
operationId: adminListMemberships
summary: List memberships of a game (admin, unrestricted)
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/PageSize"
- $ref: "#/components/parameters/PageToken"
responses:
"200":
description: One page of membership records.
content:
application/json:
schema:
$ref: "#/components/schemas/MembershipListResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"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:
- AdminMemberships
operationId: adminRemoveMember
summary: Remove a member from a game (admin)
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/MembershipIDPath"
responses:
"200":
description: Updated membership record with status removed.
content:
application/json:
schema:
$ref: "#/components/schemas/MembershipRecord"
"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:
- AdminMemberships
operationId: adminBlockMember
summary: Apply a platform-level block to a member (admin)
parameters:
- $ref: "#/components/parameters/GameIDPath"
- $ref: "#/components/parameters/MembershipIDPath"
responses:
"200":
description: Updated membership record with status blocked.
content:
application/json:
schema:
$ref: "#/components/schemas/MembershipRecord"
"404":
$ref: "#/components/responses/NotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
components:
parameters:
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
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.
target_engine_version:
type: string
description: Semver of the game engine to launch.
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 produced by Runtime Manager after a successful
container start. Set on the game record only after the start sequence
succeeds; absent before then.
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. Used for incident investigation.
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
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
MembershipRecord:
type: object
additionalProperties: false
required:
- membership_id
- game_id
- user_id
- race_name
- status
- joined_at
properties:
membership_id:
type: string
game_id:
type: string
user_id:
type: string
race_name:
type: string
status:
type: string
enum:
- active
- removed
- blocked
joined_at:
type: integer
format: int64
removed_at:
type: integer
format: int64
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:
type: string
game_type:
type: string
enum:
- public
- private
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
UpdateGameRequest:
type: object
additionalProperties: false
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
GameListResponse:
type: object
additionalProperties: false
required:
- items
properties:
items:
type: array
items:
$ref: "#/components/schemas/GameRecord"
next_page_token:
type: string
MembershipListResponse:
type: object
additionalProperties: false
required:
- items
properties:
items:
type: array
items:
$ref: "#/components/schemas/MembershipRecord"
next_page_token:
type: string
ProbeResponse:
type: object
additionalProperties: false
required:
- status
properties:
status:
type: 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
NotFoundError:
description: The requested game, application, 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
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