Appearance
Security & Architecture
The Platform SDK is built with casino-grade security protocols in mind. This page explains how the SDK secures sessions, how it behaves when tokens expire or the network misbehaves, and what your backend must enforce.
API Key & Origin Verification
Every request made by the SDK automatically includes an x-platform-key header containing your API Key.
What origin/CORS verification actually protects (honest scope): because frontend applications are inherently public, your API Key is public knowledge. Verifying the Origin header / CORS policy on the backend prevents browsers on other domains from making authenticated browser requests with the key. It does not prevent theft of the key itself: non-browser clients (curl, servers, native apps, mobile webviews) bypass CORS entirely. Treat the API key as an identifier, not a secret; real authorization comes from the per-user session tokens below, and your backend should apply its own abuse controls (rate limiting, per-key quotas).
Token Management Strategy
The SDK uses a dual-token system to secure user sessions:
- Access Token: short-lived (15 minutes, operator-configurable). Held in SDK memory only. Used to authorize all
/v1/API requests. - Refresh Token: long-lived (7 days, operator-configurable). Stored in
localStorage(environment-namespaced key; see Environments) and used exclusively to obtain new access tokens.
The honest XSS trade-off: a refresh token in localStorage can be stolen by any script running on your origin, including injected scripts. The SDK accepts this trade-off (it is what makes sessions survive page refresh) and compensates so that a stolen token quickly becomes useless:
- Rotation: every refresh invalidates the previous refresh token. A stolen refresh token dies as soon as the legitimate holder refreshes.
- Reuse detection → family revocation: if the backend receives an already-used (dead) refresh token, it revokes the entire session family (every descendant of the original login) and the SDK fires
session-expired. A stolen token being used is therefore a detectable, self-limiting event, not a silent permanent compromise. - Short access-token TTL: limits the blast radius of a memory-scraping attack.
- Operator-configurable 2FA and lockout (below) further bound brute-force risk.
In short: a refresh token in localStorage can be stolen; the design goal is that a stolen token quickly becomes useless, and that its use is detected.
Why tokens, not cookies
The SDK stores tokens rather than relying on browser cookies for three reasons:
- White-label, multi-domain deployments. Operator frontends run on their own domains; there is no shared cookie domain to anchor sessions on. Tokens work on any origin the operator controls.
- CSRF immunity. Tokens are attached to requests by the SDK, not sent automatically by the browser; cross-site request forgery has nothing to ride on.
- The XSS trade-off is mitigated, not ignored. Cookie-based storage has its own XSS exposure (and adds CSRF surface to manage). Here, the short access-token lifetime, rotation, reuse detection, and family revocation bound what a script-injection attack can achieve, and make any use of a stolen refresh token a detectable, self-limiting event.
Re-authentication for sensitive actions
Some account changes are dangerous enough to require proof of the current password, even in an authenticated session:
enable2FA(currentPassword)anddisable2FA(code, currentPassword); see Two-Factor AuthchangePassword({ currentPassword, newPassword }); see Password Recovery & Change- Changing the account email (same flow shape: current password + verified new address)
A wrong current password surfaces AUTH_INVALID_CREDENTIALS (401); the same typed error as a wrong login, never a bespoke message.
text
GIVEN an authenticated session
WHEN a re-auth-required action is called with the wrong current password
THEN the call rejects with AUTH_INVALID_CREDENTIALS and NOTHING changes.Breached-password screening: passwords are checked against a database of known-compromised passwords at registration and on every password change:
text
GIVEN a register or changePassword submission
WHEN the password is on the breached-password list
THEN it is rejected with typed AUTH_VALIDATION, and the user is told to
choose a different password.The check runs as a k-anonymity range query; only a short hash prefix leaves the platform; the password itself never does.
Email & phone verification
Accounts verify their contact channels with one-time codes delivered by email (and SMS, where a phone is registered). The User object carries emailVerified / phoneVerified flags.
text
GIVEN an unverified email or phone on the account
WHEN the player requests a verification code and enters it correctly
THEN the corresponding verified flag flips to true and onAuthStateChanged
fires with the updated user.Re-verification follows the same flow after an email change (see below). Verification codes are single-use, expire (default 10 minutes), and are attempt-capped per the platform's lockout policy.
| Method | Returns | Description |
|---|---|---|
sendVerificationCode(channel) | Promise<void> | Request a code for the given channel ('EMAIL' | 'PHONE') |
verifyEmail(code) | Promise<User> | Verify the account email; flips emailVerified |
verifyPhone(code) | Promise<User> | Verify the account phone; flips phoneVerified |
Session management
Players can see and end their own sessions across devices:
| Member | Returns | Description |
|---|---|---|
listSessions() | Promise<DeviceSession[]> | The account's active sessions, newest activity first |
revokeSession(id) | Promise<void> | End one session (its tabs fire session-expired) |
revokeAllOtherSessions() | Promise<void> | End every session except the current one |
typescript
interface DeviceSession {
id: string; // opaque session identifier
device: string; // e.g. 'Chrome on Windows'; derived from the user agent
location?: string; // approximate, when available
createdAt: string; // ISO-8601
lastActiveAt: string; // ISO-8601
current: boolean; // true for the session making the call
}text
GIVEN the account's sessions
WHEN revokeSession(id) or revokeAllOtherSessions() is called
THEN the targeted sessions end server-side, their tabs fire session-expired,
and listSessions() no longer returns them.Errors: unknown session ID → AUTH_SESSION_NOT_FOUND (added to the error taxonomy). Normal logout still revokes only the current session's tokens (see Session Lifecycle).
Changing the account email
javascript
await sdk.auth.changeEmail({ currentPassword, newEmail });Changing the email is a re-auth action (current password required) plus a verified handover:
text
GIVEN an authenticated session and the correct current password
WHEN changeEmail({ currentPassword, newEmail }) is called
THEN a verification email is sent to the NEW address, and NOTHING changes yet;
the old email keeps full access until the new one is verified. A new email
that is already registered on the platform rejects with `AUTH_EMAIL_TAKEN`
(same typed error as registration).
GIVEN a pending email change
WHEN the player verifies the new address (code from the verification email)
THEN the account's email becomes the new address, emailVerified flips to true,
and the old address no longer identifies the account at login.Errors: wrong current password → AUTH_INVALID_CREDENTIALS; address already in use → AUTH_EMAIL_TAKEN.
Refresh & 401 Handling
When the access token is expired (or a request returns 401), the SDK will:
- Pause the failed request.
- Single-flight refresh: if a refresh is already in flight, the request waits on that same promise; N parallel 401s produce exactly ONE refresh call. Without single-flight, parallel refreshes under rotation destroy each other.
- Call
/v1/auth/refreshwith a bounded timeout. A single-flight refresh without a timeout would let one hung connection stall every queued request forever. - Retry the original request at most once after a successful refresh. A second
401on retry is terminal (do not loop). - On refresh failure, one rule applies: a token-level failure (invalid, revoked, expired, or reused refresh token) clears the session and fires
session-expired(onAuthStateChanged(null)+ the dedicated event); a transport failure (timeout, network) retains the session and surfaces a typedTimeoutError/NetworkErrorto every queued request; a flaky network never logs users out.
401 vs 403 semantics: 401 means "unauthenticated / token expired" → refresh may be attempted. 403 means "authenticated but not permitted" → never triggers a refresh; it is surfaced to the caller immediately as a typed ForbiddenError.
Multi-tab: tabs coordinate over a BroadcastChannel so one shared refresh (and one shared rotation) serves all tabs; a logout or session-expiry in one tab propagates to all tabs.
Proactive Silent Refresh
Beyond the 401 path, the SDK proactively refreshes the access token: before any authenticated call, if the token expires within refreshWindowMs (default 60 s, configurable) plus skew tolerance, the SDK silently refreshes first; long-lived tabs never send a dead token.
Transport Policy
This is the SDK's transport behavior for every request, in every module.
| Behavior | Rule |
|---|---|
| Request timeout | Every HTTP request (including /v1/auth/refresh) uses a bounded timeout via AbortController. Default 15 s, configurable. |
| 5xx retry | Idempotent requests (GET, and idempotency-keyed POSTs) are retried on 5xx/network errors with exponential backoff + jitter, max 2 retries. Non-idempotent POSTs are never auto-retried. |
| 429 handling | On 429, the SDK honors the Retry-After header (seconds) and surfaces a typed RateLimitError carrying retryAfterMs. At most one scheduled auto-retry; otherwise the typed error reaches the caller. |
| Refresh timeout | The refresh call has its own timeout (default 10 s). A timeout counts as refresh failure: if the refresh token is unusable/expired, fire session-expired; otherwise surface a typed network error so callers can retry later. |
| Clock skew | Proactive refresh applies a 30 s skew tolerance; the client refreshes when fewer than refreshWindowMs + 30 s remain, so a slightly-fast client clock never sends a token the server considers dead. |
Rate Limiting & Lockout
The SDK surfaces typed errors; the backend enforces the limits and returns 429 with Retry-After plus the error code. All numeric limits are operator-configurable:
| Surface | Default limit | On exceed |
|---|---|---|
login / verifyLogin2FA | 5 failures / 15 min / account | temporary account lockout; typed AccountLockedError with retryAfterMs |
forgotPassword | 3 requests / hour / identifier | generic success response still returned (anti-enumeration preserved), but no email sent |
| 2FA enable/verify | 5 attempts / 15 min | typed TooManyAttemptsError |
| Reset-token attempts | 10 failures / token | token revoked |
Error Taxonomy
All SDK errors are typed classes with stable code strings; message is display text and is non-normative; branch on code/class only. Cross-module common errors (network, timeout, rate-limit, session-expired, auth, forbidden) are defined once in the Error Reference. Transport failures surface typed errors to the caller; they never fire session-expired and never end the session; only token-level refresh failures do. Auth-specific codes:
| Code | Error class | HTTP status | Trigger condition | Retryable |
|---|---|---|---|---|
AUTH_INVALID_CREDENTIALS | InvalidCredentialsError | 401 | Wrong username/password | Yes (mind lockout) |
AUTH_VALIDATION | AuthValidationError | 400 | Malformed input (email format, password strength, missing fields) or a breached password | No (fix input) |
AUTH_EMAIL_TAKEN | EmailTakenError | 409 | Registration with an existing email | No |
AUTH_USERNAME_TAKEN | UsernameTakenError | 409 | Registration with an existing username | No |
AUTH_2FA_REQUIRED | TwoFactorRequiredError | 401 | Login for a 2FA-enabled account (carries tempToken for the challenge) | No (call verifyLogin2FA) |
AUTH_2FA_INVALID_CODE | InvalidTwoFactorCodeError | 401 | Wrong 6-digit code in the 2FA challenge | Yes (mind attempt limit) |
AUTH_ACCOUNT_LOCKED | AccountLockedError | 429 | Temporary lockout after repeated failures (carries retryAfterMs) | After retryAfterMs |
AUTH_SOCIAL_ACCOUNT_EXISTS | SocialAccountExistsError | 409 | Social login email-collision where linking is not allowed (see Social Login) | No (log in to link the account) |
AUTH_RESET_TOKEN_INVALID | InvalidResetTokenError | 400 | Unknown, expired, or over-attempted reset token | No (request a new one) |
AUTH_SESSION_NOT_FOUND | SessionNotFoundError | 404 | revokeSession with an unknown session ID (see Session management) | No |
AUTH_FORBIDDEN | see Error Reference FORBIDDEN | 403 | Authenticated but not permitted (e.g., KYC-gated action) | No |
RG_SELF_EXCLUDED | SelfExcludedError (owned by Responsible Gaming) | 403 | Login during an active self-exclusion; see Responsible Gaming | No (until the exclusion expires) |
Session Readiness
Session restore on page load is asynchronous. Use the readiness signal before rendering protected routes, and read the token expiry for session-UI:
javascript
const authenticated = await sdk.auth.sessionReady; // true = session restored
sdk.auth.tokenExpiry; // epoch ms the access token expires