Radondocs

API reference

The complete @radonsdk/auth surface — constructor, lifecycle, every free and Pro provider method with real signatures, session helpers, HTTP routes, entities, and error codes.

A complete reference to the @radonsdk/auth public API. Every signature here is the real one. For guided walkthroughs, start with the Quickstart or an auth method.

Provider accessors marked Pro require a verified license — see Radon Pro. A free accessor throws ProviderNotConfiguredError if the provider wasn't enabled; a Pro accessor also throws LicenseRequiredError until auth.init() confirms a license.

new Radon(config)

Creates the SDK instance. createRadon(config) is an equivalent factory. See Configuration for every option in detail.

import { Radon } from "@radonsdk/auth";

const auth = new Radon({
  adapter,                                          // required
  session: { secret: process.env.RADON_SECRET! },   // required
  providers: { /* enabled methods */ },
  appName: "Acme",
  licenseKey: process.env.RADON_LICENSE_KEY,        // Pro
  encryptionKey: process.env.RADON_ENCRYPTION_KEY,  // TOTP
});

Lifecycle & top-level members

auth.init(): Promise<void>

Runs adapter setup (creates tables on SQL adapters) and, if any Pro provider is configured, verifies the license against the license service. Call once on boot. Throws LicenseRequiredError (Pro configured, no key) or LicenseInvalidError (verification failed).

auth.close(): Promise<void>

Adapter teardown (e.g. close connection pools).

auth.createSessionToken(userId, options?): { token: string; expiresAt: Date }

Issue a stateless JWT session token for a user id. options is { claims?: Record<string, unknown>; expiresInSec? }.

auth.verifyToken(token): JwtClaims

Verify a session token and return its claims. Throws TokenInvalidError on a bad signature or expiry. claims.sub is the user id.

auth.getSessionUser(token): Promise<RadonUser>

Verify a token and load the current user from the adapter (sanitized of internal secret fields). Throws TokenInvalidError / UserNotFoundError.

auth.on(event, handler): UnsubscribePro

Subscribe to a lifecycle event. Returns an unsubscribe function.

auth.oauth(name): OAuthProviderPro

Get a configured Pro OAuth provider by the name you keyed it under, e.g. auth.oauth("github").

auth.licensedboolean

Whether the Pro tier is unlocked (a valid license was confirmed in init()).

auth.oauthProvidersstring[]

Names of all configured Pro OAuth providers.

auth.adapterRadonAdapter

The configured storage adapter.

auth.sessionsSessionManager

The stateless JWT session issuer/verifier (free tier). See below.

auth.engineRadonEngine

The low-level engine, for advanced/direct use.

auth.eventsEventBus

The event bus. Emission is always active; subscribing via auth.on is Pro.

Provider accessors

AccessorProviderTier
auth.emailCodeEmail one-time codeFree
auth.magicLinkMagic linkFree
auth.emailPasswordEmail + passwordFree
auth.googleGoogle OAuthFree
auth.oauth(name)A configured OAuth providerPro
auth.phoneOtpPhone / SMS OTPPro
auth.totp2FA / TOTPPro
auth.webauthnPasskeys / WebAuthnPro
auth.refreshRefresh tokensPro
auth.sessionsProviderMulti-device sessions, guests, impersonationPro
auth.apiKeysAPI keysPro
auth.orgsOrgs / teamsPro
auth.accountData export / deletionPro

auth.sessions — stateless JWTs

The SessionManager used by the free tier. Tokens are HS256 JWTs verified without a database read.

auth.sessions.issue(userId, options?): { token: string; expiresAt: Date }

Issue a session token. options is { claims?, expiresInSec? }. auth.createSessionToken delegates here.

auth.sessions.verify(token): JwtClaims

Verify a token and return its claims, applying the configured clockToleranceSec.

auth.emailCode

sendCode({ email, metadata? }): Promise<{ expiresAt: Date }>

Issue a 6-digit code for email and deliver it via the configured sender. metadata is carried on the code and returned to you on verify.

verify({ email, code }): Promise<{ user: RadonUser; created: boolean }>

Verify a submitted code and resolve/create the user by verified email (merge-by-email). created is true for a brand-new signup.

sendLink({ email, redirectTo?, metadata? }): Promise<{ expiresAt: Date; url: string }>

