Files
scrabble-game/.claude/vk-games-integration.md
T
Ilia Denisov 65c194264c
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
feat(vk): embed the game as a VK Mini App
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.
2026-06-27 11:37:31 +02:00

9.4 KiB
Raw Blame History

VK Mini App / VK Games — integration reference

Captured research + our implementation map, so a future session does not need to re-fetch the VK docs. Authoritative external source: https://dev.vk.com/ (the dev.vk.com portal does not render via plain HTTP fetch; the facts below were cross-checked against the VKCOM reference repos cited at the end and verified against our own Go implementation).

A VK game is technically a VK Mini App: an HTML5 SPA VK loads in an iframe inside vk.com (desktop + mobile web) and in a WebView inside the VK mobile apps (iOS/Android). We serve our existing SPA under a dedicated /vk/ path, mirroring the Telegram /telegram/ entry — the single-origin, path-routed model.

1. Embedding model

  • VK loads the app at the Web iframe URL configured in the app settings (HTTPS + valid cert required), appending the signed launch parameters as the URL query string.
  • An optional separate Mobile iframe URL is used by the VK mobile apps (we use the same).
  • No special X-Frame-Options / CSP frame-ancestors is required from us — VK frames the configured origin. (Our edge sets no framing headers today, so VK works as-is; see the clickjacking note in §Security.)
  • URL must match the settings exactly (scheme, host, no stray www/whitespace).

2. Launch parameters (URL query)

VK appends these to the iframe src. The vk_* set is what the signature covers.

Param Meaning
vk_user_id signed-in VK user numeric id — the identity
vk_app_id our registered app id
vk_is_app_user 0/1 — user authorized/installed the app
vk_are_notifications_enabled 0/1
vk_language 2-letter UI language (ru, en, …)
vk_platform mobile_iphone | mobile_android | mobile_web | desktop_web | …
vk_ts unix seconds when VK generated the params
vk_ref where the app was opened from (catalog, feed, …)
vk_access_token_settings comma-separated granted scopes (often empty)
vk_group_id, vk_viewer_group_role, vk_is_favorite, vk_client optional/contextual
sign the signature (see §3) — NOT part of the signed set

Always present: vk_user_id, vk_app_id, vk_platform, vk_ts, sign.

The user's name is NOT in the launch params (only vk_user_id). Read it client-side via VKWebAppGetUserInfo (see §4) — unsigned, so treat it as a cosmetic display seed only.

3. Signature verification (sign) — CONFIRMED base64url, not hex

