# 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:

- `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 (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 ~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` 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

- VKCOM/vk-bridge — <https://github.com/VKCOM/vk-bridge>
- VKCOM/vk-apps-launch-params (canonical signature examples) — <https://github.com/VKCOM/vk-apps-launch-params>
- kravetsone/vk-launch-params — <https://github.com/kravetsone/vk-launch-params>
- SevereCloud/vksdk `vkapps.ParamsVerify` (Go reference) — <https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/vkapps>
- VK Mini Apps API — <https://github.com/VKCOM/vk-mini-apps-api>
