跳到主要内容

HTTP Entry Points

Status

Active

Date

2026-04-28

Owners

  • Platform Backend

Last Verified Commit

Use git log -- <this file> for current last-touch history; this field is intentionally not pinned to a static hash so it does not become stale after unrelated commits.

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 providersAggregator relay namespace (/aggregator/provider/{team_code}/{provider_code}/{operation})gateway (proxies to game_service)Aggregator authenticates the native provider and resolves identity; game_service owns provider protocol and wallet semanticsgame_service is bound to 127.0.0.1 only. Gateway exposes only exact allow-listed protocol operations and transparently preserves raw query/body and responses. /integration/* is the internal player backing API and 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

servers_v2 accepts only Aggregator's signed Provider-protocol relay. Public ingress is gateway under /aggregator/provider/{team_code}/{provider_code}/{operation}. Gateway and game_service both restrict this namespace to a static method/provider/operation matrix; it is not an open wildcard. Aggregator authenticates the native provider, resolves its player/session mapping, and signs the exact method, target path, raw query, context and body. game_service verifies that signature and the signed RGB external player/brand before executing the original provider protocol and wallet semantics. It first claims a durable relay Inbox event, transactionally records committed RGB entity snapshots in an outbox, and returns a signed internal result envelope. Aggregator applies its platform projection before releasing the nested exact Provider response. There are no normalized wallet callbacks, direct native provider prefixes, or verification bypass switches. game_service remains loopback-only.

/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.

Boundary evidence:

  • servers_v2/game_service/tests/test_aggregator_only_surface.py
  • servers_v2/gateway/tests/test_provider_routes.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 signed relay player's RGB external identity and verifies it with player_service; provider accounts are never parsed as an RGB trust boundary.

  • 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
  • game_service /integration/bti/league-candidates is a legacy-namespaced internal route: only admin_service may call it, with its per-caller token and the selected X-Brand-Id; it is not a public player or provider surface
  • 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