Stage 6: gateway edge (Connect/FlatBuffers over h2c, platform/email/guest auth, sessions, rate-limit, admin passthrough, live push bridge)
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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user