본문으로 건너뛰기

HTTP Entry Points

Status

Active

Date

2026-04-28

Owners

  • Platform Backend

Last Verified Commit

56362a7a

Audience-Level Entry Surfaces

The Public Ingress column names the HTTP listener that the audience actually reaches over the network. The Domain Owner column names the service that hosts the route handlers and owns the business logic. They diverge for game providers: callbacks land on gateway (the only public-internet listener for provider callbacks; agent_service and admin_service also bind non-loopback in compose but their public ingress is the back-office UI, not provider traffic — see servers_v2/tests/test_compose_contract.py::test_loopback_bound_services_pin_host_ip_to_127_0_0_1 for the exclusion list) and are reverse-proxied to game_service, which is loopback-only. Treat Domain Owner as the place to read code; never expose a non-ingress service directly to the public network.

AudienceSurfacePublic IngressDomain OwnerNotes
playersroot legacy paths and /api/v1/*gatewaygateway (with downstream fan-out)compatibility-preserving edge for web clients
agentstop-level /agent/* plus legacy aliases (/user/*, /home/*, /withdraw/*, /player/*)agent_serviceagent_serviceagent frontend does not go through gateway; agent_service has an empty api_prefix so routes are mounted at the top level — there is no /api/v1/agent/* namespace
back-office admins/api/v1/*, /ws, /internal/meta/*admin_serviceadmin_serviceLegacy /api/admin/* surface is split by status: ADR-009-removed families (auth, pushbullet, shooter, web/{rules,faq,config}, legacy admin/agents/agent-withdrawals/meta v2) return 404; un-migrated families that servers_v2 does not replace (/api/admin/{role,menu,config,i18n,bi}/*, /coin/*, /api/admin/legacy-catchall/*) return 410 Gone with Sunset + Deprecation headers via app/api/routes/legacy_sunset.py. See docs/architecture/migration-readiness.md and docs/runbooks/legacy-middle-server-retirement.md for the per-prefix posture.
game providersprovider callback prefixes (/HO, /mg, /wc, /bti, /bt1, /splus, /digitain)gateway (proxies to game_service)game_servicegame_service is bound to 127.0.0.1 only -- gateway is the public listener. Callback auth and provider semantics live in game_service. /integration/* is the internal backing API used by gateway's /api/v1/game/* proxy routes (player surface) and is not provider-facing; it must never be exposed publicly.
internal service callers/internal/* route families(none, internal only)owner serviceprotected by X-Internal-Service-Token; never reachable from the public network.

Player Entry Surface

gateway is the only player-facing HTTP edge in servers_v2.

Primary route families:

  • /api/v1/user/*
  • /api/v1/game/*
  • /api/v1/finance/*
  • /api/v1/records/*
  • /api/v1/common/*
  • legacy root aliases such as /user/*, /game/*, /finance/*

Compatibility evidence:

  • servers_v2/gateway/tests/test_player_route_contracts.py
  • servers_v2/gateway/tests/test_legacy_routes.py

Agent Entry Surface

agent_service serves the agent frontend directly. The service has an empty api_prefix (servers_v2/agent_service/app/core/settings/app.py) so routes are mounted at the top level — there is no /api/v1/ namespace.

Primary route families (mounted via app/api/routes/api.py):

  • /agent/* — auth (/agent/login, /agent/forget, …)
  • /agent/home/* — dashboard
  • /agent/withdraw/* — withdrawals (/agent/withdraw/init, /agent/withdraw/add)
  • /agent/player/* — downstream-player views
  • /agent/sub/* — sub-agent management
  • /agent/bank/* — bank cards
  • /agent/messages/* — messages
  • /agent/coupon/* — coupons (/agent/coupon/grant, /agent/coupon/recycle)
  • /agent/provider/* — game providers
  • /agent/common/* — shared endpoints
  • /agent/recovery/* — SMS-driven password reset

Legacy aliases (mounted from app/api/routes/legacy_routes.py at the top level, include_in_schema=False) preserve paths the older agent frontend already calls:

  • /user/login, /user/refresh_token, /user/forget, /user/top_info, /user/info, /user/edit/password, /user/edit/bank/*, /user/edit/phone/*
  • /home/*, /withdraw/*, /player/*
  • /provider/* (list / percentage / sub/percentage / income)
  • /messages/* (list / {id} / read / unread/count)
  • /bank/list, /common/agent_min_withdrawal_limit
  • /coupon/* (grant / recycle / logs / recycle/logs)
  • additional /agent/* legacy reporting endpoints (/agent/agentStatistic, /agent/agentStatisticDaily, /agent/statisticDaily) distinguished from the new /agent/sub/* namespace by suffix

For the authoritative current list, regenerate the OpenAPI snapshot with python3 tools/openapi/export_openapi.py and inspect docs/reference/openapi/agent_service.json.

Compatibility evidence:

  • servers_v2/agent_service/tests/test_agent_integration.py
  • servers_v2/agent_service/tests/test_legacy_auth_flows.py
  • servers_v2/agent_service/tests/test_legacy_withdraw_flows.py

Admin Entry Surface

admin_service is the only intended back-office HTTP edge in servers_v2.

Primary route families:

  • /api/v1/* operational APIs
  • /ws real-time top-info websocket
  • /internal/meta/* internal synchronization hooks

Removed by ADR-009 (now respond 404):

  • /api/admin/user/* legacy admin auth and session compatibility
  • /api/admin/pushbullet/* and /api/admin/shooter/* recon compatibility
  • /api/admin/web/rules/*, /api/admin/web/faq/*, /api/admin/web/config/* legacy web CMS compatibility
  • legacy admin v2 paths previously served by legacy_admin_v2.py, legacy_agents_v2.py, legacy_agent_withdrawals_v2.py, and legacy_meta_v2.py

Un-migrated legacy middle_server families (return 410 Gone with Sunset + Deprecation headers, mounted via app/api/routes/legacy_sunset.py; not 404 — the loud envelope makes forgotten callers fail visibly rather than look like a deployment glitch):

  • /api/admin/role/* — role management
  • /api/admin/menu/* — menu management
  • /api/admin/config/* — legacy config editing
  • /api/admin/i18n/* — i18n management
  • /api/admin/bi/* — BI push tooling
  • /coin/* — coin-exchange passthrough
  • /api/admin/legacy-catchall/{path:path} — replacement stub for the retired POST /{path:path} generic forwarder

Per-prefix ownership and migration posture are tracked in docs/runbooks/legacy-middle-server-retirement.md; the readiness matrix lives in docs/architecture/migration-readiness.md.

Important note:

  • servers_v2 intentionally does not reproduce the old generic middle_server catch-all POST /{path:path} forwarding behavior.
  • admin compatibility in v2 is explicit and route-by-route, not a broad proxy.

Provider Entry Surface

game_service owns business logic for the provider callback prefixes listed below. Public-internet ingress for these prefixes is the gateway listener (added to gateway's PUBLIC_PATH_PREFIXES allowlist in servers_v2/gateway/app/middleware/auth.py so gateway forwards them to game_service without a player JWT). game_service itself is bound to 127.0.0.1 in compose; never expose its host port:

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

/integration/* is not in this list and is not provider-facing. It is the internal backing API for gateway's player-side /api/v1/game/* routes (e.g. /api/v1/game/list/integration/games); see servers_v2/gateway/CLAUDE.md for the full proxy table. Reachable only over the loopback bind.

Compatibility evidence:

  • servers_v2/game_service/tests/test_legacy_contracts.py

Brand Resolution At The Edge

Per ADR-009, every external entry surface resolves a brand_id before domain code runs:

  • gateway calls _extract_domain(request) to read the Host or Origin header, then looks up the Redis maps domain:agent:{host} and domain:level:{host}. Both maps now resolve to a value containing brand_id. The resolved brand is attached to request.state.brand_id and propagated to downstream services via the X-Brand-Id request header.

  • agent_service resolves brand from the agent frontend domain the same way; the agent_brand allow list determines whether the resolved brand is permitted for the authenticated agent.

  • admin_service is brand-global only for catalog/global-control routes. Brand-scoped operational routes ship the operating brand either in the URL path (most writes, e.g. PUT /brands/{brand_id}/providers, PATCH /brands/{brand_id}/config/{key}) or in the request body (catalog-style allow-list writes whose primary key is (agent_id, brand_id) rather than the brand alone — today this is POST /agent-brand). The BrandResolutionMiddleware populates request.state.brand_id from the X-Brand-Id header (with a Redis domain-map fallback). Two route-level dependencies in app/api/deps.py enforce the same matrix against the state brand:

    • require_brand_path_matches_request (T17) — path-brand surface; attached via Depends(...) on every brand-scoped path route.
    • enforce_brand_body_matches_request — body-brand surface; called as a plain function from the route handler with the body's brand_id field, because FastAPI's Depends cannot introspect parsed body shape. Used today by POST /agent-brand.

    The matrix below applies identically to both surfaces. "Candidate brand_id" means the path brand_id or the body's brand_id field depending on which dep is in play.

    • state.brand_id absent and operator is NOT super_admin: fail closed with the standard brand-required envelope (status=54).
    • state.brand_id absent and operator IS super_admin: allowed to act on the candidate brand (break-glass).
    • state.brand_id == candidate brand_id: allowed.
    • state.brand_id != candidate brand_id and operator IS super_admin: allowed and audited via admin_brand_header_jwt_mismatch_total{outcome="super_admin_allowed"}.
    • state.brand_id != candidate brand_id and operator is NOT super_admin: hard-rejected 403 and audited via admin_brand_header_jwt_mismatch_total{outcome="rejected"}.

    Error envelopes carry the surface name (brand path check rejected vs brand body check rejected) so an operator reading a 403 can tell at a glance which dep fired.

    This is in addition to the existing CR6-D-M2 cross-check in get_current_admin which compares header vs JWT brand_id.

  • game_service resolves brand from the outbound account namespace ({brand_code}_{account}) reverse-parsed from provider callback payloads.

  • recon_service has no public HTTP callback surface; reconciliation signals (SMS, pushbullet) reach recon_service through internal collection paths and brand is resolved from the matched player_deposit.brand_id. There is no edge brand resolution to perform on those signals.

  • /internal/* callers must propagate X-Brand-Id. Internal handlers reject requests that arrive without a brand context for brand-scoped operations.

After login, JWT claims must contain brand_id. gateway rejects requests whose JWT brand does not match the brand resolved from the request domain.

Internal Entry Surface

Internal service routes are currently grouped by owner:

  • player_service: /internal/players/*, /internal/common/*
  • wallet_service: /internal/wallet/*
  • rolling_service: /internal/rolling/*
  • promotion_service: /internal/promotions/*
  • recon_service: /internal/recon/*

Protection and exceptions:

  • owner-service /internal/* routes are intended for service-to-service callers only
  • gateway and admin_service are responsible for preserving external compatibility while attaching per-caller internal auth (X-Caller-Service plus INTERNAL_SERVICE_TOKEN_<CALLER>); the legacy shared INTERNAL_SERVICE_TOKEN is only a Stage A/B fallback and must be absent or empty after the Stage C hard flip
  • explicit external webhook paths remain the exception, for example wallet_service Plisio callback handling that still lives under /internal/wallet/plisio/callback for compatibility reasons