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
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.
158 lines
9.4 KiB
Markdown
158 lines
9.4 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 (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 ~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 — <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>
|