Authentication
AuthClient is the SDK 3.0 sign-in client: email+password, OTP code, OAuth (Google/Apple/…), password reset, and anonymous sessions. It persists the session locally (localStorage / chrome.storage.local / memory), refreshes the access token on a timer, and emits onAuthChange on login, refresh, and logout.
import { AuthClient } from '@monetize.software/sdk/core';
const auth = new AuthClient({ paywallId: 'pw_123' });
await auth.ready(); // wait for session hydration from storage
if (auth.getCachedUser()) {
// user is already signed in from a previous session
}Principles
- Single source of truth for the session —
AuthClient. AnyBillingClientaccepts it as an option and automatically attachesAuthorization: Bearer <access>to every API request, plus syncsidentityfromauth.user. - Default persistence —
localStorageon web,chrome.storage.localon extensions (auto-detected), in-memory as a fallback. You can supply your ownStorageAdapter. - Lazy refresh.
getAccessToken()refreshes itself when expiry is less than 60s away. A network error returns the current token (still valid); a 401 on refresh signs the user out (refresh token revoked). - Refresh deduplication. Parallel
getAccessToken()calls share a single inflight refresh request to the backend.
No cookies. SDK 3.0 is deliberately built around Bearer tokens so the same code runs in a Chrome extension, Telegram Mini App and a regular browser without juggling SameSite/partitioned cookies.
Sign-in methods
API at a glance
| Method | What it does | Returns |
|---|---|---|
signInWithEmail({email, password}) | Sign in with password | AuthSession |
signUp({email, password}) | Sign up. With email-confirm enabled returns confirmation_required without a session | SignUpResult |
sendOtp({email}) | Sends a 6-digit code to the email | void |
verifyOtp({email, token, type?}) | Verifies the code → session | AuthSession |
requestPasswordReset({email}) | Recovery email with a code | void |
updatePassword({password}) | Updates the password of the signed-in user | void |
signInWithOAuth({provider}) | OAuth via popup with PKCE | AuthSession |
startOAuthFlow({provider}) / completeOAuthFlow({state, code}) | Low-level split of signInWithOAuth for offscreen-architecture (used by @monetize.software/sdk-extension). Most hosts call signInWithOAuth instead. | {authorize_url, state} / AuthSession |
signInAnonymously({captchaToken?, userMeta?, forceNewAnon?}) | Anonymous sign-in. Idempotent + resumes the same anon user via stored refresh-token. Parallel calls dedupe. | AuthSession |
upgradeAnonymousToEmail({email, password, idempotencyKey?}) | Upgrades an anonymous user to email/password without losing balances. With email-confirm enabled returns confirmation_required. | UpgradeAnonymousResult |
signOut({forgetAnonymous?}) | Logout — clears local state first, then best-effort hits the server | void |
revokeAllSessions() | ”Sign out everywhere” — invalidates ALL of this user’s refresh tokens across every device | void |
resendConfirmation({email}) | Resends the confirmation email after signUp with email-confirm. 429 = rate limit (~1/minute) | void |
refresh() | Forces a refresh — usually unnecessary, refresh is lazy | AuthSession \| null |
getAccessToken() | Bearer token for APIs; lazy refresh near expiry | string \| null |
getCachedSession() / getCachedUser() | Sync snapshot, no network | AuthSession \| AuthUser \| null |
getLastLogin() | Last login method + email from storage. Useful for UI prefill (“Continue as user@example.com via Google”). | LastLogin \| null |
onAuthChange(cb) | Subscription with a discriminated event: INITIAL_SESSION (always first), SIGNED_IN/SIGNED_OUT/TOKEN_REFRESHED/USER_UPDATED/PASSWORD_RECOVERY. Callback: (event, session) => void | unsubscribe |
ready() | Promise that resolves when the session has hydrated from storage | Promise<void> |
destroy() / isDestroyed() | Tears down storage subscriptions and clears listeners. Call on host teardown (SPA route change, hot reload, tests). | void / boolean |
Built-in login screen (gate a feature behind sign-in): paywall.openSignin()
You don’t need to build your own login UI. open() always shows the price layout, but PaywallUI also exposes dedicated handles that open the modal straight on the built-in auth screen — email/password + OAuth buttons (per settings.auth_providers), fully styled, localized, in Shadow DOM. No custom form, no provider-icon library needed.
import { PaywallUI } from '@monetize.software/sdk';
const paywall = new PaywallUI({ paywallId: 'pw_123', auth: true });
// Gate a feature behind sign-in — login only, no plans:
document.getElementById('login').onclick = () => paywall.openSignin();
// New-account flow (starts on the signup form):
document.getElementById('register').onclick = () => paywall.openSignup();Use this whenever sign-in is a precondition rather than a purchase prompt:
- A returning customer already bought and just needs to log in so the SDK picks up their subscription.
- You want to require login before unlocking a feature, while keeping
open()reserved for the Upgrade/plans flow.
| Method | Opens on | Notes |
|---|---|---|
openSignin() | signin form | The explicit name. Use for “log in to gate a feature”. |
openSignup() | signup form | Respects allow_signup from the paywall layout — if signup is disabled, falls back to signin. |
openAuth() | signin form | Alias of openSignin(), kept for backwards compatibility. |
After successful sign-in the modal closes; Back also closes it (the user came only to log in). The trial doesn’t block these flows. Without auth connected (no managed-auth) they’re a no-op — there’s no one to sign in. Symmetric to openSupport() — a short path to a single task without the full paywall flow.
Wiring with BillingClient
Pass auth to BillingClient — the token and identity will be attached automatically, no getAuthToken callback needed.
import { AuthClient, BillingClient } from '@monetize.software/sdk/core';
const auth = new AuthClient({ paywallId: 'pw_123' });
const billing = new BillingClient({ paywallId: 'pw_123', auth });
await billing.bootstrap(); // identity = auth.user; Bearer attached
const user = await billing.getUser();If you set identity explicitly on BillingClient — after the first onAuthChange event (including INITIAL_SESSION on hydration) auto-sync overwrites it with auth.user. Don’t mix both approaches on the same instance.