Stage 11: account linking & merge (email + Telegram Login Widget)
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 11s
Tests · UI / test (push) Successful in 20s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 11s
Tests · UI / test (push) Successful in 20s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s
Link an email (confirm-code) or Telegram (web Login Widget) to the current account; if the identity already has its own account, merge the two into the one in use (the current account is primary, except a guest initiator whose durable counterpart wins). The merge runs in one transaction (internal/accountmerge): stats + hint wallet summed, paid_account ORed, identities/games/chat/complaints transferred, friends/blocks de-duplicated, the secondary kept as a merged_into tombstone so a shared finished game's no-cascade FKs hold; a shared active game blocks the merge. - migration 00009: accounts.paid_account, merged_into, merged_at (+ jetgen) - internal/link orchestrator; session.RevokeAllForAccount on merge - connector ValidateLoginWidget RPC + loginwidget HMAC validator - edge ops link.email.request/confirm/merge, link.telegram.confirm/merge; supersedes the Stage 8 email.bind.* surface (request never reveals 'taken' before the code is verified, so a probe cannot enumerate addresses) - UI Profile link section + irreversible-merge dialog; Telegram web sign-in - focused regression tests (merge core, guest inversion, active-game refusal, finished-shared-game kept), gateway transcode + connector + UI codec/e2e - docs: PLAN, ARCHITECTURE 3/4/9, FUNCTIONAL(+ru), module READMEs
This commit is contained in:
+33
-13
@@ -113,7 +113,9 @@ arrive from a platform rather than completing a mandatory registration).
|
||||
records live in `backend`, which stores only a **SHA-256 hash** of the opaque
|
||||
token (never the plaintext), keeps a warmed in-memory cache for fast
|
||||
resolution, and treats sessions as **revoke-only** — they have no TTL and live
|
||||
until explicitly revoked (`status` → `revoked`).
|
||||
until explicitly revoked (`status` → `revoked`). A revoke can target one token or,
|
||||
on an account merge (§4), **every** session of the retired account
|
||||
(`RevokeAllForAccount`, which also evicts them from the warm cache).
|
||||
- **Guest** = ephemeral web session (no platform, no email). A guest is backed by
|
||||
a durable `accounts` row flagged `is_guest` and carrying **no identity** — the
|
||||
row is a technical necessity (the `sessions` and `game_players` foreign keys
|
||||
@@ -134,17 +136,33 @@ arrive from a platform rather than completing a mandatory registration).
|
||||
authenticated account: a 6-digit code (stored only as a SHA-256 hash, 15-minute
|
||||
TTL, ≤ 5 attempts) is sent through a `Mailer` seam (an SMTP relay, or a
|
||||
development log mailer when none is configured) and, once verified, attaches a
|
||||
confirmed email identity. An email already confirmed by **another** account is
|
||||
refused — adopting it would be a merge, which Stage 11 owns. Accounts and
|
||||
identities use application-generated **UUIDv7** primary keys.
|
||||
- **Linking** is initiated from an authenticated profile: choose a platform →
|
||||
complete that platform's web-auth confirm → attach the identity to the
|
||||
current account.
|
||||
- **Merge**: if the identity being linked already has its own account with
|
||||
history, the two accounts are **merged into the current one (A is primary)**:
|
||||
statistics are summed, games and friends are transferred, duplicates are
|
||||
de-duplicated, the secondary account is retired. High blast-radius; an
|
||||
isolated, well-tested stage.
|
||||
confirmed email identity. Accounts and identities use application-generated
|
||||
**UUIDv7** primary keys. A service flag `paid_account` (lifetime one-time
|
||||
payment; no purchase flow yet) is carried on the account and ORed on a merge.
|
||||
- **Linking** (Stage 11) is initiated from an authenticated profile and proves
|
||||
control of the identity before attaching it: **email** through the confirm-code
|
||||
flow, **Telegram** through the web **Login Widget** (validated by the connector,
|
||||
HMAC under `SHA-256(bot_token)` — distinct from Mini App initData; the gateway
|
||||
passes the trusted `external_id` to the backend, as for `auth.telegram`). The
|
||||
request step **always** sends/accepts the proof (no pre-send "already taken"
|
||||
signal, so a probe cannot enumerate registered addresses); a required **merge**
|
||||
is revealed **only after** the proof is verified and is performed behind an
|
||||
explicit, irreversible confirmation. A free identity is simply attached (and a
|
||||
guest is promoted to durable, clearing `is_guest`).
|
||||
- **Merge** retires the account that owns the linked identity into the **current**
|
||||
account, in a single transaction (`internal/accountmerge`): statistics summed
|
||||
(max points kept), the hint wallet summed, `paid_account` ORed, identities
|
||||
repointed, games / chat / complaints transferred, friends and blocks
|
||||
de-duplicated (friendships keep the strongest status accepted>pending>declined),
|
||||
pending invitations/codes dropped, and the secondary kept as an **audit
|
||||
tombstone** (`accounts.merged_into`/`merged_at`) so a shared **finished** game's
|
||||
no-cascade foreign keys stay valid — its seat there is left untouched. A merge is
|
||||
**refused** only when the two share an **active** game. The current account is the
|
||||
primary, **except** when the initiator is a **guest** and the linked identity
|
||||
already has a **durable** owner: then the durable account wins, the guest's active
|
||||
games move into it, the guest is retired, and a **fresh session** is minted for the
|
||||
durable account (the client switches to it). The secondary's sessions are revoked
|
||||
(§3). High blast-radius; an isolated, well-tested stage.
|
||||
|
||||
## 5. Game engine integration (`scrabble-solver`)
|
||||
|
||||
@@ -337,7 +355,9 @@ requires (there is no DM surface; chat is per-game).
|
||||
- Tables: `accounts` (durable internal accounts; Stage 3 added the away-window
|
||||
columns `away_start`/`away_end` and the hint wallet `hint_balance`; Stage 6's
|
||||
migration `00005` added the `is_guest` flag for ephemeral guest rows; Stage 9's
|
||||
migration `00007` added the `notifications_in_app_only` out-of-app push toggle),
|
||||
migration `00007` added the `notifications_in_app_only` out-of-app push toggle;
|
||||
Stage 11's migration `00009` added the `paid_account` service flag and the
|
||||
merge-tombstone columns `merged_into`/`merged_at`),
|
||||
`identities` (platform/email/robot identities, unique `(kind, external_id)`;
|
||||
Stage 5's migration `00004` admits the `robot` kind),
|
||||
`sessions` (revoke-only opaque-token hashes), the Stage 3 game tables
|
||||
|
||||
Reference in New Issue
Block a user