Stage 6: gateway edge (Connect/FlatBuffers over h2c, platform/email/guest auth, sessions, rate-limit, admin passthrough, live push bridge)
Tests · Go / test (push) Successful in 8s
Tests · Integration / integration (push) Successful in 11s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 10s

New public ingress and the first network edge. Framework + a vertical slice of
operations end-to-end; remaining ops reuse the same transcode pattern in Stage 7.

Contracts (new module scrabble/pkg):
- push.proto (backend->gateway gRPC server-stream) + scrabble.fbs (FlatBuffers
  edge payloads), committed generated Go; buf/flatc Makefiles (dev-time codegen).

Backend:
- REST handlers on the /api/v1 groups: internal session endpoints
  (telegram/guest/email login -> mint, resolve, revoke) and the user slice
  (profile, submit_play, state, lobby enqueue/poll, chat).
- internal/notify in-process Publisher hub + internal/pushgrpc gRPC server
  (BACKEND_GRPC_ADDR) streaming your_turn/opponent_moved/chat/nudge/match_found;
  emission in game.commit, social, matchmaker.
- migration 00005 accounts.is_guest; guests are durable rows excluded from stats;
  ProvisionGuest; email-as-login (RequestLoginCode/LoginWithCode).

Gateway (new module scrabble/gateway):
- Connect Gateway service over h2c (Execute + Subscribe), FlatBuffers<->JSON
  transcode registry, Telegram initData HMAC validator (seam), session cache,
  token-bucket rate limiter (3 classes), push fan-out hub, backend REST + push
  gRPC client, admin Basic-Auth reverse proxy.

