Pular para o conteúdo principal

@cactus-agents/accounts

@cactus-agents/accounts@2.9.0 — autenticação, conta do usuário e carteira em um único pacote. É o pacote mais consumido do SDK pelo front-web-base.

Este pacote substituiu @cactus-agents/auth, @cactus-agents/user e @cactus-agents/wallet, que foram removidos do monorepo (core 452450e, 2026-04-17; unificação em eac3812). Os três não existem mais no registry — pnpm add @cactus-agents/auth falha.

Dois entry points

ImportConteúdoDepende de React?
@cactus-agents/accountsServices HTTP (auth, user, wallet, recovery), transforms e regras purasNão
@cactus-agents/accounts/reactStore Zustand useAccountsStore, hooks e initAccounts()Sim

Esta página cobre o root. A camada React tem página própria: accounts/react.

Peer deps (ambas opcionais no package.json, exigidas só pelo subpath /react): react >= 18, zustand >= 4.

Instalação

pnpm add @cactus-agents/accounts

No front-web-base já é dependência declarada — não precisa instalar.

Migração dos pacotes antigos

Todo símbolo que vivia em auth/user/wallet continua exportado com o mesmo nome; só o pacote de origem mudou. Basta reescrever o import.

AntesAgora
import { createAuthFromClient } from "@cactus-agents/auth"import { createAuthFromClient } from "@cactus-agents/accounts"
import { createUserFromClient } from "@cactus-agents/user"import { createUserFromClient } from "@cactus-agents/accounts"
import { createWalletFromClient, fetchWalletData } from "@cactus-agents/wallet"import { createWalletFromClient, fetchWalletData } from "@cactus-agents/accounts"

Símbolos que a doc antiga atribuía a auth/user/wallet e hoje vivem aqui:

SímboloAntigo pacoteArquivo atual
AuthService, AuthUser, AuthUserInfo, LoginPayload, LoginResponse, ForgotPasswordPayload, ValidateDocumentPayload/Response, RefreshTokenResponse, ForceRequestKycReasonauthsrc/types/auth.ts
shouldRefreshToken, markTokenSaved, markRefreshStarted, markRefreshCompleted, initialRefreshState, TokenRefreshStateauthsrc/refresh.ts
UserService, UserFetcher, UserLimits, UpdateLimitsPayload, SelfExclusionPayload, TimeoutLimitsPayload, IncomeReport*usersrc/types/user.ts
LoginHistoryItem, LoginHistoryResponse, transformLoginHistoryItem, transformLoginHistoryResponseusersrc/login-history.ts
TIMEOUT_DAYS_OPTIONS, SELF_EXCLUSION_MONTHS_OPTIONS, LIMIT_PERIOD, parseLimitPeriod, isoDurationToHours, hoursToIsoDuration, isPermanentExclusion, isLimitChangeBlocked, getLimitChangeUnblocksAt, filterSelfExclusionOptions, filterTimeoutOptions, getMinSelfExclusionMonths, getMinTimeoutDaysusersrc/responsible-gaming.ts
getAccountRestriction, getRestrictionType, isAccountRestricted, SPA_EXCLUSION_CODES, AccountRestrictionusersrc/account-restriction.ts
WalletService, WalletFetcher, Wallet, RealWallet, BonusWallet, CashbackWallet, Rollover, Transaction*, WalletDatawalletsrc/types/wallet.ts
fetchWalletData, aggregateWalletData, transformWalletResponse, transformTransaction, transformTransactionResponse, transformRolloverwalletsrc/wallet-service.ts, src/wallet-transform.ts

Uso básico

Cada domínio tem um par createXFromClient(client) (recomendado, aceita ApiClient) e createXService(fetcher) (fetcher manual).

import { createApiClient } from "@cactus-agents/api-client";
import {
createAuthFromClient,
createUserFromClient,
createWalletFromClient,
createRecoveryFromClient,
fetchWalletData,
} from "@cactus-agents/accounts";

const client = createApiClient({ baseUrl, tenant, language });

