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): JwtClaimsVerify 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): UnsubscribeProSubscribe to a lifecycle event. Returns an unsubscribe function.
auth.oauth(name): OAuthProviderProGet a configured Pro OAuth provider by the name you keyed it under, e.g.
auth.oauth("github").
auth.licensedbooleanWhether the Pro tier is unlocked (a valid license was confirmed in init()).
auth.oauthProvidersstring[]Names of all configured Pro OAuth providers.
auth.adapterRadonAdapterThe configured storage adapter.
auth.sessionsSessionManagerThe stateless JWT session issuer/verifier (free tier). See below.
auth.engineRadonEngineThe low-level engine, for advanced/direct use.
auth.eventsEventBusThe event bus. Emission is always active; subscribing via auth.on is Pro.
Provider accessors
| Accessor | Provider | Tier |
|---|---|---|
auth.emailCode | Email one-time code | Free |
auth.magicLink | Magic link | Free |
auth.emailPassword | Email + password | Free |
auth.google | Google OAuth | Free |
auth.oauth(name) | A configured OAuth provider | Pro |
auth.phoneOtp | Phone / SMS OTP | Pro |
auth.totp | 2FA / TOTP | Pro |
auth.webauthn | Passkeys / WebAuthn | Pro |
auth.refresh | Refresh tokens | Pro |
auth.sessionsProvider | Multi-device sessions, guests, impersonation | Pro |
auth.apiKeys | API keys | Pro |
auth.orgs | Orgs / teams | Pro |
auth.account | Data export / deletion | Pro |
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): JwtClaimsVerify 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.
auth.magicLink
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? }): stringBuild 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 }ProBuild 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 }>ProExchange 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 }>ProIssue a numeric code and deliver it via SMS. Codes default to a 5-minute TTL.
verify({ phone, code }): Promise<{ user: RadonUser; created: boolean }>ProVerify 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 }>ProGenerate 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[] }>ProConfirm 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>ProSecond-factor check for an enrolled user. Allows ±window steps of drift
(default 1 step / ±30s).
verifyRecoveryCode(userId, code): Promise<boolean>ProVerify and consume a recovery code. Each works once.
isEnabled(userId): Promise<boolean>ProWhether 2FA is active for the user.
regenerateRecoveryCodes(userId): Promise<string[]>ProIssue a fresh set of recovery codes, invalidating the old set.
disable(userId): Promise<void>ProDisable 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 }>ProProduce registration options for the browser. Persist the returned challenge.
finishRegistration({ userId, response, expectedChallenge }): Promise<{ credential: StoredCredential }>ProVerify the browser's registration response and store the credential.
startAuthentication(userId?): Promise<{ options; challenge: string }>ProProduce authentication options. Omit userId for usernameless
(discoverable-credential) login.
finishAuthentication({ userId?, response, expectedChallenge }): Promise<{ user: RadonUser }>ProVerify the assertion, update the signature counter (replay protection), and return the authenticated user.
listCredentials(userId): Promise<Array<Omit<StoredCredential, "publicKey">>>ProList a user's registered passkeys without exposing key material.
removeCredential(userId, credentialId): Promise<void>ProRemove 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>ProIssue a fresh access + refresh pair in a new rotation family.
refresh(refreshToken, { claims?, device? }?): Promise<TokenPair>ProExchange a refresh token for a new pair, rotating (retiring) the old one.
revoke(refreshToken): Promise<void>ProRevoke a single refresh token by its plaintext value.
revokeAllForUser(userId): Promise<void>ProRevoke 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 }>ProCreate a DB-backed session carrying device metadata. Returns the opaque token.
listDevices(userId, currentToken?): Promise<DeviceSession[]>ProList 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>ProRevoke a single session (one device) by its id.
revoke(token): Promise<void>ProRevoke a single session by its opaque token.
revokeAll(userId): Promise<void>ProRevoke all of a user's sessions.
createGuest({ device?, metadata? }?): Promise<{ user; token; session }>ProCreate an anonymous guest (a real user row with no auth method) plus a session.
isGuest(userId): Promise<boolean>ProWhether a user is still an unclaimed guest.
upgrade({ userId, provider, providerAccountId, email?, emailVerified? }): Promise<{ user }>ProAttach a real identity to a guest, keeping the same user id (and its data).
impersonate(adminUserId, targetUserId, { expiresInSec?, reason? }?): Promise<{ token; expiresAt }>ProMint 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 }>ProCreate 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>ProTiming-safe verification, checking expiry and revocation. Updates lastUsedAt.
Throws ApiKeyInvalidError.
list({ userId?, orgId? }): Promise<PublicApiKey[]>ProList a user's or org's keys (hashes stripped).
revoke(apiKeyId): Promise<void>ProRevoke 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 }>ProCreate an org and make ownerUserId its owner. slug auto-derives from name
if omitted; a duplicate slug throws OrgError.
get(idOrSlug): Promise<RadonOrg | null>ProFetch an org by id or slug.
listForUser(userId): Promise<RadonOrgMembership[]>ProList a user's org memberships.
listMembers(orgId): Promise<RadonOrgMembership[]>ProList members of an org.
roleOf(orgId, userId): Promise<OrgRole | null>ProA user's role in an org, or null if not a member.
invite({ orgId, email, role?, invitedByUserId? }): Promise<{ invite; token; url? }>ProInvite 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>ProAccept an invite: add the signed-in user to the org with the invited role.
addMember(orgId, userId, role?): Promise<RadonOrgMembership>ProDirectly add a member (no invite). role defaults to member. Caller must
authorize.
setRole(orgId, userId, role): Promise<RadonOrgMembership>ProChange a member's role. Cannot demote the last owner.
removeMember(orgId, userId): Promise<void>ProRemove a member. Cannot remove the last owner.
assertRole(orgId, userId, min): Promise<void>ProThrow NotAuthorizedError unless the user has at least min role — use it to
gate your own actions.
delete(orgId): Promise<void>ProDelete an org and its memberships/invites.
auth.account (Pro)
exportData(userId): Promise<UserDataExport>ProRight-to-access: every stored record for a user, with secret material stripped.
deleteUser(userId): Promise<void>ProRight-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 idOther 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 & path | Body / query | Effect |
|---|---|---|
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 /logout | — | Clears 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 }ProA refresh token in a rotation family.
RadonApiKey{ id, userId, orgId, prefix, keyHash, name, scopes, expiresAt, lastUsedAt, revokedAt, metadata, createdAt }ProAn API key. PublicApiKey is the same shape with keyHash omitted.
RadonOrg{ id, name, slug, metadata, createdAt, updatedAt }ProAn organization / workspace / team.
RadonOrgMembership{ id, orgId, userId, role, metadata, createdAt }ProA user's membership; role is OrgRole ("owner" | "admin" | "member").
RadonOrgInvite{ id, orgId, email, role, tokenHash, invitedByUserId, expiresAt, acceptedAt, metadata, createdAt }ProA pending org invitation.
DeviceInfo{ userAgent?, ip?, label?, [key]: unknown }ProDevice/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 class | code | HTTP |
|---|---|---|
RateLimitError | rate_limit_exceeded | 429 |
CodeInvalidError | code_invalid | 401 |
CodeExpiredError | code_expired | 401 |
CodeNotFoundError | code_not_found | 401 |
MaxAttemptsError | max_attempts_exceeded | 401 |
InvalidCredentialsError | invalid_credentials | 401 |
TokenInvalidError | token_invalid | 401 |
ApiKeyInvalidError | api_key_invalid | 401 |
RefreshTokenInvalidError | refresh_token_invalid | 401 |
SessionExpiredError | session_expired | 401 |
SessionNotFoundError | session_not_found | 401 |
NotAuthorizedError | not_authorized | 403 |
UserNotFoundError | user_not_found | 404 |
EmailExistsError | email_exists | 409 |
WeakPasswordError | weak_password | 400 |
PasswordNotSetError | password_not_set | 400 |
OrgError | org_error | 400 |
OAuthError | oauth_error | 400 |
TotpError | totp_error | 400 |
WebAuthnError | webauthn_error | 400 |
SmsSendError | sms_send_failed | 500 |
EncryptionRequiredError | encryption_required | 400 |
InvalidConfigError | invalid_config | 400 |
ProviderNotConfiguredError | provider_not_configured | 501 |
AdapterCapabilityError | adapter_capability | 501 |
LicenseRequiredError | license_required | 501 |
LicenseInvalidError | license_invalid | 500 |
EmailSendError | email_send_failed | 500 |
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 */ }
}