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
+36 -1
View File
@@ -42,6 +42,7 @@ const (
purposeLogin = "login"
purposeLink = "link"
purposeChange = "change"
purposeDelete = "delete"
)
// Errors returned by the email confirm-code flow.
@@ -65,6 +66,9 @@ var (
// ErrTooManyRequests is returned when confirm-code sends to an address are being
// requested too frequently (the resend cooldown or the rolling-hour cap).
ErrTooManyRequests = errors.New("account: too many code requests")
// ErrNoEmail is returned when an email-code step-up is requested for an account that
// holds no confirmed email (the caller must use the typed-phrase path instead).
ErrNoEmail = errors.New("account: no confirmed email")
)
// EmailService runs the email confirm-code flow: it issues a 6-digit code over a
@@ -115,7 +119,14 @@ func (s *EmailService) issueCode(ctx context.Context, accountID uuid.UUID, email
if err := s.store.replacePendingConfirmation(ctx, accountID, email, codeHash, tokenHash, purpose, s.now().Add(emailCodeTTL)); err != nil {
return err
}
msg, err := renderConfirmationEmail(purpose, code, s.confirmURL(token, locale), s.baseURL, locale)
// Account deletion is never a one-tap link (a prefetch or a stray click must not delete
// an account): the delete code is entered in the app only, so the email omits the
// deeplink and ConfirmByToken refuses a delete token.
deeplink := s.confirmURL(token, locale)
if purpose == purposeDelete {
deeplink = ""
}
msg, err := renderConfirmationEmail(purpose, code, deeplink, s.baseURL, locale)
if err != nil {
return err
}
@@ -347,6 +358,9 @@ func (s *EmailService) ConfirmByToken(ctx context.Context, token string) (LinkCo
return LinkConfirmation{}, err
}
return LinkConfirmation{Purpose: purposeChange, Account: pend.accountID}, nil
case purposeDelete:
// Deletion is confirmed in the app with the code, never via a one-tap link.
return LinkConfirmation{}, fmt.Errorf("account: deletion cannot be confirmed by link")
default:
return LinkConfirmation{}, fmt.Errorf("account: unsupported confirmation purpose %q", pend.purpose)
}
@@ -380,6 +394,27 @@ func (s *Store) confirmedEmailAccount(ctx context.Context, email string) (uuid.U
return row.AccountID, true, nil
}
// confirmedEmailOf returns the account's confirmed email address and true, or ("", false)
// when it holds none. It backs the deletion step-up, which mails a code to the account's
// own address.
func (s *Store) confirmedEmailOf(ctx context.Context, accountID uuid.UUID) (string, bool, error) {
stmt := postgres.SELECT(table.Identities.ExternalID).
FROM(table.Identities).
WHERE(
table.Identities.AccountID.EQ(postgres.UUID(accountID)).
AND(table.Identities.Kind.EQ(postgres.String(KindEmail))).
AND(table.Identities.Confirmed.EQ(postgres.Bool(true))),
).LIMIT(1)
var row model.Identities
if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
if errors.Is(err, qrm.ErrNoRows) {
return "", false, nil
}
return "", false, fmt.Errorf("account: confirmed email of %s: %w", accountID, err)
}
return row.ExternalID, true, nil
}
// replacePendingConfirmation clears any pending code for (accountID, email) and
// inserts a fresh one, inside one transaction.
func (s *Store) replacePendingConfirmation(ctx context.Context, accountID uuid.UUID, email, codeHash, linkTokenHash, purpose string, expiresAt time.Time) error {
+18
View File
@@ -98,6 +98,24 @@ var confirmEmailCopy = map[string]map[string]emailCopy{
FooterIgnore: "Если вы не запрашивали смену, просто проигнорируйте письмо — адрес останется прежним.",
},
},
purposeDelete: {
"en": {
Subject: "Confirm your Erudit account deletion",
Preheader: "Confirm account deletion",
Heading: "Delete your account",
Intro: "Enter this code in the app to permanently delete your account:",
CTALabel: "",
FooterIgnore: "If you didn't request this, ignore it — your account stays as it is.",
},
"ru": {
Subject: "Подтвердите удаление аккаунта Эрудит",
Preheader: "Подтверждение удаления аккаунта",
Heading: "Удаление аккаунта",
Intro: "Введите этот код в приложении, чтобы удалить аккаунт без восстановления:",
CTALabel: "",
FooterIgnore: "Если вы не запрашивали удаление, проигнорируйте письмо — аккаунт останется.",
},
},
}
// emailBrand is the brand wordmark per locale.
+43
View File
@@ -178,6 +178,49 @@ func (s *EmailService) ConfirmChange(ctx context.Context, accountID uuid.UUID, n
return s.store.GetByID(ctx, accountID)
}
// HasEmail reports whether accountID owns a confirmed email. The account-deletion step-up
// mails a confirm-code when it does, and falls back to a typed phrase otherwise.
func (s *EmailService) HasEmail(ctx context.Context, accountID uuid.UUID) (bool, error) {
_, ok, err := s.store.confirmedEmailOf(ctx, accountID)
return ok, err
}
// RequestDeleteCode mails an account-deletion confirm-code to the account's own confirmed
// email (no deeplink — deletion is confirmed in the app). It returns ErrNoEmail when the
// account holds no email, ErrTooManyRequests when throttled.
func (s *EmailService) RequestDeleteCode(ctx context.Context, accountID uuid.UUID) error {
addr, ok, err := s.store.confirmedEmailOf(ctx, accountID)
if err != nil {
return err
}
if !ok {
return ErrNoEmail
}
if !s.allowSend(addr) {
return ErrTooManyRequests
}
return s.issueCode(ctx, accountID, addr, purposeDelete, s.accountLocale(ctx, accountID))
}
// VerifyDeleteCode verifies the account-deletion code against the account's own email and
// consumes it on success. It returns ErrNoEmail (no email), the usual confirm-code errors
// (ErrNoPendingCode, ErrCodeExpired, ErrTooManyAttempts, ErrCodeMismatch), or nil when the
// code is valid — the caller then performs the deletion.
func (s *EmailService) VerifyDeleteCode(ctx context.Context, accountID uuid.UUID, code string) error {
addr, ok, err := s.store.confirmedEmailOf(ctx, accountID)
if err != nil {
return err
}
if !ok {
return ErrNoEmail
}
conf, err := s.verifyPendingCode(ctx, accountID, addr, code)
if err != nil {
return err
}
return s.store.consumeConfirmation(ctx, conf.id, s.now())
}
// verifyPendingCode loads and checks the pending confirm-code for (accountID,
// addr), counting a wrong attempt. It returns the confirmation on success.
func (s *EmailService) verifyPendingCode(ctx context.Context, accountID uuid.UUID, addr, code string) (emailConfirmation, error) {
+46
View File
@@ -5,6 +5,8 @@ package inttest
import (
"context"
"database/sql"
"errors"
"strings"
"testing"
"time"
@@ -133,3 +135,47 @@ func TestDropAllRobotGames(t *testing.T) {
t.Errorf("the human game should be kept, got: %v", err)
}
}
// TestDeleteStepUpEmail: an email account's delete code verifies (wrong code rejected, no
// deeplink in the mail); a platform-only account has no email and cannot request a code.
func TestDeleteStepUpEmail(t *testing.T) {
ctx := context.Background()
store := account.NewStore(testDB)
mailer := &capturingMailer{}
svc := account.NewEmailService(store, mailer, "https://erudit-game.ru")
acc, err := store.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString())
if err != nil {
t.Fatalf("provision: %v", err)
}
if err := store.AttachIdentity(ctx, acc.ID, account.KindEmail, "del-"+uuid.NewString()+"@example.com", true); err != nil {
t.Fatalf("attach email: %v", err)
}
if has, err := svc.HasEmail(ctx, acc.ID); err != nil || !has {
t.Fatalf("HasEmail = (%v, %v), want true", has, err)
}
if err := svc.RequestDeleteCode(ctx, acc.ID); err != nil {
t.Fatalf("request delete code: %v", err)
}
if strings.Contains(mailer.lastBody, "/confirm/") {
t.Error("a delete email must not carry a one-tap deeplink")
}
code := sixDigit.FindString(mailer.lastBody)
if err := svc.VerifyDeleteCode(ctx, acc.ID, "000000"); err == nil {
t.Error("a wrong delete code must be rejected")
}
if err := svc.VerifyDeleteCode(ctx, acc.ID, code); err != nil {
t.Fatalf("verify correct delete code: %v", err)
}
noEmail, err := store.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString())
if err != nil {
t.Fatalf("provision no-email: %v", err)
}
if has, _ := svc.HasEmail(ctx, noEmail.ID); has {
t.Error("HasEmail must be false for a platform-only account")
}
if err := svc.RequestDeleteCode(ctx, noEmail.ID); !errors.Is(err, account.ErrNoEmail) {
t.Errorf("request delete for no-email account = %v, want ErrNoEmail", err)
}
}
+4
View File
@@ -80,6 +80,10 @@ func (s *Server) registerRoutes() {
// refused without disclosure, never merged).
u.POST("/link/email/change/request", s.handleChangeEmailRequest)
u.POST("/link/email/change/confirm", s.handleChangeEmailConfirm)
// Account deletion (legal retention, not erasure): step-up via a mailed code
// (email accounts) or a typed phrase (platform-only), then tombstone + free creds.
u.POST("/delete/request", s.handleRequestDelete)
u.POST("/delete/confirm", s.handleConfirmDelete)
}
if s.games != nil {
u.GET("/games", s.handleListGames)
+131
View File
@@ -0,0 +1,131 @@
package server
import (
"context"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"scrabble/backend/internal/accountdelete"
"scrabble/backend/internal/game"
)
// Account deletion is legal retention, not erasure (docs/ARCHITECTURE.md §9.1): the account
// row survives as a tombstone while its credentials are journalled + freed, its live
// surfaces anonymised, and its own social/ephemeral data dropped. Messages are kept. The
// step-up is a mailed code for an account with a confirmed email, or a typed phrase for a
// platform-only account (possession-proof is unattainable there, so the phrase is
// anti-impulse only).
// deletePhrase is the fixed confirmation phrase a no-email account types to delete.
// Compared case-insensitively; the client localises only the surrounding instruction.
const deletePhrase = "DELETE"
// deleteRequestResponse tells the client which step-up the account uses.
type deleteRequestResponse struct {
Method string `json:"method"` // "email" | "phrase"
}
// handleRequestDelete starts account deletion: it mails a delete code to an account with a
// confirmed email, or reports the typed-phrase path otherwise.
func (s *Server) handleRequestDelete(c *gin.Context) {
uid, ok := userID(c)
if !ok {
abortBadRequest(c, "missing identity")
return
}
ctx := c.Request.Context()
hasEmail, err := s.emails.HasEmail(ctx, uid)
if err != nil {
s.abortErr(c, err)
return
}
if !hasEmail {
c.JSON(http.StatusOK, deleteRequestResponse{Method: "phrase"})
return
}
if err := s.emails.RequestDeleteCode(ctx, uid); err != nil {
s.abortErr(c, err)
return
}
c.JSON(http.StatusOK, deleteRequestResponse{Method: "email"})
}
// deleteConfirmBody carries the step-up proof: a mailed code (email account) or the typed
// phrase (platform-only account).
type deleteConfirmBody struct {
Code string `json:"code"`
Phrase string `json:"phrase"`
}
// handleConfirmDelete verifies the step-up and deletes the account.
func (s *Server) handleConfirmDelete(c *gin.Context) {
uid, ok := userID(c)
if !ok {
abortBadRequest(c, "missing identity")
return
}
var req deleteConfirmBody
if err := c.ShouldBindJSON(&req); err != nil {
abortBadRequest(c, "invalid request body")
return
}
ctx := c.Request.Context()
hasEmail, err := s.emails.HasEmail(ctx, uid)
if err != nil {
s.abortErr(c, err)
return
}
if hasEmail {
if err := s.emails.VerifyDeleteCode(ctx, uid, req.Code); err != nil {
s.abortErr(c, err)
return
}
} else if !strings.EqualFold(strings.TrimSpace(req.Phrase), deletePhrase) {
abortBadRequest(c, "confirmation phrase does not match")
return
}
if err := s.deleteAccount(ctx, uid); err != nil {
s.abortErr(c, err)
return
}
c.JSON(http.StatusOK, okResponse{OK: true})
}
// deleteAccount runs the deletion orchestration after the step-up passed: it resigns the
// account's active games (so opponents are not stranded and robot games end cleanly),
// drops its all-robot games, tombstones + anonymises the account (journalling and freeing
// its credentials), and revokes its sessions. The tombstone is the point of no return —
// its failure aborts; the game cleanup and session revocation around it are best-effort.
func (s *Server) deleteAccount(ctx context.Context, uid uuid.UUID) error {
deleter := accountdelete.NewDeleter(s.db)
if s.games != nil {
if games, err := s.games.ListForAccount(ctx, uid); err == nil {
for _, g := range games {
if g.Status != game.StatusActive {
continue
}
if _, err := s.games.Resign(ctx, g.ID, uid); err != nil {
s.log.Warn("delete: resign game failed", zap.String("game", g.ID.String()), zap.Error(err))
}
}
} else {
s.log.Warn("delete: list games failed", zap.Error(err))
}
}
if _, err := deleter.DropAllRobotGames(ctx, uid); err != nil {
s.log.Warn("delete: drop all-robot games failed", zap.Error(err))
}
if err := deleter.AnonymizeAndTombstone(ctx, uid); err != nil {
return err
}
if s.sessions != nil {
if err := s.sessions.RevokeAllForAccount(ctx, uid); err != nil {
s.log.Warn("delete: revoke sessions failed", zap.Error(err))
}
}
return nil
}
@@ -351,6 +351,26 @@ func (c *Client) ChangeEmailConfirm(ctx context.Context, userID, email, code str
return out, err
}
// DeleteRequestResp reports which account-deletion step-up the account uses.
type DeleteRequestResp struct {
Method string `json:"method"`
}
// DeleteRequest starts account deletion: the backend mails a delete code (email accounts)
// or reports the typed-phrase path.
func (c *Client) DeleteRequest(ctx context.Context, userID string) (DeleteRequestResp, error) {
var out DeleteRequestResp
err := c.do(ctx, http.MethodPost, "/api/v1/user/delete/request", userID, "", nil, &out)
return out, err
}
// DeleteConfirm verifies the step-up (a mailed code or the typed phrase) and deletes the
// account.
func (c *Client) DeleteConfirm(ctx context.Context, userID, code, phrase string) error {
return c.do(ctx, http.MethodPost, "/api/v1/user/delete/confirm", userID, "",
map[string]string{"code": code, "phrase": phrase}, nil)
}
// LinkUnlink detaches a platform identity (kind = "telegram" | "vk") from the caller
// and returns the refreshed profile in the result.
func (c *Client) LinkUnlink(ctx context.Context, userID, kind string) (LinkResultResp, error) {
+11
View File
@@ -37,6 +37,17 @@ func encodeAck(ok bool) []byte {
return b.FinishedBytes()
}
// encodeDeleteRequestResult builds an AccountDeleteRequestResult payload reporting which
// deletion step-up the account uses ("email" | "phrase").
func encodeDeleteRequestResult(method string) []byte {
b := flatbuffers.NewBuilder(32)
m := b.CreateString(method)
fb.AccountDeleteRequestResultStart(b)
fb.AccountDeleteRequestResultAddMethod(b, m)
b.Finish(fb.AccountDeleteRequestResultEnd(b))
return b.FinishedBytes()
}
// encodeConfirmLinkResult builds an EmailConfirmLinkResult payload, embedding the
// minted Session for a login. All strings and the nested Session table are built
// before the result table is opened.
@@ -21,6 +21,8 @@ const (
MsgLinkUnlink = "link.unlink"
MsgEmailChangeRequest = "link.email.change.request"
MsgEmailChangeConfirm = "link.email.change.confirm"
MsgAccountDeleteReq = "account.delete.request"
MsgAccountDeleteConf = "account.delete.confirm"
)
// registerLinkOps adds the linking & merge operations. The telegram ops need the
@@ -32,6 +34,8 @@ func registerLinkOps(r *Registry, backend *backendclient.Client, tg TelegramVali
r.ops[MsgLinkUnlink] = Op{Handler: linkUnlinkHandler(backend), Auth: true}
r.ops[MsgEmailChangeRequest] = Op{Handler: changeEmailRequestHandler(backend), Auth: true, Email: true}
r.ops[MsgEmailChangeConfirm] = Op{Handler: changeEmailConfirmHandler(backend), Auth: true, Email: true}
r.ops[MsgAccountDeleteReq] = Op{Handler: deleteRequestHandler(backend), Auth: true, Email: true}
r.ops[MsgAccountDeleteConf] = Op{Handler: deleteConfirmHandler(backend), Auth: true}
if tg != nil {
r.ops[MsgLinkTelegram] = Op{Handler: linkTelegramHandler(backend, tg, false), Auth: true}
r.ops[MsgLinkTelegramMerge] = Op{Handler: linkTelegramHandler(backend, tg, true), Auth: true}
@@ -94,6 +98,28 @@ func changeEmailConfirmHandler(backend *backendclient.Client) Handler {
}
}
// deleteRequestHandler starts account deletion, returning which step-up the account uses.
func deleteRequestHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
res, err := backend.DeleteRequest(ctx, req.UserID)
if err != nil {
return nil, err
}
return encodeDeleteRequestResult(res.Method), nil
}
}
// deleteConfirmHandler verifies the step-up proof and deletes the account.
func deleteConfirmHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsAccountDeleteConfirm(req.Payload, 0)
if err := backend.DeleteConfirm(ctx, req.UserID, string(in.Code()), string(in.Phrase())); err != nil {
return nil, err
}
return encodeAck(true), nil
}
}
// linkUnlinkHandler detaches a platform identity (telegram|vk) from the caller and
// returns the refreshed link result (status "unlinked" + the updated profile).
func linkUnlinkHandler(backend *backendclient.Client) Handler {
+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()
}
+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);
}
}