const auth = createAuthFromClient(client);
const user = createUserFromClient(client);
const wallet = createWalletFromClient(client);
const recovery = createRecoveryFromClient(client);

const session = await auth.login({ login: "user@email.com", password: "senha123" });
const walletData = await fetchWalletData(wallet);

Fetcher manual — note que UserFetcher exige patch e delete além de get/post (os outros três exigem só get/post):

import { createUserService } from "@cactus-agents/accounts";

const user = createUserService({
get: (path) => myHttpClient.get(path),
post: (path, body) => myHttpClient.post(path, body),
patch: (path, body) => myHttpClient.patch(path, body),
delete: (path) => myHttpClient.delete(path),
});

AuthService

MétodoVerboEndpoint
login(payload)POST/auth/login
logout()POST/auth/logout
logoutAuto()POST/auth/logout-auto
getUserProfile(options?)GET/auth/user-profile
refreshToken()POST/users/refresh-token
registerSimplified(payload)POST/bff/register-simplified
forgotPassword(payload)POST/auth/passwords/reset/options
validateDocument(payload)POST/documents/validate

:::warning logoutAuto() existe, mas não está ligado em ponta nenhuma A intenção de logoutAuto() é o logout forçado (401/440 vindos do BFF): bater em /auth/logout-auto para o BFF distinguir, no audit log, um sign-out iniciado pelo usuário de uma sessão terminada pelo servidor. O comportamento no front seria idêntico ao de logout().

Na prática, hoje, nada chama esse método — em três camadas:

  1. O método é real e funciona (src/auth-service.ts, com teste em src/__tests__/auth-service.test.ts). Só é alcançável via o AuthService do root.
  2. A camada React tem um adapter defaultLogoutAuto() (src/react/auth-http.ts) apontando para /api/auth/logout-auto, mas ele não é exportado do barrel /react e não tem nenhum call site — é código morto.
  3. AccountsConfig só tem o seam logout?não existe logoutAuto?. Ou seja, nem dá para ligar o fluxo automático via initAccounts().

E no front-web-base não existe rota /api/auth/logout-auto: o caminho de 401/440 termina em POST /api/auth/logout, que chama o logout() comum. Consequência prática: o audit log do BFF não distingue logout forçado de voluntário hoje.

Usar isso de verdade exige criar a rota de proxy no base e chamar logoutAuto() de lá (ou abrir um seam novo no core). Não escreva código assumindo que o caminho já existe. :::

:::caution forgotPassword() é @deprecated Use RecoveryService.getOptions(). O endpoint é o mesmo, mas a API espera document (não login) — e o RecoveryService cobre o fluxo multi-step completo. :::

login(payload)

interface LoginPayload {
login: string; // email ou documento
password: string;
captcha_token?: string;
app_source?: string;
two_factor_code?: number;
/** Token de device-fingerprint da Incognia, gerado client-side antes do submit.
* Só brands que integram Incognia enviam. */
icg_token?: string;
}

interface LoginResponse {
access_token: string;
expires_in: number;
token_type: string;
user: AuthUser;
userInfo: AuthUserInfo;
type?: "cancelled_account" | "two_factor_code";
}

getUserProfile(options?)

interface GetUserProfileOptions {
/** Anexa `?check_spa_again=1` — dispara consulta SIGAP síncrona no BFF.
* Só em ação explícita do usuário, NUNCA em fetch de rotina. Default: false. */
checkSpaAgain?: boolean;
/** Anexa `?icg_token=...` para correlacionar o fetch com o fingerprint do login. */
icgToken?: string;
}

Retorna { user: AuthUser, userInfo: AuthUserInfo }.

registerSimplified(payload)

interface RegisterSimplifiedPayload {
email: string;
password: string;
country: string;
nationalities: string; // JSON string
optIn: boolean;
ddi?: string;
phone?: string;
name?: string;
birth_date?: string;
number?: string; // documento
captcha_token?: string;
app_source?: string;
affiliation_code?: string;
}

