npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@radonsdk/auth

v0.1.0

Published

Provider-agnostic, database-agnostic authentication for Node. Email codes, magic links, password auth, Google OAuth, JWT sessions, and framework integrations — bring your own database.

Downloads

156

Readme

Radon

Authentication for Node that you actually own. Email codes, magic links, email + password (with reset), and Google OAuth — over any database, with any email provider, wired into your framework in a few lines. No hosted service, no per-MAU pricing, no vendor lock-in.

import { Radon } from "@radonsdk/auth";
import { postgresAdapter } from "@radonsdk/auth/adapters/postgres";
import { resendSender } from "@radonsdk/auth/senders/resend";

const auth = new Radon({
  adapter: postgresAdapter(pool),
  session: { secret: process.env.RADON_SECRET! },
  providers: {
    emailCode:     { sender: resendSender({ apiKey, from: "[email protected]" }) },
    magicLink:     { sender: resendSender({ apiKey, from: "[email protected]" }), baseUrl: "https://app.com/verify" },
    emailPassword: { sender: resendSender({ apiKey, from: "[email protected]" }), resetUrl: "https://app.com/reset" },
    google:        { clientId, clientSecret, redirectUri: "https://app.com/api/auth/google/callback" },
  },
});
  • Bring your own database — MongoDB, PostgreSQL, MySQL, Prisma, Supabase, Firebase, SQLite. One small adapter contract; swap freely.
  • Bring your own email — Resend (default), SendGrid, Postmark, AWS SES, or a failover chain across several.
  • Four framework integrations — Next.js, Express, Fastify, Hono. Prebuilt routes + middleware, not hand-wired routing.
  • Stateless JWT sessions you can verify in your own middleware.
  • Secrets never stored in the clear — codes and session tokens are hashed before they touch your database; passwords are bcrypt-hashed.

5-minute setup

1. Install

npm install @radonsdk/auth
# your database driver (pick one):
npm install pg              # or mongoose / mysql2 / better-sqlite3 / @supabase/supabase-js / firebase-admin

Email transports (Resend/SendGrid/Postmark) use fetchno SDK to install. (Only AWS SES needs @aws-sdk/client-sesv2.)

2. Set two env vars

RADON_SECRET=$(openssl rand -hex 32)   # signs JWT sessions & link tokens
RESEND_API_KEY=re_...                  # or your chosen provider's key

3. Create the Radon instance

// lib/auth.ts
import { Radon } from "@radonsdk/auth";
import { postgresAdapter } from "@radonsdk/auth/adapters/postgres";
import { resendSender } from "@radonsdk/auth/senders/resend";
import { Pool } from "pg";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const sender = resendSender({
  apiKey: process.env.RESEND_API_KEY!,
  from: "Acme <[email protected]>",
});

export const auth = new Radon({
  adapter: postgresAdapter(pool),
  session: { secret: process.env.RADON_SECRET! },
  appName: "Acme",
  providers: {
    emailCode:     { sender },
    magicLink:     { sender, baseUrl: "https://acme.com/verify" },
    emailPassword: { sender, resetUrl: "https://acme.com/reset" },
    // google:     { clientId, clientSecret, redirectUri },  // see below
  },
});

// Create tables once (SQL adapters); safe to call on boot.
await auth.init();

4. Mount the auth routes

Pick your framework — each is a couple of lines. The integration exposes every flow under one mount point (routes listed below).

// app/api/auth/[...radon]/route.ts
import { radonNextHandler } from "@radonsdk/auth/integrations/next";
import { auth } from "@/lib/auth";

const handler = radonNextHandler(auth, { basePath: "/api/auth" });
export const GET = handler;
export const POST = handler;

Protect a route / read the user:

import { getUserFromRequest } from "@radonsdk/auth/integrations/next";
import { auth } from "@/lib/auth";

export async function GET(req: Request) {
  const user = await getUserFromRequest(auth, req);
  if (!user) return new Response("Unauthorized", { status: 401 });
  return Response.json({ user });
}
import express from "express";
import { radonExpress, requireAuth } from "@radonsdk/auth/integrations/express";
import { auth } from "./lib/auth";

const app = express();
app.use(express.json());
app.use("/api/auth", radonExpress(auth));

app.get("/me", requireAuth(auth), (req, res) => res.json(req.radonUser));
import Fastify from "fastify";
import { radonFastify, requireAuth } from "@radonsdk/auth/integrations/fastify";
import { auth } from "./lib/auth";

const app = Fastify();
await app.register(radonFastify(auth), { prefix: "/api/auth" });

