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:
@@ -13,12 +13,14 @@ import (
|
||||
|
||||
"scrabble/gateway/internal/backendclient"
|
||||
"scrabble/gateway/internal/connector"
|
||||
"scrabble/gateway/internal/vkauth"
|
||||
fb "scrabble/pkg/fbs/scrabblefb"
|
||||
)
|
||||
|
||||
// Message types in the vertical slice.
|
||||
const (
|
||||
MsgAuthTelegram = "auth.telegram"
|
||||
MsgAuthVK = "auth.vk"
|
||||
MsgAuthGuest = "auth.guest"
|
||||
MsgAuthEmailReq = "auth.email.request"
|
||||
MsgAuthEmailLogin = "auth.email.login"
|
||||
@@ -89,8 +91,9 @@ type TelegramValidator interface {
|
||||
|
||||
// NewRegistry builds the slice's message-type catalog over the backend client.
|
||||
// The Telegram auth op is registered only when a validator is supplied (the
|
||||
// connector is configured); otherwise auth.telegram is simply unknown.
|
||||
func NewRegistry(backend *backendclient.Client, tg TelegramValidator) *Registry {
|
||||
// connector is configured); otherwise auth.telegram is simply unknown. Optional ops
|
||||
// (e.g. WithVKAuth) are applied last from opts.
|
||||
func NewRegistry(backend *backendclient.Client, tg TelegramValidator, opts ...Option) *Registry {
|
||||
r := &Registry{ops: make(map[string]Op)}
|
||||
if tg != nil {
|
||||
r.ops[MsgAuthTelegram] = Op{Handler: authTelegramHandler(backend, tg)}
|
||||
@@ -126,9 +129,27 @@ func NewRegistry(backend *backendclient.Client, tg TelegramValidator) *Registry
|
||||
r.ops[MsgFeedbackUnread] = Op{Handler: feedbackUnreadHandler(backend), Auth: true}
|
||||
registerSocialOps(r, backend)
|
||||
registerLinkOps(r, backend, tg)
|
||||
for _, opt := range opts {
|
||||
opt(r, backend)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Option configures an optional registry operation at construction. It is kept out of
|
||||
// NewRegistry's positional signature so existing call sites stay unaffected.
|
||||
type Option func(r *Registry, backend *backendclient.Client)
|
||||
|
||||
// WithVKAuth registers the VK Mini App auth op (auth.vk), which verifies launch params
|
||||
// in-process under the VK app secret. A blank secret leaves auth.vk unregistered, so
|
||||
// the op is simply unknown wherever VK is not configured.
|
||||
func WithVKAuth(secret string) Option {
|
||||
return func(r *Registry, backend *backendclient.Client) {
|
||||
if secret != "" {
|
||||
r.ops[MsgAuthVK] = Op{Handler: authVKHandler(backend, secret)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup returns the operation for messageType, and whether it is registered.
|
||||
func (r *Registry) Lookup(messageType string) (Op, bool) {
|
||||
op, ok := r.ops[messageType]
|
||||
@@ -146,6 +167,9 @@ func DomainCode(err error) (string, bool) {
|
||||
if errors.Is(err, connector.ErrInvalidInitData) {
|
||||
return "invalid_init_data", true
|
||||
}
|
||||
if errors.Is(err, vkauth.ErrInvalid) {
|
||||
return "invalid_vk_params", true
|
||||
}
|
||||
if errors.Is(err, connector.ErrInvalidLoginWidget) {
|
||||
return "invalid_login_widget", true
|
||||
}
|
||||
@@ -175,6 +199,25 @@ func authTelegramHandler(backend *backendclient.Client, tg TelegramValidator) Ha
|
||||
}
|
||||
}
|
||||
|
||||
// authVKHandler verifies a VK Mini App launch in-process (HMAC over the signed vk_*
|
||||
// params under the app secret) and provisions/finds the bound account. Unlike Telegram,
|
||||
// VK omits the user's name from the signed params, so the client-supplied display_name
|
||||
// rides the wire as a cosmetic seed for a brand-new account.
|
||||
func authVKHandler(backend *backendclient.Client, secret string) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsVKLoginRequest(req.Payload, 0)
|
||||
user, err := vkauth.Verify(string(in.Params()), secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sess, err := backend.VKAuth(ctx, user.ExternalID, user.Language, string(in.DisplayName()), string(in.BrowserTz()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeSession(sess), nil
|
||||
}
|
||||
}
|
||||
|
||||
func authGuestHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
// The guest bootstrap historically carried no payload; the detected zone is
|
||||
|
||||
Reference in New Issue
Block a user