Issue a signed magic link and email it. redirectTo overrides the configured baseUrl for this send (e.g. to carry a post-login path); the signed token is appended as ?token=.

verify(signedToken): Promise<{ user: RadonUser; created: boolean }>

Verify the signed token from the link URL — checks the HMAC signature, then the single-use engine record — and resolve/create the user.

auth.emailPassword

signup({ email, password, metadata? }): Promise<{ user: RadonUser }>

Create an account with an email + password (bcrypt-hashed). Throws EmailExistsError if a password account already exists, WeakPasswordError below minLength.

login({ email, password }): Promise<{ user: RadonUser }>

Authenticate an email + password. Throws InvalidCredentialsError.

requestReset({ email, redirectTo? }): Promise<{ sent: boolean }>

Begin a password reset: email a signed reset token. redirectTo overrides the configured resetUrl for this request.

setPassword({ token, newPassword }): Promise<{ user: RadonUser }>

Verify the signed reset token and set a new password. Revokes the user's existing DB-backed sessions on success.

Login and reset don't reveal who has an account

login returns the same InvalidCredentialsError whether the email is unknown or the password is wrong. requestReset always resolves { sent: false | true } and sends no email for unknown addresses — so neither can be used to enumerate which emails are registered.

auth.google

getAuthUrl({ state?, scopes?, accessType?, prompt?, loginHint? }): string

Build the Google consent URL to redirect the user to. accessType: "offline" requests a refresh token; state is echoed back on the callback for CSRF.

handleCallback(code): Promise<{ user: RadonUser; created: boolean; profile: GoogleProfile }>

Exchange the callback code for tokens, fetch the userinfo, and resolve the profile to a user. Google-verified emails merge onto an existing account.

auth.oauth(name) (Pro)

One generic engine drives all 50 presets. Reach a configured provider with auth.oauth(name).

getAuthUrl({ state?, scopes?, params? }): { url: string; codeVerifier?: string; state?: string }Pro

Build the authorization URL. For PKCE presets a codeVerifier is returned — persist it and pass it back to handleCallback.

handleCallback({ code, codeVerifier? }): Promise<{ user; created; profile; tokens }>Pro

Exchange the code for tokens, fetch/normalize the profile, and resolve the user. profile is a NormalizedProfile; tokens is the raw token response.

auth.phoneOtp (Pro)

sendCode({ phone, metadata? }): Promise<{ expiresAt: Date }>Pro

Issue a numeric code and deliver it via SMS. Codes default to a 5-minute TTL.

verify({ phone, code }): Promise<{ user: RadonUser; created: boolean }>Pro

Verify the code and resolve/create the user keyed by phone number. Phone accounts have no verified email, so they do not merge-by-email.

auth.totp (Pro)

A second factor layered on any primary method. Enroll, then verify on each login.

beginEnrollment(userId): Promise<{ secret: string; uri: string }>Pro

Generate a secret (stored as pending, encrypted at rest) and an otpauth:// URI to render as a QR code. Throws EncryptionRequiredError if no encryptionKey is configured.

confirmEnrollment(userId, code): Promise<{ recoveryCodes: string[] }>Pro

Confirm the user scanned the secret by verifying a live code. Activates 2FA and returns 10 one-time recovery codes — show these once; only hashes are stored.

verify(userId, code): Promise<boolean>Pro

Second-factor check for an enrolled user. Allows ±window steps of drift (default 1 step / ±30s).

verifyRecoveryCode(userId, code): Promise<boolean>Pro

Verify and consume a recovery code. Each works once.

isEnabled(userId): Promise<boolean>Pro

Whether 2FA is active for the user.

regenerateRecoveryCodes(userId): Promise<string[]>Pro

Issue a fresh set of recovery codes, invalidating the old set.

disable(userId): Promise<void>Pro

Disable 2FA and wipe all TOTP state for the user.

auth.webauthn (Pro)

Passkeys. Radon generates challenges and verifies responses with @simplewebauthn/server; the browser performs the ceremony.

startRegistration(userId): Promise<{ options; challenge: string }>Pro

Produce registration options for the browser. Persist the returned challenge.

finishRegistration({ userId, response, expectedChallenge }): Promise<{ credential: StoredCredential }>Pro

