Files
scrabble-game/gateway/README.md
T
Ilia Denisov 57c778f9b2
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 57s
feat(telegram,game): single bot + per-user variant preferences
Collapse the two per-language Telegram bots into one unified bot and
replace language-based variant gating with explicit per-user variant
preferences.

- Telegram: one bot; drop service_language and the supported_languages
  set everywhere (DB, account, auth, FlatBuffers Session wire, gateway,
  connector proto). The single bot renders chat and out-of-app push in
  the recipient's preferred_language; remove the game-language push
  routing override (notify Intent.Language / push Event.language).
- Preferences: new accounts.variant_preferences (text[], DB default
  {erudit_ru}, CHECK non-empty + subset of the three variants). Gates
  the New Game picker, vs-AI and the friend invitation the player
  creates, enforced server-side (HTTP 400 otherwise); an invited friend
  may still accept any variant. Edited on the Settings screen; variants
  are Erudit-first everywhere.
- Admin: drop the per-bot language selectors (broadcast / send-to-user)
  and the feedback channel_lang column/field.
- Env/CI: collapse TELEGRAM_BOT_TOKEN_{EN,RU}, TELEGRAM_GAME_CHANNEL_ID_{EN,RU},
  VITE_TELEGRAM_LINK{,_EN,_RU} and VITE_TELEGRAM_GAME_CHANNEL_NAME_{EN,RU}
  to single unsuffixed names; drop GATEWAY_DEFAULT_SUPPORTED_LANGUAGES.
- Docs updated (ARCHITECTURE, FUNCTIONAL + _ru, platform/telegram, gateway,
  backend, ui, UI_DESIGN, PRERELEASE).

The migration squash is deferred to a follow-up PR.
2026-06-20 14:23:25 +02:00

128 lines
6.8 KiB
Markdown

