Files
scrabble-game/.claude/vk-games-integration.md
T
Ilia Denisov 373aa2aa6d
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
docs: sync VK theme-follow + home-bar safe-area into FUNCTIONAL/UI_DESIGN/.claude
The #142 polish shipped three VK behaviours the docs never picked up (they were
written for the #140 auth MVP):

- the "auto" theme follows the VK client light/dark (VKWebAppUpdateConfig), not
  the VK webview's prefers-color-scheme;
- the layout clears the VK mobile home bar via CSS env() max'd with the VK bridge
  insets (VKWebAppUpdateInsets) — the bridge value is needed on Android, where the
  VK webview exposes no env() inset (.claude said env() handled it alone);
- share/copy route through the bridge (VKWebAppShare/VKWebAppCopyText).

FUNCTIONAL (+_ru) VK paragraph gains theme+home-bar parity with the Telegram one;
UI_DESIGN gets a VK-integration note beside the Telegram one.
2026-06-30 08:05:38 +02:00

11 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 (the friend-code invite uses it; navigator.share is absent in the desktop VK iframe). Used.
  • VKWebAppCopyText — clipboard copy that works inside the VK iframe, where navigator.clipboard is blocked. Used as the copy-code / copy-link path.
  • VKWebAppUpdateConfig (subscribe) — light/dark scheme; the app follows it while the theme pref is "auto" (the VK webview's prefers-color-scheme does not track it). Used.
  • VKWebAppSetViewSettings / VKWebAppSetSwipeSettings — viewport / swipe-back (mobile); not used.
  • VKWebAppUpdateInsets (+ VKWebAppUpdateConfig) — device safe-area insets; the app max'es them with CSS env(safe-area-inset-*) (viewport-fit=cover) so the bottom home bar is cleared. The bridge value is needed on Android, where the VK webview exposes no env() inset. Used.

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.

Deep links — NOT possible on VK (confirmed on the contour). The VK iframe receives ONLY the signed vk_* launch params (+ sign); VK strips any custom data from the app link. The documented vk.com/app<id>#<payload> form is eaten by the vk.com SPA (which owns the URL hash), and a vk.com/app<id>?hash=<payload> query is dropped (the diagnostic showed rawSearch with only vk_* and an empty hash). So the friend-code invite link is just vk.com/app<id> (vkShareLink, app id from vk_app_id); the recipient enters the copied code by hand (VKWebAppCopyText works). The vkStartParam reader + the bootVK routing stay as a no-op today, ready if a post-moderation VK channel (e.g. an invite API) ever delivers a payload.

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 plus vkAppId/ vkStartParam/vkShare/vkCopyText/vkOnScheme), app.svelte.ts bootVK (+ deep-link routing and VK scheme→theme) + the /vk/ dispatch branch + shared retryMiniAppBoot, codec.ts encodeVKLogin, transport.ts/client.ts/mock/client.ts authVK, deeplink.ts vkShareLink, Friends.svelte (VK share/copy), app.css --tg-safe-* defaulting to env(safe-area-inset-*).
  • 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: payments (VK Pay / votes), native (Capacitor) VK, account-linking a vk identity to an existing account, VK push. (Done after the launch+auth MVP — Group B: native share + clipboard via the bridge, the friend-code deep link, the auto-theme follow, and the home-bar safe area.)

Sources