Verify the browser's registration response and store the credential.

startAuthentication(userId?): Promise<{ options; challenge: string }>Pro

Produce authentication options. Omit userId for usernameless (discoverable-credential) login.

finishAuthentication({ userId?, response, expectedChallenge }): Promise<{ user: RadonUser }>Pro

Verify the assertion, update the signature counter (replay protection), and return the authenticated user.

listCredentials(userId): Promise<Array<Omit<StoredCredential, "publicKey">>>Pro

List a user's registered passkeys without exposing key material.

removeCredential(userId, credentialId): Promise<void>Pro

Remove a passkey by credential id.

You must persist the challenge between the two calls

Each start* call returns a challenge that its matching finish* call needs as expectedChallenge. Radon does not store it for you — keep it in the session (or a short-lived store) across the two round-trips, or verification fails.

auth.refresh (Pro)

Short-lived access JWT + long-lived opaque refresh token, with rotation on use.

issue({ userId, claims?, device? }): Promise<TokenPair>Pro

Issue a fresh access + refresh pair in a new rotation family.

refresh(refreshToken, { claims?, device? }?): Promise<TokenPair>Pro

Exchange a refresh token for a new pair, rotating (retiring) the old one.

revoke(refreshToken): Promise<void>Pro

Revoke a single refresh token by its plaintext value.

revokeAllForUser(userId): Promise<void>Pro

Revoke every refresh token for a user ("log out everywhere").

TokenPair is { accessToken, accessExpiresAt, refreshToken, refreshExpiresAt, familyId }.

Reuse of a rotated token burns the whole family

Presenting an already-rotated refresh token (a sign of theft/replay) revokes the entire token family and throws RefreshTokenInvalidError. A refresh.reuse_detected event fires so you can alert. This is deliberate — one stolen token can't be quietly replayed.

auth.sessionsProvider (Pro)

Database-backed sessions (listable and individually revocable), guest sessions, and impersonation.

create({ userId, device?, ttlMs? }): Promise<{ token; session }>Pro

Create a DB-backed session carrying device metadata. Returns the opaque token.

listDevices(userId, currentToken?): Promise<DeviceSession[]>Pro

List a user's active sessions with device/timestamp metadata. Pass the current request's token to flag which session is "this device".

revokeDevice(sessionId): Promise<void>Pro

Revoke a single session (one device) by its id.

revoke(token): Promise<void>Pro

Revoke a single session by its opaque token.

revokeAll(userId): Promise<void>Pro

Revoke all of a user's sessions.

createGuest({ device?, metadata? }?): Promise<{ user; token; session }>Pro

Create an anonymous guest (a real user row with no auth method) plus a session.

isGuest(userId): Promise<boolean>Pro

Whether a user is still an unclaimed guest.

upgrade({ userId, provider, providerAccountId, email?, emailVerified? }): Promise<{ user }>Pro

Attach a real identity to a guest, keeping the same user id (and its data).

impersonate(adminUserId, targetUserId, { expiresInSec?, reason? }?): Promise<{ token; expiresAt }>Pro

Mint a session for another user (support/debugging). Defaults to a 1-hour TTL; the token carries imp: true + act: adminUserId claims and emits an impersonation event.

Impersonation does NO authorization check

impersonate mints a fully valid session for any user with no credential check — Radon has no notion of who an admin is. Your app must verify the current actor is an authorized admin before calling it. Misuse is a complete account-takeover primitive.

auth.apiKeys (Pro)

Service-to-service / CLI keys tied to a user and/or org.

create({ userId?, orgId?, name, scopes?, expiresAt?, metadata? }): Promise<{ key; record }>Pro

Create a key. Requires at least one of userId / orgId and a name. The full plaintext key (rk_<prefix>_<secret>) is returned once; only its hash is stored.

verify(presentedKey): Promise<PublicApiKey>Pro

Timing-safe verification, checking expiry and revocation. Updates lastUsedAt. Throws ApiKeyInvalidError.

list({ userId?, orgId? }): Promise<PublicApiKey[]>Pro

List a user's or org's keys (hashes stripped).

revoke(apiKeyId): Promise<void>Pro

Revoke a key by its id.

auth.orgs (Pro)

