Files
galaxy-game/user/openapi.yaml
T
2026-04-10 19:05:02 +02:00

1534 lines
48 KiB
YAML

openapi: 3.0.3
info:
title: Galaxy User Service Internal REST API
version: v1
description: |
This specification documents the trusted internal REST contract of
`galaxy/user`.
The current runtime is implemented as an internal-only HTTP service backed
by Redis.
Scope:
- regular-user state only; system-admin identity belongs to future
`Admin Service`
- auth-facing user resolution, ensure, existence, and subject blocking
- gateway-facing authenticated account reads and self-service mutations
- lobby-facing eligibility snapshots
- geo-facing declared-country synchronization
- admin/internal reads, filtered listing, and explicit mutation commands
This specification is internal REST only. It intentionally does not
describe public edge transport, gateway gRPC, or the auxiliary async
event contracts documented in `README.md` and `docs/flows.md`.
The auth-facing paths listed under `AuthIntegration` are already reserved
by `Auth / Session Service` and their route shapes must remain stable.
Current transport rules:
- request bodies are strict JSON only
- unknown fields are rejected
- trailing JSON input is rejected
- error responses use `{ "error": { "code", "message" } }`
- stable error codes are `invalid_request`, `conflict`,
`subject_not_found`, `internal_error`, and `service_unavailable`
servers:
- url: http://localhost:8091
description: Default local internal listener for User Service.
tags:
- name: AuthIntegration
description: Trusted auth-facing user ownership and block-policy endpoints with frozen route shapes reserved by `Auth / Session Service`.
- name: MyAccount
description: Gateway-facing authenticated account queries and self-service mutations.
- name: LobbyIntegration
description: Trusted lobby-facing synchronous eligibility reads.
- name: GeoIntegration
description: Trusted geo-facing declared-country synchronization.
- name: AdminUsers
description: Trusted administrative lookup, listing, and explicit mutation commands.
paths:
/api/v1/internal/user-resolutions/by-email:
post:
tags:
- AuthIntegration
operationId: resolveUserByEmail
summary: Resolve one e-mail address without creating a user
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UserResolutionByEmailRequest"
responses:
"200":
description: Current coarse user-resolution state for the e-mail subject.
content:
application/json:
schema:
$ref: "#/components/schemas/UserResolutionByEmailResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}/exists:
get:
tags:
- AuthIntegration
operationId: userExistsByID
summary: Check whether a stable user identifier exists
parameters:
- $ref: "#/components/parameters/UserIDPath"
responses:
"200":
description: Existence check result for the supplied `user_id`.
content:
application/json:
schema:
$ref: "#/components/schemas/UserExistsResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/ensure-by-email:
post:
tags:
- AuthIntegration
operationId: ensureUserByEmail
summary: Resolve, create, or block one e-mail subject
description: |
Returns an existing user for `email`, creates a new regular platform
user when registration is allowed, or returns a blocked outcome when
policy denies the flow.
`registration_context` is required on the current auth-to-user call.
Its frozen shape is `preferred_language` plus `time_zone`. The
registration context is create-only. Implementations must ignore it for
existing users and must not overwrite settings of an already existing
account.
During the current rollout `Auth / Session Service` sends temporary
`preferred_language="en"` and forwards the public confirm `time_zone`.
Gateway-side geoip language derivation is a later rollout and is not
part of the current source-of-truth contract.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/EnsureByEmailRequest"
responses:
"200":
description: Ensure-user outcome for the supplied `email`.
content:
application/json:
schema:
$ref: "#/components/schemas/EnsureByEmailResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}/block:
post:
tags:
- AuthIntegration
operationId: blockUserByID
summary: Block one user by stable user identifier
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/BlockUserByIDRequest"
responses:
"200":
description: The block mutation applied or the subject was already blocked.
content:
application/json:
schema:
$ref: "#/components/schemas/BlockMutationResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/user-blocks/by-email:
post:
tags:
- AuthIntegration
operationId: blockUserByEmail
summary: Block one e-mail subject even when no user exists yet
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/BlockUserByEmailRequest"
responses:
"200":
description: The block mutation applied or the subject was already blocked.
content:
application/json:
schema:
$ref: "#/components/schemas/BlockMutationResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}/account:
get:
tags:
- MyAccount
operationId: getMyAccount
summary: Read one authenticated regular-user account aggregate
parameters:
- $ref: "#/components/parameters/UserIDPath"
responses:
"200":
description: Read-optimized account aggregate for the supplied `user_id`.
content:
application/json:
schema:
$ref: "#/components/schemas/GetMyAccountResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}/profile:
post:
tags:
- MyAccount
operationId: updateMyProfile
summary: Update self-service profile fields
description: |
`race_name` uniqueness is enforced through a canonical reservation
policy that is case-insensitive, rejects the frozen anti-fraud
confusable pairs, and preserves the original stored casing.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateMyProfileRequest"
responses:
"200":
description: Updated account aggregate after the profile mutation commits.
content:
application/json:
schema:
$ref: "#/components/schemas/GetMyAccountResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}/settings:
post:
tags:
- MyAccount
operationId: updateMySettings
summary: Update self-service settings fields
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateMySettingsRequest"
responses:
"200":
description: Updated account aggregate after the settings mutation commits.
content:
application/json:
schema:
$ref: "#/components/schemas/GetMyAccountResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}/eligibility:
get:
tags:
- LobbyIntegration
operationId: getUserEligibility
summary: Read one synchronous lobby-facing eligibility snapshot
description: |
Returns a read-optimized snapshot for lobby decisions. Unknown users are
represented as `exists=false` instead of `404`.
parameters:
- $ref: "#/components/parameters/UserIDPath"
responses:
"200":
description: Eligibility snapshot for the supplied `user_id`.
content:
application/json:
schema:
$ref: "#/components/schemas/UserEligibilityResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}/declared-country/sync:
post:
tags:
- GeoIntegration
operationId: syncDeclaredCountry
summary: Synchronize the current effective declared country
description: |
Applies the latest effective declared country chosen by
`Geo Profile Service`.
`declared_country` must be a known uppercase ISO 3166-1 alpha-2
country code. When the supplied value is already stored on the user
account, the command is a no-op and returns the existing
`updated_at` unchanged.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SyncDeclaredCountryRequest"
responses:
"200":
description: Declared-country synchronization applied successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/DeclaredCountrySyncResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}:
get:
tags:
- AdminUsers
operationId: getUserByID
summary: Read one user by stable user identifier
parameters:
- $ref: "#/components/parameters/UserIDPath"
responses:
"200":
description: Exact user lookup result for the supplied `user_id`.
content:
application/json:
schema:
$ref: "#/components/schemas/UserLookupResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/user-lookups/by-email:
post:
tags:
- AdminUsers
operationId: getUserByEmail
summary: Read one user by exact-after-trim e-mail
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UserLookupByEmailRequest"
responses:
"200":
description: Exact user lookup result for the supplied `email`.
content:
application/json:
schema:
$ref: "#/components/schemas/UserLookupResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/user-lookups/by-race-name:
post:
tags:
- AdminUsers
operationId: getUserByRaceName
summary: Read one user by exact race name
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UserLookupByRaceNameRequest"
responses:
"200":
description: Exact user lookup result for the supplied `race_name`.
content:
application/json:
schema:
$ref: "#/components/schemas/UserLookupResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users:
get:
tags:
- AdminUsers
operationId: listUsers
summary: List users with deterministic pagination and rich filters
description: |
Returns full user account aggregates ordered by `created_at desc`, then
`user_id desc`.
All supplied query filters combine with logical `AND`.
`page_token` is opaque and bound to the normalized filter set that
produced it. Malformed or filter-mismatched tokens return
`400 invalid_request`.
parameters:
- $ref: "#/components/parameters/PageSize"
- $ref: "#/components/parameters/PageToken"
- name: paid_state
in: query
description: Filter by current free or paid state.
schema:
type: string
enum:
- free
- paid
- name: paid_expires_before
in: query
description: Filter to users whose paid entitlement expires before this RFC 3339 timestamp.
schema:
type: string
format: date-time
- name: paid_expires_after
in: query
description: Filter to users whose paid entitlement expires after this RFC 3339 timestamp.
schema:
type: string
format: date-time
- name: declared_country
in: query
description: Filter by the current effective declared country.
schema:
$ref: "#/components/schemas/CountryCode"
- name: sanction_code
in: query
description: Filter by one active sanction code.
schema:
$ref: "#/components/schemas/SanctionCode"
- name: limit_code
in: query
description: Filter by one active limit code.
schema:
$ref: "#/components/schemas/LimitCode"
- name: can_login
in: query
description: Filter by the derived login eligibility marker.
schema:
type: boolean
- name: can_create_private_game
in: query
description: Filter by the derived private-game creation eligibility marker.
schema:
type: boolean
- name: can_join_game
in: query
description: Filter by the derived game-join eligibility marker.
schema:
type: boolean
responses:
"200":
description: Deterministically ordered page of users.
content:
application/json:
schema:
$ref: "#/components/schemas/UserListResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}/entitlements/grant:
post:
tags:
- AdminUsers
operationId: grantEntitlement
summary: Grant a new entitlement period
description: |
Grants a current paid entitlement when the current effective state is
`free`.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/GrantEntitlementRequest"
responses:
"200":
description: Entitlement grant applied successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/EntitlementCommandResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}/entitlements/extend:
post:
tags:
- AdminUsers
operationId: extendEntitlement
summary: Extend the current entitlement period
description: |
Extends the current finite paid entitlement.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ExtendEntitlementRequest"
responses:
"200":
description: Entitlement extension applied successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/EntitlementCommandResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}/entitlements/revoke:
post:
tags:
- AdminUsers
operationId: revokeEntitlement
summary: Revoke the effective paid entitlement
description: |
Revokes the current effective paid entitlement.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RevokeEntitlementRequest"
responses:
"200":
description: Entitlement revocation applied successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/EntitlementCommandResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}/sanctions/apply:
post:
tags:
- AdminUsers
operationId: applySanction
summary: Apply one sanction record
description: |
Applies one new active sanction record.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ApplySanctionRequest"
responses:
"200":
description: Sanction application applied successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/SanctionCommandResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}/sanctions/remove:
post:
tags:
- AdminUsers
operationId: removeSanction
summary: Remove one active sanction record
description: |
Removes the current active sanction for one `sanction_code`.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RemoveSanctionRequest"
responses:
"200":
description: Sanction removal applied successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/SanctionCommandResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}/limits/set:
post:
tags:
- AdminUsers
operationId: setLimit
summary: Set one active user-specific limit record
description: |
Creates one new active limit or replaces the current active record of
the same `limit_code`.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SetLimitRequest"
responses:
"200":
description: User-specific limit set successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/LimitCommandResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
/api/v1/internal/users/{user_id}/limits/remove:
post:
tags:
- AdminUsers
operationId: removeLimit
summary: Remove one active user-specific limit record
description: |
Removes the current active user-specific limit for one `limit_code`.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RemoveLimitRequest"
responses:
"200":
description: User-specific limit removal applied successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/LimitCommandResponse"
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/SubjectNotFoundError"
"409":
$ref: "#/components/responses/ConflictError"
"500":
$ref: "#/components/responses/InternalError"
"503":
$ref: "#/components/responses/ServiceUnavailableError"
components:
parameters:
UserIDPath:
name: user_id
in: path
required: true
description: Stable regular-user identifier owned by User Service.
schema:
$ref: "#/components/schemas/UserID"
PageSize:
name: page_size
in: query
description: Maximum number of users returned in one page.
schema:
type: integer
minimum: 1
maximum: 200
default: 50
PageToken:
name: page_token
in: query
description: Opaque deterministic pagination cursor returned by the previous page and bound to the normalized filter set that produced it. Malformed or filter-mismatched tokens return `400 invalid_request`.
schema:
type: string
schemas:
UserID:
type: string
description: Stable regular-user identifier.
minLength: 1
Email:
type: string
format: email
description: |
Login and contact e-mail address. The service trims surrounding
whitespace and validates the value structurally, then treats the
trimmed value as the exact stored and lookup value. The service does
not lowercase or otherwise canonicalize e-mail before storage or exact
lookup.
RaceName:
type: string
description: |
Stored race name preserving the user-selected casing after successful
uniqueness checks. Uniqueness is enforced against a canonical
reservation key rather than exact string equality only.
minLength: 1
maxLength: 64
LanguageTag:
type: string
description: |
BCP 47 language tag. User Service validates semantic correctness on
auth-driven creation and stores the canonical tag form.
minLength: 1
maxLength: 32
TimeZoneName:
type: string
description: |
IANA time zone name. User Service validates semantic correctness on
auth-driven creation and stores the trimmed caller value without
additional alias canonicalization.
minLength: 1
maxLength: 128
CountryCode:
type: string
description: |
ISO 3166-1 alpha-2 country code in uppercase ASCII form. The geo sync
command additionally rejects well-formed but unknown region codes.
pattern: "^[A-Z]{2}$"
UserResolutionKind:
type: string
enum:
- existing
- creatable
- blocked
EnsureUserOutcome:
type: string
enum:
- existing
- created
- blocked
BlockUserOutcome:
type: string
enum:
- blocked
- already_blocked
PlanCode:
type: string
enum:
- free
- paid_monthly
- paid_yearly
- paid_lifetime
SanctionCode:
type: string
enum:
- login_block
- private_game_create_block
- private_game_manage_block
- game_join_block
- profile_update_block
LimitCode:
type: string
description: |
Current supported user-specific limit codes. Retired legacy codes may
still exist in stored history for backward compatibility, but they are
not part of this write or read contract.
enum:
- max_owned_private_games
- max_pending_public_applications
- max_active_game_memberships
ActorRef:
type: object
additionalProperties: false
required:
- type
properties:
type:
type: string
description: Machine-readable actor type such as `admin`, `service`, or `billing`.
id:
type: string
description: Optional stable actor identifier.
RegistrationContext:
type: object
description: |
Frozen create-only initialization context used by the current
auth-facing ensure-by-email contract. `preferred_language` is
semantically validated as BCP 47 and stored in canonical tag form on
create. `time_zone` is semantically validated as an IANA time zone
name and stored after trim without additional alias canonicalization.
additionalProperties: false
required:
- preferred_language
- time_zone
properties:
preferred_language:
$ref: "#/components/schemas/LanguageTag"
description: |
Create-only initial preferred language. During the current rollout
`Auth / Session Service` sends a temporary `"en"` default and
forwards `time_zone`. Gateway-side geoip derivation is not part of
the current source-of-truth contract. Future derived values must
remain valid BCP 47 tags.
time_zone:
$ref: "#/components/schemas/TimeZoneName"
description: Create-only initial IANA time zone name.
UserResolutionByEmailRequest:
type: object
additionalProperties: false
required:
- email
properties:
email:
$ref: "#/components/schemas/Email"
UserResolutionByEmailResponse:
type: object
additionalProperties: false
required:
- kind
properties:
kind:
$ref: "#/components/schemas/UserResolutionKind"
user_id:
$ref: "#/components/schemas/UserID"
block_reason_code:
type: string
description: Present only for `kind=blocked`.
UserExistsResponse:
type: object
additionalProperties: false
required:
- exists
properties:
exists:
type: boolean
EnsureByEmailRequest:
type: object
additionalProperties: false
required:
- email
- registration_context
properties:
email:
$ref: "#/components/schemas/Email"
registration_context:
$ref: "#/components/schemas/RegistrationContext"
EnsureByEmailResponse:
type: object
additionalProperties: false
required:
- outcome
properties:
outcome:
$ref: "#/components/schemas/EnsureUserOutcome"
user_id:
$ref: "#/components/schemas/UserID"
description: |
Present for `existing` and `created`. A `created` outcome returns
the durable newly materialized `user_id` created together with an
initial generated `player-<shortid>` race name and free
entitlement snapshot.
block_reason_code:
type: string
description: Present only for `outcome=blocked`.
BlockUserByIDRequest:
type: object
additionalProperties: false
required:
- reason_code
properties:
reason_code:
type: string
BlockUserByEmailRequest:
type: object
additionalProperties: false
required:
- email
- reason_code
properties:
email:
$ref: "#/components/schemas/Email"
reason_code:
type: string
BlockMutationResponse:
type: object
additionalProperties: false
required:
- outcome
properties:
outcome:
$ref: "#/components/schemas/BlockUserOutcome"
user_id:
$ref: "#/components/schemas/UserID"
EntitlementSnapshot:
type: object
description: |
Materialized current effective entitlement snapshot.
The current snapshot is read-optimized and repaired lazily when a
finite paid state has already reached `ends_at`, so callers do not
observe stale paid/free state.
additionalProperties: false
required:
- plan_code
- is_paid
- source
- starts_at
- updated_at
properties:
plan_code:
$ref: "#/components/schemas/PlanCode"
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
updated_at:
type: string
format: date-time
ActiveSanction:
type: object
additionalProperties: false
required:
- sanction_code
- scope
- reason_code
- applied_at
properties:
sanction_code:
$ref: "#/components/schemas/SanctionCode"
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
ActiveLimit:
type: object
additionalProperties: false
description: |
Current supported active user-specific limit override. Retired legacy
limit codes are ignored on reads and are not returned.
required:
- limit_code
- value
- reason_code
- applied_at
properties:
limit_code:
$ref: "#/components/schemas/LimitCode"
value:
type: integer
minimum: 0
reason_code:
type: string
actor:
$ref: "#/components/schemas/ActorRef"
applied_at:
type: string
format: date-time
expires_at:
type: string
format: date-time
EffectiveLimit:
type: object
additionalProperties: false
description: |
Materialized numeric quota after the frozen `free` or `paid` default
catalog is combined with any active user-specific override for the same
`limit_code`.
`max_owned_private_games` is meaningful only while the current
entitlement is paid and is omitted from free effective limits.
`max_active_game_memberships` applies only to public games.
`max_pending_public_applications` stores the total public-games budget.
`Game Lobby` subtracts current active public memberships from this
value and clamps at `0` to derive remaining pending-application
headroom.
required:
- limit_code
- value
properties:
limit_code:
$ref: "#/components/schemas/LimitCode"
value:
type: integer
minimum: 0
AccountView:
type: object
additionalProperties: false
required:
- user_id
- email
- race_name
- preferred_language
- time_zone
- entitlement
- active_sanctions
- active_limits
- created_at
- updated_at
properties:
user_id:
$ref: "#/components/schemas/UserID"
email:
$ref: "#/components/schemas/Email"
race_name:
$ref: "#/components/schemas/RaceName"
preferred_language:
$ref: "#/components/schemas/LanguageTag"
time_zone:
$ref: "#/components/schemas/TimeZoneName"
declared_country:
$ref: "#/components/schemas/CountryCode"
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
GetMyAccountResponse:
type: object
additionalProperties: false
required:
- account
properties:
account:
$ref: "#/components/schemas/AccountView"
UpdateMyProfileRequest:
type: object
additionalProperties: false
description: |
The current implementation accepts only `race_name` here. Attempts to
mutate `email` or `declared_country` are rejected as `400
invalid_request` through strict unknown-field handling.
required:
- race_name
properties:
race_name:
$ref: "#/components/schemas/RaceName"
UpdateMySettingsRequest:
type: object
additionalProperties: false
required:
- preferred_language
- time_zone
properties:
preferred_language:
$ref: "#/components/schemas/LanguageTag"
time_zone:
$ref: "#/components/schemas/TimeZoneName"
EligibilityMarkers:
type: object
additionalProperties: false
required:
- can_login
- can_create_private_game
- can_manage_private_game
- can_join_game
- can_update_profile
properties:
can_login:
type: boolean
can_create_private_game:
type: boolean
can_manage_private_game:
type: boolean
can_join_game:
type: boolean
can_update_profile:
type: boolean
UserEligibilityResponse:
type: object
additionalProperties: false
required:
- exists
- user_id
- active_sanctions
- effective_limits
- markers
properties:
exists:
type: boolean
user_id:
$ref: "#/components/schemas/UserID"
entitlement:
description: |
Current effective entitlement snapshot. Omitted when `exists=false`.
$ref: "#/components/schemas/EntitlementSnapshot"
active_sanctions:
type: array
items:
$ref: "#/components/schemas/ActiveSanction"
effective_limits:
description: |
Materialized effective quotas for the current supported lobby
catalog. Unknown users return an empty array. Free users omit
`max_owned_private_games`.
type: array
items:
$ref: "#/components/schemas/EffectiveLimit"
markers:
$ref: "#/components/schemas/EligibilityMarkers"
SyncDeclaredCountryRequest:
type: object
additionalProperties: false
description: |
Synchronizes the latest effective declared country selected by
`Geo Profile Service`. Repeating the current stored value is accepted
as a no-op.
required:
- declared_country
properties:
declared_country:
$ref: "#/components/schemas/CountryCode"
DeclaredCountrySyncResponse:
type: object
additionalProperties: false
required:
- user_id
- declared_country
- updated_at
properties:
user_id:
$ref: "#/components/schemas/UserID"
declared_country:
$ref: "#/components/schemas/CountryCode"
updated_at:
type: string
format: date-time
description: |
Effective account mutation timestamp. Same-value no-op syncs return
the existing stored timestamp unchanged.
UserAdminView:
allOf:
- $ref: "#/components/schemas/AccountView"
UserLookupByEmailRequest:
type: object
additionalProperties: false
required:
- email
properties:
email:
$ref: "#/components/schemas/Email"
UserLookupByRaceNameRequest:
type: object
additionalProperties: false
required:
- race_name
properties:
race_name:
$ref: "#/components/schemas/RaceName"
UserLookupResponse:
type: object
additionalProperties: false
required:
- user
properties:
user:
$ref: "#/components/schemas/UserAdminView"
UserListResponse:
type: object
additionalProperties: false
required:
- items
properties:
items:
type: array
items:
$ref: "#/components/schemas/UserAdminView"
next_page_token:
type: string
GrantEntitlementRequest:
type: object
additionalProperties: false
description: |
Grants one current paid entitlement.
`plan_code=free` is invalid here. `starts_at` may be current or past,
but not future. Finite paid plans require `ends_at`, while
`paid_lifetime` forbids it.
required:
- plan_code
- source
- reason_code
- actor
- starts_at
properties:
plan_code:
$ref: "#/components/schemas/PlanCode"
source:
type: string
reason_code:
type: string
actor:
$ref: "#/components/schemas/ActorRef"
starts_at:
type: string
format: date-time
ends_at:
type: string
format: date-time
description: Required for `paid_monthly` and `paid_yearly`; omitted for `paid_lifetime`.
ExtendEntitlementRequest:
type: object
additionalProperties: false
description: |
Extends the current finite paid entitlement by appending one new paid
history segment.
required:
- source
- reason_code
- actor
- ends_at
properties:
source:
type: string
reason_code:
type: string
actor:
$ref: "#/components/schemas/ActorRef"
ends_at:
type: string
format: date-time
RevokeEntitlementRequest:
type: object
additionalProperties: false
description: |
Revokes the current effective paid entitlement and materializes a new
`free` snapshot immediately.
required:
- source
- reason_code
- actor
properties:
source:
type: string
reason_code:
type: string
actor:
$ref: "#/components/schemas/ActorRef"
EntitlementCommandResponse:
type: object
additionalProperties: false
description: Resulting current effective entitlement snapshot after one
successful trusted entitlement command.
required:
- user_id
- entitlement
properties:
user_id:
$ref: "#/components/schemas/UserID"
entitlement:
$ref: "#/components/schemas/EntitlementSnapshot"
ApplySanctionRequest:
type: object
additionalProperties: false
required:
- sanction_code
- scope
- reason_code
- actor
- applied_at
properties:
sanction_code:
$ref: "#/components/schemas/SanctionCode"
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
RemoveSanctionRequest:
type: object
additionalProperties: false
required:
- sanction_code
- reason_code
- actor
properties:
sanction_code:
$ref: "#/components/schemas/SanctionCode"
reason_code:
type: string
actor:
$ref: "#/components/schemas/ActorRef"
SanctionCommandResponse:
type: object
additionalProperties: false
required:
- user_id
- active_sanctions
properties:
user_id:
$ref: "#/components/schemas/UserID"
active_sanctions:
type: array
items:
$ref: "#/components/schemas/ActiveSanction"
SetLimitRequest:
type: object
additionalProperties: false
required:
- limit_code
- value
- reason_code
- actor
- applied_at
properties:
limit_code:
$ref: "#/components/schemas/LimitCode"
value:
type: integer
minimum: 0
reason_code:
type: string
actor:
$ref: "#/components/schemas/ActorRef"
applied_at:
type: string
format: date-time
expires_at:
type: string
format: date-time
RemoveLimitRequest:
type: object
additionalProperties: false
required:
- limit_code
- reason_code
- actor
properties:
limit_code:
$ref: "#/components/schemas/LimitCode"
reason_code:
type: string
actor:
$ref: "#/components/schemas/ActorRef"
LimitCommandResponse:
type: object
additionalProperties: false
required:
- user_id
- active_limits
properties:
user_id:
$ref: "#/components/schemas/UserID"
active_limits:
type: array
items:
$ref: "#/components/schemas/ActiveLimit"
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
message:
type: string
responses:
InvalidRequestError:
description: Request body, path, or query fields are invalid.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
invalidRequest:
value:
error:
code: invalid_request
message: request is invalid
SubjectNotFoundError:
description: The referenced user or lookup subject does not exist.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
subjectNotFound:
value:
error:
code: subject_not_found
message: subject not found
ConflictError:
description: The requested mutation conflicts with current source-of-truth state.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
conflict:
value:
error:
code: conflict
message: request conflicts with current state
InternalError:
description: Internal User Service error.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
internalError:
value:
error:
code: internal_error
message: internal server error
ServiceUnavailableError:
description: User Service is temporarily unable to serve the request safely.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
unavailable:
value:
error:
code: service_unavailable
message: service is unavailable