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
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.
176 lines
11 KiB
Markdown
176 lines
11 KiB
Markdown
# 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>
|