feat(vk): embed the game as a VK Mini App
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
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.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
// VK Mini App SDK access via @vkontakte/vk-bridge. The bridge is imported lazily inside the
|
||||
// functions that need it — not at module top level — because the SDK reads browser globals on
|
||||
// import: the lazy import keeps the pure URL helpers below importable in the node test environment,
|
||||
// and code-splits the bridge into a chunk loaded only on the /vk/ entry. This wraps the subset the
|
||||
// app uses: launch detection, the signed launch parameters (for auth.vk) and the user's display name
|
||||
// (VKWebAppGetUserInfo, since VK omits it from the signed launch params). Every helper is safe to
|
||||
// call outside VK.
|
||||
|
||||
async function bridge() {
|
||||
return (await import('@vkontakte/vk-bridge')).default;
|
||||
}
|
||||
|
||||
/**
|
||||
* onVKPath reports whether the app is served under the dedicated VK entry path (/vk/).
|
||||
*/
|
||||
export function onVKPath(): boolean {
|
||||
if (typeof location === 'undefined') return false;
|
||||
return location.pathname.startsWith('/vk/');
|
||||
}
|
||||
|
||||
/**
|
||||
* vkLaunchParams returns the raw signed VK launch query string (the vk_* parameters plus sign) from
|
||||
* the page URL — the exact form the gateway verifies — or '' when the URL carries no signed launch
|
||||
* (an ordinary browser tab, or the /vk/ path opened directly).
|
||||
*/
|
||||
export function vkLaunchParams(): string {
|
||||
if (typeof location === 'undefined') return '';
|
||||
const query = location.search.replace(/^\?/, '');
|
||||
return new URLSearchParams(query).has('sign') ? query : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* insideVK reports whether the app launched as a VK Mini App — the URL carries signed launch
|
||||
* parameters (an ordinary browser tab has none).
|
||||
*/
|
||||
export function insideVK(): boolean {
|
||||
return vkLaunchParams() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* vkInit signals to the VK client that the Mini App has loaded (VKWebAppInit), dismissing VK's own
|
||||
* loading cover. Best-effort: it resolves even if the bridge is unavailable (outside VK), so the
|
||||
* caller can await it unconditionally.
|
||||
*/
|
||||
export async function vkInit(): Promise<void> {
|
||||
try {
|
||||
await (await bridge()).send('VKWebAppInit', {});
|
||||
} catch {
|
||||
// Outside VK there is no client to receive it; the launch continues regardless.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* vkUserName fetches the launching user's display name via VKWebAppGetUserInfo, since VK omits it
|
||||
* from the signed launch params. Returns '' on any failure or outside VK, so the backend falls back
|
||||
* to a generated placeholder. The value is a cosmetic seed only — being unsigned, the gateway never
|
||||
* trusts it for identity.
|
||||
*/
|
||||
export async function vkUserName(): Promise<string> {
|
||||
try {
|
||||
const u = await (await bridge()).send('VKWebAppGetUserInfo', {});
|
||||
return [u.first_name, u.last_name].filter(Boolean).join(' ').trim();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user