docs: sign-in methods matrix — unlink + change-email

FUNCTIONAL (+ru): drop the stale 'linking UI hidden' note; document the profile
sign-in-methods matrix, the unlink last-identity guard, and the non-disclosing
atomic email change. ARCHITECTURE: unlink + change-email behaviour and the
purpose=change deeplink branch.
This commit is contained in:
Ilia Denisov
2026-07-03 10:01:54 +02:00
parent 912096a0f1
commit 150660819a
3 changed files with 32 additions and 10 deletions
+12 -2
View File
@@ -243,8 +243,8 @@ arrive from a platform rather than completing a mandatory registration).
also carries a **one-tap confirm deeplink** (`/app/#/confirm/<token>`, an opaque
256-bit token stored only as its SHA-256, 12-hour TTL): a login mints a session in the
browser that opens it (magic-link), a link confirms the identity and emits a `notify`
profile-refresh to the in-app session, and a would-be merge is deferred to the
interactive flow. The confirm runs on load; the token rides the URL fragment (never
profile-refresh to the in-app session, a change switches the account's email in place,
and a would-be merge is deferred to the interactive flow. The confirm runs on load; the token rides the URL fragment (never
sent to the server), so a plain link prefetch cannot reach it, and the manual
six-digit code is the fallback if an aggressive scanner runs the page. An
**email-login** account is created flagged `is_guest` and stays reapable until the
@@ -263,6 +263,16 @@ arrive from a platform rather than completing a mandatory registration).
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`).
- **Unlink** detaches a platform identity (`telegram`/`vk`) from the profile. The
backend **refuses removing the last identity** (`ErrLastIdentity`), so an account
never becomes unreachable; the UI mirrors the guard by hiding Unlink when only one
method remains. **Email is never unlinked — it is changed.**
- **Change email** mails a confirm-code (`purpose=change`) to the new address on the
authenticated account and, on confirm (code or one-tap deeplink), **atomically
replaces** the account's email identity with the new one, freeing the old address. A
new address already confirmed by **another** account is refused **without disclosure**
(a neutral "check the address or contact support") and **never merged** — the anti-
enumeration check is only reachable by someone who controls the new mailbox.
- **Merge** retires the account that owns the linked identity into the **current**
account, in a single transaction (`internal/accountmerge`): statistics summed
(counters incl. moves/hints added, max points kept, and the per-variant best moves