Multi-tenant orgs with ranked roles (owner > admin > member), email invites, and membership management.

create({ name, slug?, ownerUserId, metadata? }): Promise<{ org; membership }>Pro

Create an org and make ownerUserId its owner. slug auto-derives from name if omitted; a duplicate slug throws OrgError.

get(idOrSlug): Promise<RadonOrg | null>Pro

Fetch an org by id or slug.

listForUser(userId): Promise<RadonOrgMembership[]>Pro

List a user's org memberships.

listMembers(orgId): Promise<RadonOrgMembership[]>Pro

List members of an org.

roleOf(orgId, userId): Promise<OrgRole | null>Pro

A user's role in an org, or null if not a member.

invite({ orgId, email, role?, invitedByUserId? }): Promise<{ invite; token; url? }>Pro

Invite a member by email. If invitedByUserId is given, the inviter must be admin+. Emails the invite when a sender is configured.

acceptInvite(token, userId): Promise<RadonOrgMembership>Pro

Accept an invite: add the signed-in user to the org with the invited role.

addMember(orgId, userId, role?): Promise<RadonOrgMembership>Pro

Directly add a member (no invite). role defaults to member. Caller must authorize.

setRole(orgId, userId, role): Promise<RadonOrgMembership>Pro

Change a member's role. Cannot demote the last owner.

removeMember(orgId, userId): Promise<void>Pro

Remove a member. Cannot remove the last owner.

assertRole(orgId, userId, min): Promise<void>Pro

Throw NotAuthorizedError unless the user has at least min role — use it to gate your own actions.

delete(orgId): Promise<void>Pro

Delete an org and its memberships/invites.

auth.account (Pro)

exportData(userId): Promise<UserDataExport>Pro

Right-to-access: every stored record for a user, with secret material stripped.

deleteUser(userId): Promise<void>Pro

Right-to-be-forgotten: delete the user and cascade to sessions, codes, identities, refresh tokens, API keys, and org memberships.

Events

Subscribe with auth.on(event, handler) (Pro). Handlers are async and isolated — a throwing handler is reported via onEventError but never breaks auth. Emission is always active; with no subscribers it's a no-op.

user.created{ user: RadonUser }

A new user was created.

user.updated{ user: RadonUser }

A user record changed (e.g. a guest was upgraded).

user.deleted{ userId: string }

A user was deleted via account.deleteUser.

session.created{ session: RadonSession; user: RadonUser }

A DB-backed session was created.

session.revoked{ userId: string; sessionId?: string; all?: boolean }

One session, or all of a user's sessions, was revoked.

login{ user: RadonUser; method: string }

A user signed in.

refresh.rotated{ userId: string; familyId: string }

A refresh token was exchanged for a new pair.

refresh.reuse_detected{ userId: string; familyId: string }

A rotated refresh token was replayed; the family was revoked.

apikey.created{ apiKey: RadonApiKey }

An API key was created.

apikey.revoked{ apiKeyId: string }

An API key was revoked.

org.created{ org: RadonOrg }

An org was created.

org.member_added{ membership: RadonOrgMembership }

A member joined an org.

org.member_removed{ orgId: string; userId: string }

A member was removed from an org.

org.invite_sent{ orgId: string; email: string }

An org invite was sent.

impersonation{ adminUserId: string; targetUserId: string }

An admin impersonated a user.

Standalone helpers

Verify a session token anywhere without a Radon instance — same secret:

import { verifyToken } from "@radonsdk/auth";

const claims = verifyToken(cookieValue, process.env.RADON_SECRET!, {
  clockToleranceSec: 0, // optional
});
// claims.sub is the user id

Other exported helpers: multiSender(senders, options?) for email failover; signJwt(claims, secret) / verifyJwt(token, secret, options?) for raw JWT work; bcryptHasher() and sha256Hasher for hashing; and the default email templates defaultCodeTemplate, defaultLinkTemplate, defaultResetTemplate.

HTTP routes

Mounted by every framework integration, relative to your base path (e.g. /api/auth). A successful sign-in sets an HttpOnly, SameSite=Lax cookie named radon_session.

