feat(vk): embed the game as a VK Mini App
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 1m0s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s

Mirror the Telegram Mini App wrapper for VK: the SPA loads at a new /vk/
entry, authenticates from VK's signed launch parameters, and provisions a
'vk' platform identity — the minimum to run the game in VK test mode.

- Gateway verifies the launch signature in-process (internal/vkauth:
  HMAC-SHA256 over the sorted vk_* params under GATEWAY_VK_APP_SECRET,
  base64url) — a pure offline check, no side-service. New auth.vk op
  (gated on the secret), backendclient.VKAuth, /vk/ SPA mount.
- Backend: KindVK + ProvisionVK/vkSeed, /sessions/vk handler, identity
  kind widened to include 'vk' (migration 00005, expand-contract).
- UI: src/lib/vk.ts (VK Bridge, lazy-imported), bootVK + the /vk/ boot
  dispatch, encodeVKLogin + authVK across transport/client/mock. VK omits
  the name from the signed params, so the client reads it via
  VKWebAppGetUserInfo as an unsigned display seed.
- Deploy: /vk in the edge Caddyfile, GATEWAY_VK_APP_SECRET wired through
  compose + .env.example + CI (TEST_) + prod-deploy (PROD_).
- Admin console: surface the VK user id (link to the VK profile) next to
  the Telegram id on the user card.
- Docs: ARCHITECTURE §12/§13, FUNCTIONAL (+ _ru), gateway README; VK
  integration reference under .claude/.

Signature algorithm verified against dev.vk.com plus independent Node/Python
references and a %2C edge-case vector.
This commit is contained in:
Ilia Denisov
2026-06-27 11:37:31 +02:00
parent 13c22734ee
commit 65c194264c
43 changed files with 1175 additions and 50 deletions
+16 -8
View File
@@ -149,11 +149,18 @@ arrive from a platform rather than completing a mandatory registration).
- The gateway validates the originating credential **once** — Telegram `initData`
(delegated to the **validator's** `ValidateInitData` RPC, which holds the bot token —
the HMAC secret — so it never reaches the gateway), an email-code login, or a guest
the HMAC secret — so it never reaches the gateway), a **VK Mini App** launch (verified
**in-process** by `gateway/internal/vkauth`: HMAC-SHA256 over the signed `vk_*` params
under the VK app's protected key `GATEWAY_VK_APP_SECRET`, base64url — a pure offline check,
as VK signing needs no API round-trip, so no side-service), an email-code login, or a guest
bootstrap — then mints a **thin opaque server session token** (`session_id`). First
Telegram contact seeds the new account's language (from the launch `language_code`)
and display name (§4). The validator runs on the main host and never reaches the Bot
API, so login does not depend on Telegram or the remote bot being up (§10, §12).
Telegram/VK contact seeds the new account's language (Telegram's `language_code` / VK's
`vk_language`) and display name (§4; VK omits the name from the signed params, so the client
reads it via `VKWebAppGetUserInfo` as an unsigned, cosmetic seed). The validator runs on the
main host and never reaches the Bot API, so login does not depend on Telegram or the remote
bot being up (§10, §12). VK launch params carry no built-in expiry (unlike Telegram's
`auth_date`), so freshness is not enforced — the minted session is the short-lived credential,
and a replay only re-authenticates the same `vk_user_id`.
- **Single bot.** The platform side-service runs **one bot** (one token + one optional
game channel), split into a home **validator** and a remote **bot** that share the
token. `ValidateInitData` (the validator) validates `initData` against that single
@@ -1046,10 +1053,11 @@ a dedicated redeem sub-limit or a longer code is the hardening step if abuse app
Single public origin, path-routed. The Vite build has two entries: a lightweight
**landing page** and the game **SPA**. The gateway **embeds** the SPA build
(`go:embed`, baked in by a node stage in `gateway/Dockerfile`) and serves it at
`/app/` (web) and `/telegram/` (the Telegram Mini App; on that path without sign-in data
`/app/` (web), `/telegram/` (the Telegram Mini App; on that path without sign-in data
— no `initData` — the client renders a compact, shareable launch-diagnostic screen instead
of redirecting away); a stray hit on the gateway's `/`
308-redirects to `/app/`. The **landing** ships in its own static container: the
of redirecting away) and `/vk/` (the VK Mini App; the client reads the signed `vk_*` launch
parameters from the URL and the gateway verifies them in-process — §12); a stray hit on the
gateway's `/` 308-redirects to `/app/`. The **landing** ships in its own static container: the
`landing` target of `gateway/Dockerfile` (caddy:2-alpine + the same Vite build,
`deploy/landing/Caddyfile`) serves it at `/`, so stray public traffic is absorbed by
static file serving and never reaches the Go edge. Hash-named `/assets/*` are served
@@ -1058,7 +1066,7 @@ static file serving and never reaches the Go edge. Hash-named `/assets/*` are se
in-compose **caddy** is the contour's edge: it owns a single `/_gm` Basic-Auth and
routes `/_gm/grafana/*` to **Grafana** (anonymous-admin, so the one shared login gates
it with no per-user Grafana accounts) and the rest of `/_gm/*` to the backend-rendered
**admin console**; `/app/`, `/telegram/` and the Connect path go to the gateway; the
**admin console**; `/app/`, `/telegram/`, `/vk/` and the Connect path go to the gateway; the
catch-all — notably the landing at `/` — goes to the landing container. The
**Telegram validator** runs as a separate container with **no public ingress**,
answering only internal gRPC (HMAC, no Telegram egress). The **Telegram bot** holds