Retorna LoginResponse.

:::note Campos de tracking de marketing UTMs, src e ga_client_id não fazem parte de RegisterSimplifiedPayload no tipo do core — o front-web-base os anexa ao body no próprio fluxo de registro, a partir do cookie de tracking. Política de captura e atribuição em marketing-tracking. :::

AuthUser / AuthUserInfo

Shapes completos em packages/accounts/src/types/auth.ts. Pontos que costumam pegar quem lê só o nome do campo:

  • AuthUser.cactus_id?: string — identificador de plataforma (string), distinto do id numérico. Usado por integrações 3rd-party que precisam de id estável. Opcional — contas legadas podem não ter.
  • AuthUser._cached?: AuthUserCached — blocos agregados pelo BFF no /auth/user-profile, cada um atrás de flag/try-catch, todos opcionais. Só get-last-deposit é tipado; os demais ficam como unknown.
  • AuthUserInfo.preferencies?: UserPreferences — sim, preferencies. É o contrato do BFF; não "corrija" o typo.
  • AuthUserInfo carrega os limites de responsible gaming (user_limit_*, user_pause*, user_exclusion*), as exclusões read-only de operador/SPA (operator_exclusion*, spa_exclusion*) e os limites prudenciais spa_limit_deposit / spa_limit_bet / spa_limit_loss.

RecoveryService

Fluxo multi-step de recuperação de senha.

MétodoVerboEndpoint
getOptions(payload)POST/auth/passwords/reset/options
sendEmail(payload)POST/auth/passwords/reset/by-email
sendSms(payload)POST/auth/passwords/reset/by-sms
validateCode(payload)POST/auth/passwords/reset/validate-code
confirmReset(payload)POST/auth/passwords/reset/confirm
interface RecoveryOptionsPayload {
/** Exatamente UM de document/email/phone/recovery_token. O front garante a
* exclusividade mútua — o BFF rejeita payload ambíguo. */
document?: string;
email?: string;
phone?: string; // só dígitos, sem DDI
recovery_token?: string;
captcha_token?: string;
}

interface RecoveryOptionsResponse {
token: string; // token temporário usado em todo o fluxo
email: string; // mascarado: "p*****@email.com"
final_phone: string; // últimos dígitos: "7766"
kyc?: boolean;
active_options?: RecoveryActiveOptions;
}

:::caution active_options não tem default permissivo RecoveryActiveOptions (feature_email_password_recovery, feature_phone_password_recovery, feature_kyc_password_recovery) vem da config de backend da brand. Se o BFF omitir o objeto, o fluxo não renderiza nenhuma opção — o front não deve assumir "tudo liberado". :::

UserService

Perfil e endereço

MétodoVerboEndpoint
updateProfile(id, data)POST/users/update/{id}
getAddress()GET/bff/users/address-by-user
updateEmail(data)PATCH/bff/users/self-email
addPhone(data)PATCH/bff/users/add-phone
updateAddress(data)PATCH/bff/users/update-address
lookupByPostalCode(postalCode, userId?)POST/apicep
storeDocument(data)POST/documents/{endpoint}
updateContracts(type)PATCH/bff/users/self-contracts
updateMarketing(data)PATCH/bff/users/self-mkt
updateUserPreferences(data)PATCH/bff/users/update-user-info-metadata

updateAddress espera address (rua + número + complemento), não street:

interface UpdateAddressPayload {
address: string;
city: string;
state: string;
zipcode: string;
country?: string;
ddi?: string;
captcha_token?: string;
password_check?: string;
kyc_id?: number;
}

storeDocument monta o path a partir do próprio payload — o campo endpoint é retirado do body antes do POST:

interface StoreDocumentPayload {
number: string; // raw, sem formatação
captcha_token: string;
endpoint: "store-user-document" | "store-mex-document";
}
  • "store-user-document" → CPF (BRA)
  • "store-mex-document" → os demais documentos (RUT, CURP, DNI, genérico)

O valor certo vem de DocumentConfig.storeEndpoint no country-config.