# gateway
The Scrabble platform's only public ingress (module `scrabble/gateway`). It
terminates the client's **Connect-RPC + FlatBuffers** traffic over HTTP/2
cleartext (`h2c`), authenticates the originating credential, mints/resolves a
thin opaque session, rate-limits, injects `X-User-ID` when forwarding to the
backend over REST/JSON, and bridges the backend's gRPC push stream to each
client's in-app live channel. It **embeds the static UI build** (`go:embed`, baked
in by the gateway image's node stage) and serves a **landing page** at `/` and the game
**SPA** at `/app/` (web) and `/telegram/` (the Mini App) — the single-origin model.
Hash-named `/assets/*` are served `immutable`; the HTML shells are `no-cache`. It can also serve the
backend's admin console at `/_gm` behind HTTP Basic-Auth for a local non-caddy run;
in the deployed contour the front caddy owns `/_gm` (see
[`../deploy`](../deploy)). See
[`../docs/ARCHITECTURE.md`](../docs/ARCHITECTURE.md) §2, §3, §10, §12, §13.
## Package layout
```
cmd/gateway/ # main: config -> backend client -> session cache ->
# push hub -> Connect h2c server (+ admin) -> serve
proto/edge/v1/ # Connect envelope contract (committed generated Go)
internal/config/ # GATEWAY_* env config
internal/backendclient/ # typed REST client (+ X-User-ID) and push gRPC client
internal/session/ # in-memory session cache (LRU/TTL, backend fallback)
internal/ratelimit/ # token-bucket limiter (golang.org/x/time/rate) + the rejection tracker
internal/connector/ # gRPC client to the Telegram connector (initData validate, out-of-app push) + routing
internal/push/ # live-event fan-out hub (per-user client streams)
internal/transcode/ # FlatBuffers<->REST bridge + message_type registry
internal/connectsrv/ # the Connect Gateway service over h2c (+ the in-memory active_users gauge)
internal/admin/ # Basic-Auth reverse proxy mounting the backend admin console at /_gm (verbatim)
internal/webui/ # embedded UI build (go:embed dist): landing at /, SPA at /app/ + /telegram/
```
The FlatBuffers payloads and the backend push proto are the shared wire
contracts in [`../pkg`](../pkg).
## Transport contract
A single `Gateway` Connect service: `Execute(message_type, payload, request_id)`
for unary operations and `Subscribe` for the live stream. The `payload` bytes are
FlatBuffers tables (`scrabble/pkg/fbs`); the gateway transcodes them to and from
the backend's JSON. The session token rides in `Authorization: Bearer`; `auth.*`
operations are unauthenticated and return the minted token. A unary domain
outcome rides back in `ExecuteResponse.result_code` (HTTP 200); only edge
failures become Connect error codes.
`auth.telegram` validates the Mini App `initData` by calling the **Telegram connector**
(`GATEWAY_CONNECTOR_ADDR`), which holds the bot token; the gateway also routes
out-of-app push to that connector for recipients with no live in-app stream
(ARCHITECTURE.md §10). When `GATEWAY_CONNECTOR_ADDR` is unset, both are disabled.
The message-type catalog: `auth.telegram`, `auth.guest`,
`auth.email.request`, `auth.email.login`, `profile.get`, `game.submit_play`,
`game.state`, `lobby.enqueue`, `lobby.poll`, `chat.post`, `chat.read` and the play-loop ops;
live events
`your_turn`, `opponent_moved`, `chat_message`, `nudge`, `match_found` (the game events —
and `game_over`/`notify` — carry the state delta the client applies without a `game.state`
refetch). The social/account/history ops —
`friends.*` (list/incoming/request/respond/cancel/unfriend/code.issue/code.redeem),
`blocks.*`, `invitation.*` (list/create/accept/decline/cancel), `profile.update`,
`stats.get`, `game.gcg`, and the `notify` live event — go through the identical
transcode pattern (`transcode_social.go`). Account linking & merge
`link.email.request/confirm/merge` and `link.telegram.confirm/merge`
(`transcode_link.go`); the telegram ops validate the **Login Widget** payload via the
connector (`ValidateLoginWidget`) and forward the trusted `external_id`. These
**superseded** the former `email.bind.*` ops, which were removed.
## Configuration
| Variable | Default | Notes |
| --- | --- | --- |
| `GATEWAY_HTTP_ADDR` | `:8081` | public Connect/h2c listener (also serves the admin console at `/_gm`) |
| `GATEWAY_LOG_LEVEL` | `info` | zap level |
| `GATEWAY_BACKEND_HTTP_URL` | `http://localhost:8080` | backend REST base URL |
| `GATEWAY_BACKEND_GRPC_ADDR` | `localhost:9090` | backend push gRPC address |
| `GATEWAY_BACKEND_TIMEOUT` | `5s` | per backend REST call |
| `GATEWAY_ADMIN_USER` / `GATEWAY_ADMIN_PASSWORD` | unset | enable + guard the admin console at `/_gm` |
| `GATEWAY_CONNECTOR_ADDR` | unset | Telegram connector gRPC address (enables initData validation + out-of-app push) |
| `GATEWAY_SESSION_TTL` | `10m` | cached session lifetime |
| `GATEWAY_SESSION_CACHE_MAX` | `50000` | cached session cap |
| `GATEWAY_PUSH_HEARTBEAT_INTERVAL` | `10s` | live-stream keep-alive (an immediate heartbeat also fires on open, under the ~15s edge idle timeout) |
| `GATEWAY_MAX_BODY_BYTES` | `1048576` | caps one request body and one Connect message read; an oversized Execute is refused with `resource_exhausted` |
| `GATEWAY_SERVICE_NAME` | `scrabble-gateway` | OpenTelemetry `service.name` |
| `GATEWAY_OTEL_TRACES_EXPORTER` | `none` | `none`, `stdout` or `otlp` (gRPC; endpoint from `OTEL_EXPORTER_OTLP_*`) |
| `GATEWAY_OTEL_METRICS_EXPORTER` | `none` | `none`, `stdout` or `otlp` |
Rate-limit defaults (built-in): public 30/min·IP (burst 10), authenticated
300/min·user (burst 80, sized for multi-device play), admin
60/min·IP (burst 20, guarding the `/_gm` mount ahead of its Basic-Auth),
email-code 5/10 min·IP (burst 2).
Every rejection increments `gateway_rate_limited_total{class}`
(`user`/`public`/`email`/`admin`) and logs one Debug line; a reporter drains the
per-key rejection tracker every 30 s, emits a Warn summary per throttled key and
posts the report to the backend (`/api/v1/internal/ratelimit/report`), feeding
the admin console's throttled view and the high-rate auto-flag.
## Run
```sh
GATEWAY_BACKEND_HTTP_URL=http://localhost:8080 \
GATEWAY_BACKEND_GRPC_ADDR=localhost:9090 \
go run ./gateway/cmd/gateway # Connect edge on :8081
```
## Generated code
The Connect envelope Go is committed under `proto/edge/v1`. Regenerate after
editing the `.proto` (dev-time, like `backend/cmd/jetgen`):
```sh
make -C gateway tools # go install protoc-gen-go + protoc-gen-connect-go
make -C gateway gen # buf generate (local plugins)
```
The FlatBuffers payloads are generated in [`../pkg`](../pkg) (`make -C pkg fbs`).
## Tests
```sh
go test -count=1 ./gateway/...
```
All gateway tests are hermetic: no real network, a fake backend (`httptest`) and
credential fixtures. There is no integration (Docker) suite — the gateway holds
no database.