# 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: (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 (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: - `VKWebAppInit` — **required**: 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=`): - 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 ~24–72h); 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.fbs` → `VKLoginRequest{ 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; `DomainCode` → `invalid_vk_params`), `internal/backendclient` `VKAuth` → `POST /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 - VKCOM/vk-bridge — - VKCOM/vk-apps-launch-params (canonical signature examples) — - kravetsone/vk-launch-params — - SevereCloud/vksdk `vkapps.ParamsVerify` (Go reference) — - VK Mini Apps API —