updateContracts(type) envia { type } com ContractType = "tc" | "privacy" | "lgpd" | "law" | "migrate" | "mkt".

updateMarketing(data) recebe { mkt_accepted_at: string | null } — datetime "YYYY-MM-DD HH:MM:SS" ao aceitar, null ao revogar.

Preferências do usuário — ALLOWED_PREFERENCES

PATCH /bff/users/update-user-info-metadata é um endpoint campo livre: o BFF persiste qualquer JSON sob preferencies. O whitelist do que pode ser gravado vive no core, em src/types/user.ts:

export const LANDING_PAGE_OPTIONS = ["home", "casino", "casino_live", "sports", "sports_live"] as const;

/** Allow-list estrito: chave de preferência → conjunto de valores aceitos. */
export const ALLOWED_PREFERENCES = {
landingPage: LANDING_PAGE_OPTIONS,
} as const;

interface UserPreferences {
landingPage?: LandingPagePreference;
}

interface UpdateUserPreferencesPayload {
preferencies: UserPreferences; // nome do campo é contrato do BFF (sic)
}

:::caution O base nunca conhece o path nem a lista Essa é a regra fixada no CLAUDE.md do workspace: para endpoints "campo livre" do BFF, o whitelist é do core. Adicionar uma preferência nova exige entrada em ALLOWED_PREFERENCES + campo em UserPreferences + bump do core. O front-web-base não deve replicar a lista nem chamar o path do BFF direto. :::

Segurança

MétodoVerboEndpoint
changePassword(id, data)POST/users/change-password/{id}
checkPassword(data)POST/bff/users/check-password
toggleTwoFactor(data)PATCH/bff/users/self-two-factor
getSocialAccounts()GET/bff/users/account-social
connectSocial(type)POST/bff/users/account-social/connect/{type}
disconnectSocial(id)DELETE/bff/users/account-social/{id}
getLoginHistory(page?)GET/bff/users/login-history?page={page}
interface ChangePasswordPayload {
current_password: string;
new_password: string;
new_password_confirmation: string;
captcha_token?: string;
kyc_id?: number;
/** Token emitido por /bff/users/check-password quando o módulo
* `password` do fluxo `updateData` está ativo. */
password_check?: string;
}

checkPassword retorna { success: boolean }.

getLoginHistory já devolve dados normalizados (via transformLoginHistoryResponse) — o template nunca vê o shape raw:

interface LoginHistoryItem {
userId: number;
ip: string | null;
location: string | null; // "São Paulo, SP" — city + state compostos
createdAt: string; // ISO 8601: "2026-03-17T18:14:42"
latitude: string | null;
longitude: string | null;
}

interface LoginHistoryResponse {
data: LoginHistoryItem[];
currentPage: number;
lastPage: number;
perPage: number;
total: number;
}

Histórico e carteira (via UserService)

MétodoVerboEndpoint
getLastCasinoGames()GET/bff/games/user-last-casino-games-dl
getWallet()GET/users/wallet
getRollover()GET/bonus/rollover
checkRolloverAccomplished()GET/bonus/rollover-accomplished

getLastCasinoGames() mora sob /bff/games/ por razões históricas de roteamento do BFF, mas é estado por usuário (exige auth). Retorna LastCasinoGame[] já transformado ({ gameId, slug, name, image }, slug no formato "provider/game").

Responsible gaming

MétodoVerboEndpoint
updateLimits(data)PATCH/bff/users/update-limits
setTimeoutLimits(data)PATCH/bff/users/timeout-limits
setSelfExclusion(data)PATCH/bff/users/self-exclusion
interface UpdateLimitsPayload {
user_limit_deposit?: number;
user_limit_deposit_active?: number;
user_limit_deposit_period?: string;
user_limit_bet?: number;
user_limit_bet_active?: number;
user_limit_bet_period?: string;
user_limit_loss?: number;
user_limit_loss_active?: number;
user_limit_loss_period?: string;
user_limit_time?: number;
user_limit_time_active?: number;
user_limit_time_period?: string;
kyc_id?: number;
password_check?: string;
}