Algorithm (verified against our gateway/internal/vkauth + an independent Python reference):

  1. Collect the query params whose key starts with vk_ (exclude sign).
  2. Sort by key (alphabetical).
  3. Serialize as a URL-encoded query string k=v&k=v… (Go url.Values.Encode() matches VK's reference serialization for the constrained launch-param charset).
  4. HMAC-SHA256(serialized, secret) where secret = the app's «Защищённый ключ» (protected / secure key, a.k.a. client_secret) from the app settings.
  5. base64url, no padding (+-, /_, strip =).
  6. Constant-time compare against sign.

VK launch params have no built-in expiry (unlike Telegram's auth_date). We do NOT enforce freshness — the minted server session is the short-lived credential; a replay only re-authenticates the same vk_user_id.

Verified against the official doc https://dev.vk.com/ru/mini-apps/development/launch-params-sign (prose + PHP example: base64url = strtr('+/','-_') + rtrim('=')) and reproduced identically by independent Node crypto + Python references. Doc-example caveat: that page shows secret wvl68m4dR1UpLrVRli → sign exTIBP…, but the secret is a placeholder — recomputing with it does NOT yield the shown sign (it was made with the real, unshown key). Don't chase the mismatch; our vkauth.Verify is correct (gateway/internal/vkauth/vkauth_test.go carries cross-checked vectors, incl. the %2C comma case for vk_access_token_settings).

4. VK Bridge (client SDK)

@vkontakte/vk-bridge (npm, v3.x; bundled — default export bridge). Methods we use / may use:

  • VKWebAppInitrequired: tells VK the Mini App loaded (dismisses VK's loading cover).
  • VKWebAppGetUserInfo{ id, first_name, last_name, photo_200, … }; no extra scope needed.
  • VKWebAppGetLaunchParams — parsed vk_* without sign (so NOT usable for our server verification — read window.location.search instead, which carries sign).
  • VKWebAppGetAuthToken — OAuth access token for VK API calls (only if we ever call VK API).
  • VKWebAppShare — native share dialog (deferred).
  • VKWebAppSetViewSettings / VKWebAppSetSwipeSettings — viewport / swipe-back (mobile).
  • VKWebAppUpdateConfig (subscribe) — light/dark scheme + theme changes.

The bridge talks to the embedding VK client over postMessage; it is NOT an external fetch, so it has no telegram.org-style load-hang risk. The SDK reads browser globals at import — we import it lazily so the pure URL helpers stay node-test-importable.

5. Test mode (to verify before moderation)

  1. App already registered (we have the App ID).
  2. In the app settings (dev.vk.com / vk.com/editapp?act=settings&app_id=<id>):
    • Category = Игра (Game).
    • Web iframe URL = our public HTTPS /vk/ (the test-contour origin for contour testing, prod https://erudit-game.ru/vk/ later). Mobile iframe URL = same.
    • Copy the «Защищённый ключ» → set as GATEWAY_VK_APP_SECRET (Gitea TEST_/PROD_ secret).
    • Add own VK id to testers; open in test mode.
  3. Test mode = visible only to admins/testers, no payments processed.

6. Auth / identity (our model)

  • vk_user_id (from verified params) → backend identity kind='vk', external_id=vk_user_id, auto-confirmed (a platform identity). First contact seeds language from vk_language and the display name from the client-supplied VKWebAppGetUserInfo name (placeholder if empty).
  • No VK access token / VK API call needed for the launch+login MVP.

7. Payments / monetization

VK Pay / «голоса» (votes) are optional, not required to publish a free game. Not planned.

8. ToS / moderation (pre-publish, analyzed — no blocker for a free «Эрудит»)

  • Trademark: "Scrabble" is trademarked. Our public brand is «Эрудит» (erudit-game.ru), a generic Russian word-game name → fine. Ensure the VK-registered app name is «Эрудит»/word-game, NOT "Scrabble". The repo name is internal and irrelevant to moderation.
  • Pre-publish requirements: public Privacy Policy + ToS URLs (disclose collected data: vk_user_id, language; mention VK), age rating (likely 6+/12+), icon, description.
  • Dictionary: standard word lists; VK may expect offensive-word filtering — likely fine for a dictionary game, flag if moderation asks.
  • In-game chat (UGC): we already have a moderated chat + support relay → covered.
  • Moderation reviews after submission (commonly ~2472h); rejects on violence/hate/sexual/illegal content or IP infringement — none apply.

9. Platforms

Desktop web (iframe), mobile web (iframe), VK iOS app (WKWebView), VK Android app (WebView). Bridge methods behave per-platform; the app's own back chevron + app-shell document-pin cover navigation without VK-specific code. Theme/viewport fine-tuning is best verified live in the real VK client (not reproducible in Playwright — like the iOS gesture caveats).

10. Our implementation map (what to touch for VK)

  • Wire: pkg/fbs/scrabble.fbsVKLoginRequest{ params, browser_tz, display_name } (regen: make -C pkg fbs + pnpm -C ui codegen).
  • Gateway: internal/vkauth/ (the §3 verify), internal/transcode op auth.vk (registered via WithVKAuth(secret) option; DomainCodeinvalid_vk_params), internal/backendclient VKAuthPOST /api/v1/internal/sessions/vk, config GATEWAY_VK_APP_SECRET, SPA mount /vk/ in internal/connectsrv/server.go.
  • Backend: internal/account KindVK + ProvisionVK/vkSeed + confirmed for platform kinds; internal/server/handlers_auth.go handleVKAuth + route; migration 00005_vk_identity.sql (widen identities_kind_chk to include 'vk', expand-contract).
  • UI: src/lib/vk.ts (onVKPath/vkLaunchParams/insideVK/vkInit/vkUserName), app.svelte.ts bootVK + the /vk/ dispatch branch + shared retryMiniAppBoot, codec.ts encodeVKLogin, transport.ts/client.ts/mock/client.ts authVK.
  • Edge/deploy: deploy/caddy/Caddyfile /vk path; GATEWAY_VK_APP_SECRET in docker-compose.yml + .env.example + ci.yaml (TEST_… secret) + prod-deploy.yaml (PROD_… secret, deploy-main).
  • Deferred (not in the test-mode MVP): VKWebAppShare, deep-links (vk_ref/startapp), VK theme/viewport forcing, VITE_VK_APP_ID build arg, payments, account-linking a vk identity to an existing account.

Sources