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
+2
View File
@@ -1,5 +1,7 @@
// automatically generated by the FlatBuffers compiler, do not modify
export { AccountDeleteConfirm } from './scrabblefb/account-delete-confirm.js';
export { AccountDeleteRequestResult } from './scrabblefb/account-delete-request-result.js';
export { AccountRef } from './scrabblefb/account-ref.js';
export { Ack } from './scrabblefb/ack.js';
export { AlphabetEntry } from './scrabblefb/alphabet-entry.js';
@@ -0,0 +1,60 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class AccountDeleteConfirm {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):AccountDeleteConfirm {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsAccountDeleteConfirm(bb:flatbuffers.ByteBuffer, obj?:AccountDeleteConfirm):AccountDeleteConfirm {
return (obj || new AccountDeleteConfirm()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsAccountDeleteConfirm(bb:flatbuffers.ByteBuffer, obj?:AccountDeleteConfirm):AccountDeleteConfirm {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new AccountDeleteConfirm()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
code():string|null
code(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
code(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
phrase():string|null
phrase(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
phrase(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startAccountDeleteConfirm(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addCode(builder:flatbuffers.Builder, codeOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, codeOffset, 0);
}
static addPhrase(builder:flatbuffers.Builder, phraseOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, phraseOffset, 0);
}
static endAccountDeleteConfirm(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createAccountDeleteConfirm(builder:flatbuffers.Builder, codeOffset:flatbuffers.Offset, phraseOffset:flatbuffers.Offset):flatbuffers.Offset {
AccountDeleteConfirm.startAccountDeleteConfirm(builder);
AccountDeleteConfirm.addCode(builder, codeOffset);
AccountDeleteConfirm.addPhrase(builder, phraseOffset);
return AccountDeleteConfirm.endAccountDeleteConfirm(builder);
}
}
@@ -0,0 +1,48 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class AccountDeleteRequestResult {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):AccountDeleteRequestResult {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsAccountDeleteRequestResult(bb:flatbuffers.ByteBuffer, obj?:AccountDeleteRequestResult):AccountDeleteRequestResult {
return (obj || new AccountDeleteRequestResult()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsAccountDeleteRequestResult(bb:flatbuffers.ByteBuffer, obj?:AccountDeleteRequestResult):AccountDeleteRequestResult {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new AccountDeleteRequestResult()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
method():string|null
method(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
method(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startAccountDeleteRequestResult(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addMethod(builder:flatbuffers.Builder, methodOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, methodOffset, 0);
}
static endAccountDeleteRequestResult(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createAccountDeleteRequestResult(builder:flatbuffers.Builder, methodOffset:flatbuffers.Offset):flatbuffers.Offset {
AccountDeleteRequestResult.startAccountDeleteRequestResult(builder);
AccountDeleteRequestResult.addMethod(builder, methodOffset);
return AccountDeleteRequestResult.endAccountDeleteRequestResult(builder);
}
}