feat(gateway,ui): client-version gate — turn away too-old builds
Introduce a minimum-supported-client gate so a future incompatible wire change can turn away installed builds too old to speak it, cleanly, instead of letting them crash on decode. It rides the outermost stable layer (an HTTP header), never the FlatBuffers payload. Gateway: - New internal/clientver: dependency-free parse + compare of the leading MAJOR.MINOR.PATCH (a git-describe suffix is tolerated). - GATEWAY_MIN_CLIENT_VERSION config (empty => gate dormant; validated at load). - connectsrv checks the X-Client-Version header before decoding the payload: Execute returns result_code="update_required" (before the registry lookup), Subscribe returns FailedPrecondition. It fails open on an absent or garbled header — the header is a client-controlled compatibility signal, not an access control. Client: - Attach X-Client-Version on every call. - A terminal update.svelte.ts store + a non-dismissable UpdateOverlay (native opens VITE_STORE_URL, web reloads); retry.ts maps FailedPrecondition to the update_required sentinel; a mock __update hook drives the e2e. Wire-additive and contour-safe: no FBS/proto regen, no schema migration; the gate stays dormant until GATEWAY_MIN_CLIENT_VERSION is deliberately set, so web / VK / Telegram behaviour is unchanged. The silent reconciliation seam is deferred to the offline-first work (its only caller). Tests: Go clientver/config/connectsrv gate tests, retry.test.ts, Playwright update.spec.ts.
This commit is contained in:
+28
-12
@@ -81,7 +81,17 @@ Kept current as parts land so a fresh session resumes without re-deriving. Verif
|
||||
`vite-env.d.ts`. `svelte-check` 0 errors, `vitest` 590 passed, web + native `vite build` clean;
|
||||
`?nopay`/`?gp` wallet states verified live via the Playwright MCP browser (the e2e runner can't fetch
|
||||
browsers in this sandbox — the states are covered by `e2e/wallet.spec.ts`, which CI runs).
|
||||
- **C–G — pending.**
|
||||
- **C. Client-version gate — ✅ DONE & verified (2026-07-12).** Server: new `gateway/internal/clientver`
|
||||
(parse + compare), `GATEWAY_MIN_CLIENT_VERSION` config (empty ⇒ dormant, validated at load), and the
|
||||
gate in `connectsrv` — `Execute` returns `result_code="update_required"` before the registry lookup,
|
||||
`Subscribe` returns `FailedPrecondition`; fail-open on an absent/garbled header. Client:
|
||||
`X-Client-Version` on every call (`transport.ts headers()`), a terminal `update.svelte.ts` store +
|
||||
`UpdateOverlay.svelte` (native → `VITE_STORE_URL`, web → reload), `retry.ts` maps
|
||||
`FailedPrecondition → update_required`, the `__update` mock hook. `gofmt`/`vet` clean, Go
|
||||
`clientver`/config/`connectsrv` tests green, `svelte-check` 0, `vitest` 591, e2e 232 (incl.
|
||||
`update.spec.ts`), client build clean. Silent reconciliation seam deferred to D (owner); in-app
|
||||
store-update SDK noted Out of scope.
|
||||
- **D–G — pending.**
|
||||
|
||||
Open logistics (not code): **mandatory icon rebrand (future)** — author ONE vector master and generate
|
||||
*every* icon from it (web `favicon.svg` / PWA `icon-*` / maskable, Android adaptive, future iOS). Today
|
||||
@@ -287,7 +297,7 @@ is: scaffold → native-correct → gate → offline-first → CI → docs → r
|
||||
**Done when:** a native-flavoured build reaches the gateway, signs in as guest, plays a move, and the
|
||||
purchase actions are absent; `pnpm check` + `pnpm test:unit` pass.
|
||||
|
||||
### C. The client-version gate
|
||||
### C. The client-version gate — ✅ DONE
|
||||
|
||||
#### C1. Backend (gateway)
|
||||
|
||||
@@ -364,10 +374,10 @@ purchase actions are absent; `pnpm check` + `pnpm test:unit` pass.
|
||||
before the throw.
|
||||
- catch (after `toGatewayError`, line 73) and the `subscribe()` catch: if the code is
|
||||
`UPDATE_REQUIRED`, call `reportUpdateRequired()` before rethrowing/`onError`.
|
||||
- **Reconciliation exception:** provide a variant used by background guest-reconciliation that does
|
||||
NOT call `reportUpdateRequired()` (it swallows the code and stays offline). Simplest: a boolean
|
||||
on `exec`/a dedicated `authGuestSilent` path guarded by a flag; the executing session picks the
|
||||
seam. Foreground `auth.*` keep the overlay behaviour.
|
||||
- **Reconciliation exception — deferred to D (owner decision).** The silent update-path variant
|
||||
(swallows `update_required` without raising the overlay) has no caller until D.4, so it is added
|
||||
there with its caller rather than speculatively in C. In C every foreground call that gets
|
||||
`update_required` raises the overlay; offline play never trips it (the network kill switch).
|
||||
- **`ui/src/lib/retry.ts` `toGatewayError()`**: add
|
||||
`case Code.FailedPrecondition: return new GatewayError('update_required', e.message);`.
|
||||
- **`ui/src/components/UpdateOverlay.svelte`** (new, mirror `MaintenanceOverlay.svelte`): non-dismissable;
|
||||
@@ -376,8 +386,10 @@ purchase actions are absent; `pnpm check` + `pnpm test:unit` pass.
|
||||
`update.title/body/action` (add siblings to the maintenance keys).
|
||||
- **`ui/src/App.svelte`**: import + place `<UpdateOverlay />` right after `<MaintenanceOverlay />` (line 139).
|
||||
- **Mock e2e hook** — `ui/src/lib/gateway.ts` mock branch, next to `__maint`: `__update = { on: reportUpdateRequired }`.
|
||||
- **Tests:** `update.svelte.test.ts`; extend `retry.test.ts` (`FailedPrecondition → 'update_required'`);
|
||||
Playwright `update.spec.ts` driving `__update.on()`.
|
||||
- **Tests:** extend `retry.test.ts` (`FailedPrecondition → 'update_required'`) and drive the overlay via
|
||||
Playwright `update.spec.ts` (`__update.on()`). The `update.svelte.ts` store is a `$state` rune module,
|
||||
which this project's plugin-less `vitest` cannot import (`$state is not defined`); like every other
|
||||
`*.svelte.ts` store it is covered by the e2e, not a unit test.
|
||||
|
||||
**Done when:** Go `clientver` + gate tests pass; `pnpm check`/`test:unit`/`test:e2e` pass; a local
|
||||
gateway with `GATEWAY_MIN_CLIENT_VERSION=v99.0.0` shows the overlay, unset ⇒ unchanged.
|
||||
@@ -425,9 +437,10 @@ with no network**.
|
||||
`standalone && hasEmail` web gate for native), or bypass preload entirely on native since the
|
||||
dicts are bundled.
|
||||
4. **Lazy server-guest reconciliation** — when native and online and no server session: background
|
||||
`auth.guest` (the silent variant from C2), cache the session, adopt it; unlock online features.
|
||||
Guard against duplicates (only when no cached session). Local games remain device-only. On
|
||||
`update_required`, stay offline silently (no overlay).
|
||||
`auth.guest`, cache the session, adopt it; unlock online features. Guard against duplicates (only
|
||||
when no cached session). Local games remain device-only. **Add the silent update-path variant here**
|
||||
(a flag on the transport `exec`/a dedicated silent `auth.guest`) so reconciliation swallows
|
||||
`update_required` and stays offline — no overlay. This is the seam deferred from C.
|
||||
5. **Ensure both offline modes render for the local guest.** `NewGame.svelte` gates the offline
|
||||
flows on `guest || offlineMode.active`; the local guest makes `offlineMode.active` true, so both
|
||||
the "quick" (vs_ai) and "with friends" (hotseat) flows show. Verify the vs_ai human seat uses the
|
||||
@@ -576,7 +589,10 @@ breaking release deliberately sets the min version.
|
||||
|
||||
Google Play publication (beyond keeping the codebase variant-ready), iOS, OTA/live-updates, in-app
|
||||
purchases in the native build (RuStore billing SDK) and the GP anti-steering fix, VK/Telegram login in
|
||||
the native build, a soft "recommended update" tier, native splash/status-bar plugins, push
|
||||
the native build, a soft "recommended update" tier, **in-app store-update flows** (Google Play In-App
|
||||
Updates API / RuStore In-App Update SDK — a future enhancement that would swap the update overlay's
|
||||
action from opening the store listing to a store-driven immediate in-app update; needs a Capacitor
|
||||
plugin bridge, and is orthogonal to the server-driven gate), native splash/status-bar plugins, push
|
||||
notifications (FCM), and automated RuStore-API upload.
|
||||
|
||||
## End-to-end verification
|
||||
|
||||
Reference in New Issue
Block a user