go.work: use ./pkg, ./gateway + replace scrabble/pkg. CI: gateway/**, pkg/**
path filters; unit build/vet/test span all three modules. Docs (PLAN,
ARCHITECTURE, FUNCTIONAL+ru, TESTING, READMEs) updated; gateway/pkg unit tests +
guest/email-login integration tests.
This commit is contained in:
Ilia Denisov
2026-06-02 22:38:24 +02:00
parent 104eb2a978
commit 408da3f201
98 changed files with 8134 additions and 57 deletions
+67 -1
View File
@@ -39,7 +39,7 @@ independent (see ARCHITECTURE §9.1).
| 3 | Game domain (lifecycle, rules, hint, word-check, history+GCG, stats) | **done** |
| 4 | Lobby & social (matchmaking, friends, block, chat, profile, nudge) | **done** |
| 5 | Robot opponent | **done** |
| 6 | Gateway edge (Connect/FB, platform auth, sessions, push bridge, admin) | todo |
| 6 | Gateway edge (Connect/FB, platform auth, sessions, push bridge, admin) | **done** |
| 7 | UI (plain Svelte + Vite, board, lobby, chat, i18n) | todo |
| 8 | Telegram integration (bot side-service, deep-link, push) | todo |
| 9 | Admin & dictionary ops (complaint review, version reload) | todo |
@@ -355,6 +355,67 @@ Open details: deployment target/host; dashboards; load expectations.
(10 s), `BACKEND_LOBBY_REAPER_INTERVAL` (1 s). No CI change (both Go workflows
already clone the solver sibling and export `BACKEND_DICT_DIR`).
- **Stage 6** (interview + implementation):
- **Scope = framework + vertical slice** (interview): the *whole* edge mechanism
is built and the backend's REST surface + the live-event seam are opened for
the first time, but only a representative slice of operations is wired
end-to-end — auth (`auth.telegram`/`auth.guest`/`auth.email.request`/
`auth.email.login`), `profile.get`, `game.submit_play`/`game.state`,
`lobby.enqueue`/`lobby.poll`, `chat.post`, all five push events, and the admin
passthrough. The remaining domain operations (friends, blocks, invitations,
hint, word-check, pass/exchange/resign, history/GCG, profile editing) reuse the
identical transcode pattern and are wired in **Stage 7** as the UI needs them.
- **Wire contracts in a new shared `scrabble/pkg` module** (interview): the
backend push proto (`pkg/proto/push/v1`) and the FlatBuffers edge payloads
(`pkg/fbs`, one `scrabblefb` namespace) live here with **committed** generated
Go, imported by both backend and gateway. The Connect envelope proto lives in
`gateway/proto/edge/v1`. Codegen is dev-time (`buf generate` with **local**
plugins + `flatc`, driven by per-module `Makefile`s, mirroring `cmd/jetgen`);
CI only builds the committed output. `pkg` and `gateway` are bare-path modules
like `scrabble-solver`, so `go.work` carries `use ./pkg`, `use ./gateway` and a
`replace scrabble/pkg v0.0.0 => ./pkg` (the no-dot path is not VCS-fetchable);
deps were added with `go mod edit` + `go work sync` (the established no-tidy
pattern). `flatc` is pinned to **23.5.26** to match the `flatbuffers` Go runtime.
- **Guest = durable account + `is_guest`** (interview): migration `00005` adds
`accounts.is_guest`; a guest is a durable row with **no identity** (so the
`sessions`/`game_players` foreign keys hold) that is **excluded from statistics**
(the finish-time recompute skips guest seats) and from friends/history. The
earlier "guests never reach this table" comments and §3/§9 were softened to
"no profile/friends/stats persisted". Guest-row GC is a logged TODO (TODO-3).
- **Push = in-process `Publisher` + backend gRPC listener** (interview): a new
`internal/notify` hub (a `Publisher` interface defaulting to `Nop`, installed on
`game`/`social`/`lobby` via `SetNotifier` during boot — additive, existing tests
unchanged) is drained by a new backend gRPC server (`internal/pushgrpc`,
`BACKEND_GRPC_ADDR` default `:9090`) serving `Push.Subscribe`. Emission lives in
`game.commit` (so robot-driver and timeout-sweep moves emit `your_turn`/
`opponent_moved` too — the background sources a handler-only design would miss),
`social` (`chat_message`/`nudge`) and the matchmaker (`match_found`). Event
payloads are FlatBuffers-encoded **in the backend** (it imports `pkg/fbs`); the
gateway forwards them verbatim. Revoke/session-invalidation and cursor-resume are
**deferred** (single-instance MVP).
- **Edge envelope = minimal, token in header** (interview): the `Gateway` Connect
service is `Execute(message_type, payload, request_id)` + `Subscribe`; the
session token rides in `Authorization: Bearer`; auth ops are unauthenticated and
return the token in the FlatBuffers `Session`. Domain outcomes ride back in the
`ExecuteResponse.result_code` (HTTP 200); only edge failures (rate limit, missing
session, unknown type, internal) are Connect error codes. No Ed25519/signing
(the galaxy donor's crypto stack was dropped, per §3).
- **Admin = gateway validates Basic-Auth** (interview): the gateway checks
`GATEWAY_ADMIN_USER`/`_PASSWORD` and reverse-proxies to backend
`/api/v1/admin/*`; the backend admin surface is a single `ping` until Stage 9.
- **Rate-limit = 2 dimensions, 3 classes** (interview): public per-IP (30/min,
burst 10), authenticated per-user (120/min, burst 40), admin per-IP (60/min,
burst 20), plus an email-code per-IP sub-limit (5/10 min); token bucket
(`golang.org/x/time/rate`) with a lazy stale-bucket sweep.
- **Email-as-login** (discharges the Stage 4 deferral): `account.EmailService`
gained `RequestLoginCode`/`LoginWithCode`, reusing the confirm-code mechanism but
provisioning-or-finding the account by email identity (it does **not** refuse an
already-confirmed address — that is the returning user).
- **CI**: both Go workflows gained `gateway/**` (and `pkg/**` where backend depends
on it) path filters and now build/vet/test `./backend/... ./pkg/... ./gateway/...`
(unit) — integration stays `./backend/...` (the only module with tagged tests).
The solver clone + `BACKEND_DICT_DIR` steps are unchanged.
## Deferred TODOs (cross-stage)
- **TODO-1 — publish & version the solver.** Once `scrabble-solver` is stable,
@@ -375,3 +436,8 @@ Open details: deployment target/host; dashboards; load expectations.
dynamic-reload mechanism (Stage 9) — keep the `BACKEND_DICT_DIR` directory as
the runtime contract: a new `.dawg` appears in it and is loaded with
`dawg.Load`.
- **TODO-3 — garbage-collect abandoned guest accounts.** Stage 6 makes a guest a
durable `accounts` row (no identity, `is_guest`), so an ephemeral guest leaves a
row behind. Add a periodic reaper (or a finished-and-idle sweep) that deletes
guest accounts with no active games once their last session is gone; the
`ON DELETE CASCADE` foreign keys clean up the dependent rows.