feat(account): deletion orchestration + step-up + gateway edge

Step-up: email accounts confirm with a mailed code (purpose=delete, no deeplink —
ConfirmByToken refuses a delete token so a stray click can't delete); platform-only
accounts type a fixed phrase (anti-impulse). Endpoints /user/delete/{request,confirm};
the confirm orchestration resigns active games, drops all-robot games, tombstones +
anonymizes the account (freeing its creds), and revokes its sessions — the tombstone is
the point of no return, the rest best-effort. Gateway account.delete.{request,confirm}
ops + fbs AccountDeleteConfirm/AccountDeleteRequestResult + branded ru/en delete email.
Integration tests cover the step-up (code + no-email) and the orchestration pieces.
This commit is contained in:
Ilia Denisov
2026-07-03 13:10:34 +02:00
parent fcde7d3db6
commit aa2290b7b4
15 changed files with 589 additions and 1 deletions
+13
View File
@@ -507,6 +507,19 @@ table LinkUnlinkRequest {
kind:string;
}
// AccountDeleteConfirm carries the account-deletion step-up proof: a mailed code (email
// accounts) or the typed phrase (platform-only accounts).
table AccountDeleteConfirm {
code:string;
phrase:string;
}
// AccountDeleteRequestResult reports which deletion step-up the account uses: "email"
// (a code was mailed) or "phrase" (type the confirmation phrase).
table AccountDeleteRequestResult {
method:string;
}
// LinkResult is the unified result of a confirm or merge step. status is "linked"
// (bound to the caller), "merge_required" (the identity belongs to another account —
// the secondary_* fields summarise it for the irreversible confirmation), or
@@ -0,0 +1,71 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type AccountDeleteConfirm struct {
_tab flatbuffers.Table
}
func GetRootAsAccountDeleteConfirm(buf []byte, offset flatbuffers.UOffsetT) *AccountDeleteConfirm {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &AccountDeleteConfirm{}
x.Init(buf, n+offset)
return x
}
func FinishAccountDeleteConfirmBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsAccountDeleteConfirm(buf []byte, offset flatbuffers.UOffsetT) *AccountDeleteConfirm {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &AccountDeleteConfirm{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedAccountDeleteConfirmBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *AccountDeleteConfirm) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *AccountDeleteConfirm) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *AccountDeleteConfirm) Code() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *AccountDeleteConfirm) Phrase() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func AccountDeleteConfirmStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func AccountDeleteConfirmAddCode(builder *flatbuffers.Builder, code flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(code), 0)
}
func AccountDeleteConfirmAddPhrase(builder *flatbuffers.Builder, phrase flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(phrase), 0)
}
func AccountDeleteConfirmEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
@@ -0,0 +1,60 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type AccountDeleteRequestResult struct {
_tab flatbuffers.Table
}
func GetRootAsAccountDeleteRequestResult(buf []byte, offset flatbuffers.UOffsetT) *AccountDeleteRequestResult {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &AccountDeleteRequestResult{}
x.Init(buf, n+offset)
return x
}
func FinishAccountDeleteRequestResultBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsAccountDeleteRequestResult(buf []byte, offset flatbuffers.UOffsetT) *AccountDeleteRequestResult {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &AccountDeleteRequestResult{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedAccountDeleteRequestResultBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *AccountDeleteRequestResult) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *AccountDeleteRequestResult) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *AccountDeleteRequestResult) Method() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func AccountDeleteRequestResultStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func AccountDeleteRequestResultAddMethod(builder *flatbuffers.Builder, method flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(method), 0)
}
func AccountDeleteRequestResultEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}