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:
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"scrabble/gateway/internal/clientver"
|
||||
pkgtel "scrabble/pkg/telemetry"
|
||||
)
|
||||
|
||||
@@ -54,6 +55,11 @@ type Config struct {
|
||||
// MaxBodyBytes caps one inbound request body on the public listener and one
|
||||
// Connect message read; oversized requests are refused without buffering.
|
||||
MaxBodyBytes int
|
||||
// MinClientVersion, when non-empty, is the lowest client version (MAJOR.MINOR.PATCH)
|
||||
// the edge serves. A client reporting an older X-Client-Version is turned away with an
|
||||
// "update required" signal before its payload is decoded. Empty leaves the gate dormant
|
||||
// (every client is served) — the default for web-only deployments.
|
||||
MinClientVersion string
|
||||
// RateLimit configures the in-memory anti-abuse limiter.
|
||||
RateLimit RateLimitConfig
|
||||
// Abuse configures the temporary IP ban and the honeytoken (prod-only).
|
||||
@@ -226,14 +232,15 @@ func (c VKIDConfig) Enabled() bool {
|
||||
func Load() (Config, error) {
|
||||
var err error
|
||||
c := Config{
|
||||
HTTPAddr: envOr("GATEWAY_HTTP_ADDR", defaultHTTPAddr),
|
||||
LogLevel: envOr("GATEWAY_LOG_LEVEL", defaultLogLevel),
|
||||
BackendHTTPURL: envOr("GATEWAY_BACKEND_HTTP_URL", defaultBackendHTTPURL),
|
||||
BackendGRPCAddr: envOr("GATEWAY_BACKEND_GRPC_ADDR", defaultBackendGRPCAddr),
|
||||
AdminUser: os.Getenv("GATEWAY_ADMIN_USER"),
|
||||
AdminPassword: os.Getenv("GATEWAY_ADMIN_PASSWORD"),
|
||||
ValidatorAddr: os.Getenv("GATEWAY_VALIDATOR_ADDR"),
|
||||
VKAppSecret: os.Getenv("GATEWAY_VK_APP_SECRET"),
|
||||
HTTPAddr: envOr("GATEWAY_HTTP_ADDR", defaultHTTPAddr),
|
||||
LogLevel: envOr("GATEWAY_LOG_LEVEL", defaultLogLevel),
|
||||
BackendHTTPURL: envOr("GATEWAY_BACKEND_HTTP_URL", defaultBackendHTTPURL),
|
||||
BackendGRPCAddr: envOr("GATEWAY_BACKEND_GRPC_ADDR", defaultBackendGRPCAddr),
|
||||
AdminUser: os.Getenv("GATEWAY_ADMIN_USER"),
|
||||
AdminPassword: os.Getenv("GATEWAY_ADMIN_PASSWORD"),
|
||||
ValidatorAddr: os.Getenv("GATEWAY_VALIDATOR_ADDR"),
|
||||
VKAppSecret: os.Getenv("GATEWAY_VK_APP_SECRET"),
|
||||
MinClientVersion: os.Getenv("GATEWAY_MIN_CLIENT_VERSION"),
|
||||
VKID: VKIDConfig{
|
||||
AppID: os.Getenv("GATEWAY_VK_ID_APP_ID"),
|
||||
ClientSecret: os.Getenv("GATEWAY_VK_ID_CLIENT_SECRET"),
|
||||
@@ -332,6 +339,11 @@ func (c Config) validate() error {
|
||||
if c.MaxBodyBytes <= 0 {
|
||||
return fmt.Errorf("config: GATEWAY_MAX_BODY_BYTES must be positive")
|
||||
}
|
||||
if c.MinClientVersion != "" {
|
||||
if _, ok := clientver.Parse(c.MinClientVersion); !ok {
|
||||
return fmt.Errorf("config: GATEWAY_MIN_CLIENT_VERSION %q is not a MAJOR.MINOR.PATCH version", c.MinClientVersion)
|
||||
}
|
||||
}
|
||||
if c.Abuse.BanEnabled && (c.Abuse.BanThreshold <= 0 || c.Abuse.BanWindow <= 0 || c.Abuse.BanDuration <= 0) {
|
||||
return fmt.Errorf("config: GATEWAY_ABUSE_BAN_THRESHOLD/_WINDOW/_DURATION must be positive when GATEWAY_ABUSE_BAN_ENABLED")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user