interface TimeoutLimitsPayload { user_pause: number; reason: number; kyc_id?: number; password_check?: string }
interface SelfExclusionPayload { user_exclusion: number; reason: number; kyc_id?: number; password_check?: string }

Income report (IRPF)

MétodoVerboEndpoint
generateIncomeReport(data)POST/income-report/generate
getIncomeReportStatus(id)GET/income-report/{id}
getAvailableYears()GET/income-report/available-years
interface IncomeReportData {
id: number;
reference_year: number;
status: 1 | 2 | 3 | 4;
delivery_type: "email" | "download";
requested_at: string;
generated_at: string | null;
path: string | null;
download_url: string | null;
}

getIncomeReportStatus é usado em polling até o status mudar.

WalletService

MétodoVerboEndpoint
getWallet()GET/users/wallet
getTransactions(filter?)POST/bff/transactions
getCashbackTransactions(page?, datePeriod?)GET/transactions/cashback
getRollover()GET/bonus/rollover
checkRolloverAccomplished()GET/bonus/rollover-accomplished
transferBonus()POST/bonus/transfer
transferCashback()POST/cashback/transfer
getWithdrawReceipt(id)GET/withdraw/{id}/generate

fetchWalletData(service) — otimização de rollover

Use sempre fetchWalletData em vez de orquestrar as chamadas à mão:

  1. busca getWallet() primeiro;
  2. checa se a carteira de bônus tem saldo (bonus_wallet_result.credit > 0);
  3. checa expiração (expiry_datetime no futuro);
  4. só chama getRollover() + checkRolloverAccomplished() quando o bônus está ativo — e cada uma em try/catch individual, com default em caso de erro;
  5. se o bônus está vazio/expirado, devolve defaults via aggregateWalletData.

Isso evita os 400 que o backend devolve para rollover indefinido e corta requests inúteis.

import { createWalletFromClient, fetchWalletData } from "@cactus-agents/accounts";

const data = await fetchWalletData(createWalletFromClient(client));
// data.wallet.real.credit
// data.rollover.accomplished

WalletData é { wallet: Wallet; rollover: Rollover }, com Wallet = { real, bonus, cashback }.

Notas de transform

  • transformWalletResponse converte os valores da carteira de centavos para unidade monetária (/ 100).
  • transformTransactionResponse converte a paginação para camelCase e preserva o payload original em _raw (client-side, para dev tooling).
  • transformRollover normaliza first_transactionfirstTransaction e aplica accomplished.
  • normalizeTransactionStatus cobre também as variantes de falha v3 (failed, integration_failure, provider_rejected), distintas de denied (rejeição manual).
  • TransactionFilterType aceita "full" como "sem filtro" explícito. Valores fora da união fazem o BFF devolver a lista não filtrada silenciosamente.

Display de transação

buildDisplay(raw, hint?) devolve uma projeção pronta para renderizar, mantendo o componente burro:

interface TransactionDisplay {
titleKey: string; // "wallet.title.casino_bet"
titleParams?: Record<string, string>; // { game: "Fortune Tiger" }
details: TransactionDetail[]; // [{ labelKey, value }]
}

Helpers relacionados: buildTitle, buildDetails, deriveCategory (deriva a TransactionCategory semântica a partir de raw.src + direção) e parseProviderAndGame (extrai [PROVIDER] [GAME] de player_desc).

Regras puras (sem HTTP)

Responsible gaming