Method & pathBody / queryEffect
POST /email-code/send{ email }Emails a 6-digit code
POST /email-code/verify{ email, code }Signs in → cookie
POST /magic-link/send{ email }Emails a sign-in link
GET /magic-link/verify?token=…Signs in → cookie, redirects
POST /password/signup{ email, password }Creates account → cookie
POST /password/login{ email, password }Signs in → cookie
POST /password/request-reset{ email }Emails a reset link (always 200)
POST /password/reset{ token, newPassword }Sets a new password
GET /google/start?state=…Redirects to Google
GET /google/callback?code=…Signs in → cookie, redirects
POST /logoutClears the session cookie
GET /session{ user } or 401

Errors come back as { error: "<code>", message } with the status from the table below.

Entities

The records adapters store and return.

RadonUser{ id, email, emailVerified, metadata, createdAt, updatedAt }

A user account. email may be null for provider-only accounts with no verified address.

RadonIdentity{ id, userId, provider, providerAccountId, metadata, createdAt }

A linked provider account. One user has many identities — this is what makes merge-by-email possible.

RadonSession{ id, userId, tokenHash, expiresAt, metadata, createdAt }

A DB-backed session. Only the token hash is stored.

RadonOneTimeCode{ id, identifier, codeHash, purpose, expiresAt, attempts, consumedAt, metadata, createdAt }

A one-time code. Only the hash is stored; consumedAt enforces single use.

RadonRefreshToken{ id, userId, tokenHash, familyId, expiresAt, usedAt, revokedAt, device, metadata, createdAt }Pro

A refresh token in a rotation family.

RadonApiKey{ id, userId, orgId, prefix, keyHash, name, scopes, expiresAt, lastUsedAt, revokedAt, metadata, createdAt }Pro

An API key. PublicApiKey is the same shape with keyHash omitted.

RadonOrg{ id, name, slug, metadata, createdAt, updatedAt }Pro

An organization / workspace / team.

RadonOrgMembership{ id, orgId, userId, role, metadata, createdAt }Pro

A user's membership; role is OrgRole ("owner" | "admin" | "member").

RadonOrgInvite{ id, orgId, email, role, tokenHash, invitedByUserId, expiresAt, acceptedAt, metadata, createdAt }Pro

A pending org invitation.

DeviceInfo{ userAgent?, ip?, label?, [key]: unknown }Pro

Device/context metadata for multi-device tracking. All fields optional.

Errors

Every failure is a RadonError subclass with a stable code string you can branch on — never match on message text. Integrations map each to an HTTP status automatically.

Error classcodeHTTP
RateLimitErrorrate_limit_exceeded429
CodeInvalidErrorcode_invalid401
CodeExpiredErrorcode_expired401
CodeNotFoundErrorcode_not_found401
MaxAttemptsErrormax_attempts_exceeded401
InvalidCredentialsErrorinvalid_credentials401
TokenInvalidErrortoken_invalid401
ApiKeyInvalidErrorapi_key_invalid401
RefreshTokenInvalidErrorrefresh_token_invalid401
SessionExpiredErrorsession_expired401
SessionNotFoundErrorsession_not_found401
NotAuthorizedErrornot_authorized403
UserNotFoundErroruser_not_found404
EmailExistsErroremail_exists409
WeakPasswordErrorweak_password400
PasswordNotSetErrorpassword_not_set400
OrgErrororg_error400
OAuthErroroauth_error400
TotpErrortotp_error400
WebAuthnErrorwebauthn_error400
SmsSendErrorsms_send_failed500
EncryptionRequiredErrorencryption_required400
InvalidConfigErrorinvalid_config400
ProviderNotConfiguredErrorprovider_not_configured501
AdapterCapabilityErroradapter_capability501
LicenseRequiredErrorlicense_required501
LicenseInvalidErrorlicense_invalid500
EmailSendErroremail_send_failed500

Several errors carry extra fields: RateLimitError.retryAfterMs, CodeInvalidError.attemptsRemaining, and EmailSendError.sendCause / SmsSendError.sendCause.

import { RateLimitError, CodeInvalidError } from "@radonsdk/auth";

try {
  await auth.emailCode.verify({ email, code });
} catch (err) {
  if (err instanceof CodeInvalidError) { /* wrong code — err.attemptsRemaining */ }
  if (err instanceof RateLimitError)   { /* too many tries — err.retryAfterMs */ }
}

Next steps

On this page