본문으로 건너뛰기

Game Service

Status

Active

Date

2026-04-28

Owners

  • Platform Backend

Last Verified Commit

56362a7a

Runtime

API only

Purpose

game_service owns provider callback handling and game-integration-specific logic. It keeps provider-specific authentication and callback semantics isolated from the rest of the platform.

Primary Entry Points

  • /integration/*
  • /HO/*
  • /mg/*
  • /wc/*
  • /bti/*
  • /splus/*
  • /bt1/*
  • /digitain/*

Dependencies

  • PostgreSQL
  • Redis
  • wallet_service
  • provider-specific credentials and callback config

Internal coordination:

  • outbound wallet-owner calls use per-caller internal auth (X-Caller-Service: game plus INTERNAL_SERVICE_TOKEN_GAME)
  • inbound POST /integration/launch is a trusted backing route for gateway only. The gateway forwards the resolved X-Brand-Id, injects the JWT-verified player_id, and signs the service-to-service hop with X-Internal-Service-Token / X-Caller-Service: gateway; account-only or direct same-network launch callers are rejected before provider URLs are minted.

Background Work

None documented as a standalone worker today.

Owned Data

  • provider callback transaction state
  • provider integration metadata and sync behavior local to game integrations

Events

Emits:

  • no first-class Redis stream ownership is currently documented

Consumes:

  • BRAND_CATALOG_CHANGED Redis pub/sub for brand-code reverse-parse cache invalidation
  • BRAND_PROVIDER_CHANGED Redis pub/sub for per-brand provider allow-list cache invalidation

Health

  • API /health checks DB and Redis

Key Env Vars

  • DATABASE_URL
  • REDIS_URL
  • SECRET_KEY
  • WALLET_SERVICE_URL
  • MULTI_BRAND_ENFORCEMENT — required; one of off / observe / enforce. Production target is enforce post-Phase-16. Drives multi_brand_enforcement_mode{service="game_service"} gauge and the brand-aware vs raw-fallback callback resolution decision (see callback_brand.resolve_callback_brand).
  • BRAND_SIGNING_KEYrequired in production; HMAC-SHA256 secret used to sign X-Brand-Signature on outbound brand-scoped wallet writes from callback handlers (game is one of the 6 wallet write callers).
  • INTERNAL_SERVICE_TOKEN_GATEWAYrequired in production on the consumer side; per-caller token accepted from gateway for protected backing routes such as /integration/launch once PER_CALLER_TOKEN_REQUIRED=on.
  • INTERNAL_SERVICE_TOKEN_GAMErequired in production; per-caller token presented to wallet when game callbacks drive settle/credit/rollback.
  • PER_CALLER_TOKEN_REQUIREDon is the Phase 16 target on the consumer side; game consults it for inbound protected backing routes and downstream services consult it when game is the caller.
  • INTERNAL_SERVICE_TOKEN — legacy single-shared-token; deprecated. Phase 16 release gate requires the bare variant to be absent.
  • VERIFY_CALLBACKS — provider-callback signature verification toggle. MUST be true in production. Default is true (safe-by-default). The /HO/, /mg/, /wc/ paths are listed in gateway PUBLIC_PATH_PREFIXES; with this set to false the public internet can POST arbitrary settle/credit/rollback payloads to wallet_service. The boot-time guard in app/core/boot_guards.py::assert_verify_callbacks_safe_for_runtime refuses to start game_service when the runtime env resolves to a production-grade label (production/prod/staging) and VERIFY_CALLBACKS=false. The runtime bypass branch in each handler also increments game_callback_verification_bypassed_total{provider} so any non-prod accidental enabling stays observable.
  • provider credentials such as:
    • HO_*
    • WC_*
    • BTI_*
    • BT1_URL
    • SPLUS_URL
    • DIGITAIN_*
    • MG_*

Multi-Brand Constraints

Per ADR-009:

  • game provider credentials and integration secrets stay global; one set per provider serves every brand
  • provider availability is brand-scoped through brand_provider_config. If a brand has no rows, it inherits the global provider.is_show catalog for backward compatibility. Once any policy rows exist, /integration/providers, /integration/games, /integration/top, and /integration/launch only expose or launch providers enabled for that brand and still globally visible through provider.is_show
  • outbound calls to providers use a brand-namespaced account: {brand_code}_{account} (or the documented per-provider equivalent for providers that constrain account format)
  • inbound provider callbacks reverse-parse the namespaced account to recover brand_id and player account, and route the resulting bet authorization, settlement, or rollback into the correct brand
  • provider callback transaction state and provider-specific records carry brand_id
  • a callback whose namespaced account cannot be reverse-parsed is rejected and logged with provider context but no brand assumption (this rejection is unconditional and not gated by MULTI_BRAND_ENFORCEMENT because there is no safe fallback brand for an unparseable callback); the rejected callback is written to game_callback_dead_letter for later replay after the brand catalog is fixed
  • the reverse-parse cache holds brand_code -> brand_id per process; entries refresh on a 60-second TTL AND are invalidated by the BRAND_CATALOG_CHANGED Redis pub/sub channel published by admin_service after every brand create / disable. Each game_service process subscribes on startup; processes that miss a message refresh on TTL expiry within 60 seconds

Outbound account length audit

The platform's brand_code is capped at 16 characters and the player account at 32 characters, so the worst-case namespaced account ({brand_code}_{account}) is 16 + 1 + 32 = 49 characters. The audit below documents whether each currently integrated provider accepts a 49-character account; providers that don't get a per-provider fallback.

The provider columns marked unknown are not constrained in the local schema and the published spec sheets are not version-pinned in this repo; they are documented under the conservative assumption that 49 characters fits, which matches the provider integrations actually wired today (none truncate or reject the legacy account on outbound).

ProviderAccount-field capWorst-case 49-char namespaced account fits?Fallback if not
HOunknown — provider spec accepts arbitrary username strings; current integration sends account as cw[@uname] (XML attribute) without an enforced capyes(none required)
MGunknown — provider spec accepts opaque playerId strings; integration sends account as playerId (string field) without an enforced capyes(none required)
WCunknown — provider spec accepts member_id opaque strings; integration sends account as member_id and stores it as the wc_account.account PRIMARY KEY (String(255))yes(none required)
BTIunknown — JWT-embedded account claim, no provider-side schema cap; the embedded JWT is opaque to BTIyes(none required)
BT1unknown — same protocol family as BTIyes(none required)
SPLUSunknown — same protocol family as BTIyes(none required)
Digitainunknown — JWT-embedded account claim, sent in user_id field of launch URL; provider spec does not document a capyes(none required)
Integration (launch URL)n/a — internal launch URL builderyes(none required)

If a future provider is added with a stricter cap, document the per-provider fallback (suggested: sha256(brand_code + "_" + account)[:N] hash projection, with the resulting hash recorded alongside the original account in a side table for reverse lookup) and update the row above with the resolved implementation: replace the yes / no fits column with the projection algorithm and link to the side-table migration. Do NOT leave a literal todo-marker string here — the existence of this guidance paragraph already serves that role, and a literal marker would register as outstanding debt in repo-wide scans.

Tests

  • cd servers_v2/game_service && uv run pytest
  • key suites:
    • tests/test_legacy_contracts.py