app.get("/me", { preHandler: requireAuth(auth) }, (req) => req.radonUser);
import { Hono } from "hono";
import { radonHono, requireAuth } from "@radonsdk/auth/integrations/hono";
import { auth } from "./lib/auth";

const app = new Hono();
app.all("/api/auth/*", radonHono(auth, { basePath: "/api/auth" }));

app.get("/me", requireAuth(auth), (c) => c.json(c.get("radonUser")));

5. Call it from your frontend

// Email one-time code
await fetch("/api/auth/email-code/send", {
  method: "POST", headers: { "content-type": "application/json" },
  body: JSON.stringify({ email }),
});
// …user enters the 6-digit code…
await fetch("/api/auth/email-code/verify", {
  method: "POST", headers: { "content-type": "application/json" },
  body: JSON.stringify({ email, code }),
}); // → sets an HttpOnly session cookie

That's it — you have working auth. 🎉


HTTP routes

All relative to your mount point (e.g. /api/auth). Successful sign-ins set an HttpOnly, SameSite=Lax session cookie (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 → session 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 a sensible status (401 invalid credentials/codes, 429 rate-limited, 409 email exists, …).


Using the SDK directly

You don't have to use the HTTP layer — every provider is callable in code.

// Email code
await auth.emailCode.sendCode({ email });
const { user } = await auth.emailCode.verify({ email, code });

// Magic link
const { url } = await auth.magicLink.sendLink({ email });   // emailed for you
const { user } = await auth.magicLink.verify(tokenFromUrl);

// Password + reset
await auth.emailPassword.signup({ email, password });
const { user } = await auth.emailPassword.login({ email, password });
await auth.emailPassword.requestReset({ email });            // emails a link
await auth.emailPassword.setPassword({ token, newPassword });

// Google OAuth
const url = auth.google.getAuthUrl({ state });              // redirect the user here
const { user } = await auth.google.handleCallback(code);    // on the callback

// Sessions (stateless JWT)
const { token, expiresAt } = auth.createSessionToken(user.id, { claims: { role: "admin" } });
const claims = auth.verifyToken(token);                     // throws if invalid/expired
const current = await auth.getSessionUser(token);           // verify + load user

Verify a token anywhere without an SDK instance — same secret:

import { verifyToken } from "@radonsdk/auth";
const claims = verifyToken(req.cookies.radon_session, process.env.RADON_SECRET!);

Configuration

new Radon({
  adapter,                          // required — from radon/adapters/*
  session: {
    secret: process.env.RADON_SECRET!,  // required
    expiresInSec: 7 * 24 * 60 * 60,     // default 7 days
    issuer, audience,                    // optional JWT claims
  },
  appName: "Acme",                  // shown in default email copy
  providers: { /* see below */ },
  code: { length: 6, ttlMs: 600_000, maxAttempts: 5 },  // one-time-code engine
  rateLimit: { maxPerWindow: 3, windowMs: 600_000 },    // 3 sends / 10 min / email
});

Providers (include only the ones you want):

providers: {
  emailCode:     { sender, template?, from? },
  magicLink:     { sender, baseUrl, ttlMs?, template?, from? },
  emailPassword: { sender, resetUrl, hasher?, minLength?, resetTemplate?, from? },
  google:        { clientId?, clientSecret?, redirectUri?, scopes?, fetch? },
}

Google credentials fall back to the RADON_GOOGLE_CLIENT_ID, RADON_GOOGLE_CLIENT_SECRET, and RADON_GOOGLE_REDIRECT_URI environment variables, so you can omit them from code entirely.

Custom email templates

emailCode: {
  sender,
  template: ({ code, expiresInMinutes, appName }) => ({
    subject: `${code} is your ${appName} code`,
    html: `<h1>${code}</h1><p>Expires in ${expiresInMinutes} min.</p>`,
    text: `Your code: ${code}`,
  }),
}

Email senders

Each is a separate import so you only pull in what you use.

import { resendSender }   from "@radonsdk/auth/senders/resend";    // default
import { sendgridSender } from "@radonsdk/auth/senders/sendgrid";
import { postmarkSender } from "@radonsdk/auth/senders/postmark";
import { sesSender }      from "@radonsdk/auth/senders/ses";        // needs @aws-sdk/client-sesv2

resendSender({ apiKey, from: "Acme <[email protected]>" });
sendgridSender({ apiKey, from: "[email protected]" });
postmarkSender({ serverToken, from: "[email protected]" });
sesSender({ region: "us-east-1", from: "[email protected]" });

Failover across providers:

import { multiSender } from "@radonsdk/auth";
const sender = multiSender(
  [resendSender({ apiKey }), sendgridSender({ apiKey: sg })],
  { onError: (e, i) => console.warn(`sender ${i} failed`, e) },
);

Writing your own is one method:

import type { EmailSender } from "@radonsdk/auth";
const mySender: EmailSender = {
  async send({ to, subject, html, text }) {
    await myTransport(to, subject, html ?? text);
    return { provider: "custom" };
  },
};

Database adapters

Every adapter takes an already-instantiated client. SQL adapters create their tables via auth.init(); Prisma/Supabase/Firebase use schemas you define (see each adapter's docs in source, or the adapter reference).

import { mongoAdapter }    from "@radonsdk/auth/adapters/mongo";       // Mongoose Connection
import { postgresAdapter } from "@radonsdk/auth/adapters/postgres";    // pg Pool
import { mysqlAdapter }    from "@radonsdk/auth/adapters/mysql";       // mysql2 Pool
import { sqliteAdapter }   from "@radonsdk/auth/adapters/sqlite";      // better-sqlite3 Database
import { prismaAdapter }   from "@radonsdk/auth/adapters/prisma";      // PrismaClient
import { supabaseAdapter } from "@radonsdk/auth/adapters/supabase";    // SupabaseClient (service role)
import { firebaseAdapter } from "@radonsdk/auth/adapters/firebase";    // Firestore

Google OAuth setup

  1. In Google Cloud Console → APIs & Services → Credentials, create an OAuth 2.0 Client ID (Web application).
  2. Add your callback to Authorized redirect URIs, e.g. https://acme.com/api/auth/google/callback.
  3. Set RADON_GOOGLE_CLIENT_ID, RADON_GOOGLE_CLIENT_SECRET, and RADON_GOOGLE_REDIRECT_URI (or pass them in providers.google).
  4. Send users to GET /api/auth/google/start. Radon handles the exchange, fetches the profile, and merges by verified email into a single user.

How it fits together

        ┌─────────────────────────────────────────────┐
        │  Radon (SDK)  — the public API                │
        │   providers · senders · JWT sessions          │
        └───────────────┬─────────────────┬─────────────┘
      integrations/*     │                 │   senders/*
   (next·express·        │                 │  (resend·sendgrid·
    fastify·hono)        │                 │   postmark·ses)
                 ┌───────▼─────────┐       ▼
                 │   RadonEngine    │   EmailSender
                 │  codes·sessions· │
                 │  merge-by-email  │
                 └───────┬─────────┘
                 adapters/* (mongo·postgres·mysql·prisma·
                            supabase·firebase·sqlite)

The low-level RadonEngine and every provider class are exported too, if you want to compose things yourself.


Radon Pro

Pro unlocks 50 OAuth providers, phone/SMS OTP, passkeys/WebAuthn, 2FA/TOTP, and six more framework integrations. Pro code lives under radon/pro/* so free-tier apps never bundle it.

Licensing

Pro features require a license key. Set it in config and call await auth.init() — Radon verifies the key once against the license service and caches the result for the process lifetime (never per request).

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

const auth = new Radon({
  adapter,
  session: { secret: process.env.RADON_SECRET! },
  licenseKey: process.env.RADON_LICENSE_KEY,   // or falls back to that env var
  license: { appUrl: "https://yourapp.com" },  // used for the anti-sharing domain lock
  providers: {
    oauth: {
      github: { preset: "github", clientId, clientSecret, redirectUri },
    },
    phoneOtp: { sender: twilioSender({ accountSid, authToken, from }) },
    totp: {},
    webauthn: { rpID: "yourapp.com", origin: "https://yourapp.com" },
  },
});

await auth.init();   // ← verifies the license; throws if missing/invalid
  • Configure a pro provider without a valid key → auth.init() throws LicenseRequiredError with a link to buy one, and pro getters throw until a license is confirmed.
  • Keys are domain-locked: the first deployment to activate a key binds it (via a hashed instanceId derived from appUrl). A leaked key is useless on another domain. Keys are short and readable: RDN-XXXX-XXXX-XXXX.
  • The verify service is a tiny, self-hostable Express app — see license-service/. It's the only always-on infrastructure in the whole product.

OAuth — 50 providers, one engine

Every provider runs on one generic OAuth2/OIDC engine; each is a one-liner.

providers: {
  oauth: {
    github:    { preset: "github",    clientId, clientSecret, redirectUri },
    discord:   { preset: "discord",   clientId, clientSecret, redirectUri },
    microsoft: { preset: "microsoft", clientId, clientSecret, redirectUri },
  },
}

// then, after init():
const url = auth.oauth("github").getAuthUrl({ state }).url;   // redirect the user here
const { user } = await auth.oauth("github").handleCallback({ code });

Built-in presets (50): github, gitlab, bitbucket, discord, facebook, twitter, linkedin, slack, spotify, twitch, reddit, tiktok, snapchat, dropbox, box, zoom, notion, figma, salesforce, hubspot, paypal, amazon, yahoo, epicgames, battlenet, roblox, patreon, strava, fitbit, coinbase, line, wechat, kakao, naver, vk, mailru, instagram, pinterest, dribbble, behance, zoho, digitalocean, microsoft, azuread, apple, wordpress, okta, auth0, shopify, steam.

Some need per-instance params — use the builder presets:

import { microsoftPreset, shopifyPreset, oktaPreset, auth0Preset } from "@radonsdk/auth/pro/oauth";

oauth: {
  work:   { preset: microsoftPreset({ tenant: "your-tenant-id" }), clientId, clientSecret, redirectUri },
  store:  { preset: shopifyPreset({ shop: "acme" }), clientId, clientSecret, redirectUri },
  sso:    { preset: oktaPreset({ domain: "acme.okta.com" }), clientId, clientSecret, redirectUri },
}

Any provider not in the 50 — pass your own preset (same shape the built-ins use). No forking required:

oauth: {
  acme: {
    preset: {
      id: "acme", name: "Acme",
      authorizationUrl: "https://acme.com/oauth/authorize",
      tokenUrl: "https://acme.com/oauth/token",
      userInfoUrl: "https://acme.com/api/me",
      defaultScopes: ["email"],
      // optional: pkce, tokenAuthStyle, mapProfile(raw) => { id, email, ... }
    },
    clientId, clientSecret, redirectUri,
  },
}

Credentials also fall back to RADON_<PROVIDER>_CLIENT_ID / _CLIENT_SECRET / _REDIRECT_URI env vars.

Phone / SMS OTP

Same shape as email codes, over SMS. Twilio is the default transport (pluggable like email senders).

import { twilioSender } from "@radonsdk/auth/pro/sms";

providers: { phoneOtp: { sender: twilioSender({ accountSid, authToken, from: "+14155550123" }) } }

await auth.phoneOtp.sendCode({ phone: "+14155550123" });
const { user } = await auth.phoneOtp.verify({ phone: "+14155550123", code });

2FA / TOTP

Google-Authenticator-compatible second factor, layered on any primary method. Passes the RFC 6238 test vectors.

providers: { totp: { issuer: "Acme" } }

// enrollment
const { secret, uri } = await auth.totp.beginEnrollment(userId);  // render `uri` as a QR
const { recoveryCodes } = await auth.totp.confirmEnrollment(userId, codeFromApp); // show once

// at login, after the primary factor:
const ok = await auth.totp.verify(userId, codeFromApp);
// or: await auth.totp.verifyRecoveryCode(userId, recoveryCode)

Passkeys / WebAuthn

Registration and authentication ceremonies, verified server-side with the audited @simplewebauthn/server (install it to use passkeys).

Client-side required. WebAuthn needs browser APIs Radon can't call from the server. Radon produces the challenge options and verifies the responses; your frontend must call navigator.credentials.create() / .get() (or @simplewebauthn/browser) and POST the results back. Persist the challenge returned by each start* call and pass it to the matching finish* call.

providers: { webauthn: { rpID: "acme.com", origin: "https://acme.com" } }

// register
const { options, challenge } = await auth.webauthn.startRegistration(userId);
// → browser: startRegistration(options) → POST result back:
await auth.webauthn.finishRegistration({ userId, response, expectedChallenge: challenge });

// authenticate
const { options, challenge } = await auth.webauthn.startAuthentication(userId);
// → browser: startAuthentication(options) → POST result back:
const { user } = await auth.webauthn.finishAuthentication({ userId, response, expectedChallenge: challenge });

Pro framework integrations

Completing the set of ten. Same mount-and-go pattern as the free four.

import { radonNestHandler, createRadonAuthGuard } from "@radonsdk/auth/pro/integrations/nestjs";
import { radonKoa }       from "@radonsdk/auth/pro/integrations/koa";
import { radonSvelteKit } from "@radonsdk/auth/pro/integrations/sveltekit";
import { radonNuxt }      from "@radonsdk/auth/pro/integrations/nuxt";
import { radonRemix }     from "@radonsdk/auth/pro/integrations/remix";
import { radonAstro }     from "@radonsdk/auth/pro/integrations/astro";

Each mounts on a catch-all route (/api/auth/*) and exposes the same routes as the free integrations, plus a requireAuth/guard for your own routes. See each module's doc comment for the exact route-file placement.

Sessions, orgs & platform primitives

All configured under providers and reached via the SDK once licensed.

Refresh tokens (short access + long refresh, rotation)

providers: { refresh: { accessTtlSec: 900, refreshTtlMs: 30 * 864e5 } }

const pair = await auth.refresh.issue({ userId, device: { label: "CLI" } });
// pair.accessToken (short JWT) + pair.refreshToken (opaque, rotated on use)
const next = await auth.refresh.refresh(pair.refreshToken); // rotates; old token dies
await auth.refresh.revokeAllForUser(userId);

Presenting an already-rotated refresh token (theft/replay) revokes the whole token family and throws RefreshTokenInvalidError.

Multi-device sessions

providers: { sessions: {} }

const { token } = await auth.sessionsProvider.create({ userId, device: { label: "Chrome/macOS", ip } });
const devices = await auth.sessionsProvider.listDevices(userId, token); // current one is flagged
await auth.sessionsProvider.revokeDevice(devices[0].id);   // one device
await auth.sessionsProvider.revokeAll(userId);             // log out everywhere

Anonymous / guest sessions + upgrade

const guest = await auth.sessionsProvider.createGuest({ metadata: { cart } });
// ...associate data with guest.user.id...
await auth.sessionsProvider.upgrade({
  userId: guest.user.id, provider: "email", providerAccountId: email, email, emailVerified: true,
});   // same user id → data preserved, guest flag cleared

API keys (service-to-service / CLI)

providers: { apiKeys: {} }

const { key, record } = await auth.apiKeys.create({ userId, name: "CI token", scopes: ["read"] });
// key = "rk_<prefix>_<secret>" — shown ONCE. Only its hash is stored.
const owner = await auth.apiKeys.verify(presentedKey);     // throws if invalid/expired/revoked
await auth.apiKeys.revoke(record.id);

Orgs / teams

providers: { orgs: { sender: resendSender({ apiKey }), inviteUrl: "https://app.com/invite" } }

const { org } = await auth.orgs.create({ name: "Acme", ownerUserId });
await auth.orgs.invite({ orgId: org.id, email: "[email protected]", role: "member", invitedByUserId });
const membership = await auth.orgs.acceptInvite(tokenFromEmail, acceptingUserId);
await auth.orgs.setRole(org.id, userId, "admin");
const members = await auth.orgs.listMembers(org.id);

Roles rank owner > admin > member; the last owner can't be removed or demoted. auth.orgs.assertRole(orgId, userId, "admin") gates your own actions.

Impersonation (support/debugging)

// ⚠️ Radon does NOT check who is an admin. Verify admin authorization YOURSELF first.
const { token } = await auth.sessionsProvider.impersonate(adminUserId, targetUserId, {
  reason: "support ticket #42",
});
// token carries `imp: true` + `act: adminUserId` claims and emits an `impersonation` event.

Account export & deletion (GDPR)

providers: { account: {} }

const data = await auth.account.exportData(userId);  // all records, secrets stripped
await auth.account.deleteUser(userId);               // cascade: sessions, keys, identities, memberships

Events / webhooks

Hook side effects without forking Radon. Subscribing requires a license; handlers are isolated (a throwing handler never breaks auth).

const off = auth.on("user.created", ({ user }) => analytics.track(user));
auth.on("login", ({ user, method }) => {});
auth.on("session.revoked", ({ userId, all }) => {});
auth.on("apikey.created", ({ apiKey }) => {});
auth.on("org.member_added", ({ membership }) => {});
auth.on("refresh.reuse_detected", ({ userId }) => alertSecurity(userId));

Security

Radon hashes every verifiable secret, encrypts the one reversible secret (TOTP) with AES-256-GCM using your RADON_ENCRYPTION_KEY, compares all secrets in constant time, ships secure-by-default cookies (HttpOnly + Secure + SameSite=Lax), and provides CSRF helpers. See SECURITY.md for the full model and the dependency-audit results (production deps: 0 known vulnerabilities).

RADON_ENCRYPTION_KEY=$(openssl rand -hex 32)   # required for TOTP; encrypts reversible secrets at rest

Adapter capabilities

The pro session/org/key/deletion features need extra adapter methods. All 7 built-in adapters (+ the in-memory reference) implement them. A custom adapter missing a method throws a clear AdapterCapabilityError naming the method and feature — nothing fails silently.

License

MIT