const TIMEOUT_DAYS_OPTIONS = [1, 3, 7, 14, 30, 45] as const;
const SELF_EXCLUSION_MONTHS_OPTIONS = [3, 6, 12, 24, 36, 60, -1] as const; // -1 = permanente
const LIMIT_PERIOD = { DAILY: 1, WEEKLY: 2, MONTHLY: 3, YEARLY: 4 } as const;
HelperComportamento
parseLimitPeriod(value)"daily"/11, "weekly"2, … Fallback DAILY para valor desconhecido
isoDurationToHours(raw)"PT4H"4, 44, inválido → null
hoursToIsoDuration(hours)4"PT4H"
isPermanentExclusion(months)true para -1 ou >= 99
isLimitChangeBlocked(changeAt)true quando o limite mudou nas últimas 24h (cooldown)
getLimitChangeUnblocksAt(changeAt)ISO de quando o cooldown expira, ou null
getMinSelfExclusionMonths(input) / getMinTimeoutDays(input)Mínimo selecionável a partir do estado atual (*_min da API, fallback no valor vigente)
filterSelfExclusionOptions(input) / filterTimeoutOptions(input)Filtram as constantes para períodos >= mínimo — o usuário pode estender, nunca reduzir

RestrictionMinInput é um Pick de AuthUserInfo (user_exclusion, user_exclusion_min, user_pause, user_pause_min), então dá para passar o userInfo inteiro.

import {
filterSelfExclusionOptions,
filterTimeoutOptions,
isoDurationToHours,
parseLimitPeriod,
isPermanentExclusion,
} from "@cactus-agents/accounts";

const currentHours = isoDurationToHours(userInfo.user_limit_time);
const periodCode = parseLimitPeriod(userInfo.user_limit_deposit_period);
const isPermanent = isPermanentExclusion(userInfo.user_exclusion ?? 0);
const exclusionOptions = filterSelfExclusionOptions(userInfo);
const timeoutOptions = filterTimeoutOptions(userInfo);

Account restriction

Detecção centralizada de conta restrita (modo "só saques").

isAccountRestricted(userInfo); // true quando qualquer restrição está ativa
getRestrictionType(userInfo); // "spa_exclusion" | "operator_exclusion" | "self_exclusion" | "timeout" | null

const r = getAccountRestriction(userInfo);
// r.type, r.isPermanent, r.endsAt, r.activatedAt, r.note, r.spaCode, r.months, r.days

Prioridade (paridade com o legado): SPA > Operator > Self-exclusion > Timeout.

const SPA_EXCLUSION_CODES = {
PERMANENT: 99, // exclusão permanente pelo regulador
SOCIAL_BENEFIT: 102, // beneficiário social (ex: IN 22/24 no Brasil)
CENTRALIZED: 103, // autoexclusão centralizada via plataforma governamental
} as const;

Token refresh

Funções puras para o ciclo de refresh (intervalo de 6h):

import {
initialRefreshState,
markRefreshCompleted,
markRefreshStarted,
markTokenSaved,
shouldRefreshToken,
} from "@cactus-agents/accounts";

let state = markTokenSaved();

if (shouldRefreshToken(state)) {
state = markRefreshStarted(state);
try {
await authService.refreshToken();
state = markRefreshCompleted(state, true);
} catch {
state = markRefreshCompleted(state, false);
}
}

:::note Não confundir com o refresh-on-401 do api-client Este módulo é o refresh proativo por tempo. O retry automático em resposta a 401 vive no api-client (enableRefreshRetry / refreshTokenFn). :::

Último depósito

getLastDepositAmount(user); // number | null

Lê o bloco _cached["get-last-deposit"] do user-profile. O BFF envia amount em centavos; a conversão para unidades da moeda acontece dentro do helper — callers sempre recebem unidades. Retorna null quando o bloco não existe (flag do tenant desligada), quando success é false, quando não há depósito ou quando o amount não é numérico positivo.

Tipos

O barrel src/index.ts exporta a superfície completa; os shapes vivem em:

ArquivoConteúdo
src/types/auth.tsAuthUser, AuthUserInfo, payloads de login/registro/recovery, AuthService, RecoveryService
src/types/user.tsPayloads de perfil/segurança/limites, UserService, UserLimits, income report, ALLOWED_PREFERENCES
src/types/wallet.tsWallet e derivados, Rollover, Transaction*, WalletService
src/types/coins.tsCoinId, CoinDef, CoinsConfig (camada dual-currency — ver accounts/react)