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

@1moby/just-auth

v0.4.3

Published

Lightweight zero-dependency edge-native auth library for React

Downloads

261

Readme

@1moby/just-auth

Lightweight, zero-dependency, edge-native auth library for React.

OAuth 2.0 + PKCE, email/password, session management, RBAC, route-level middleware, and (with the ClickHouse adapter) a multi-org permission graph + approval-flow primitives — all built on Web Crypto API and raw SQL. Works with Cloudflare Workers, Bun, Deno, Next.js, and any runtime that supports standard Request/Response.

Features

  • Zero runtime dependencies — only React as a peer dep (@clickhouse/client is an optional peer for the CH adapter)
  • OAuth 2.0 + PKCE — built-in providers for Google, GitHub, LINE
  • Email/password auth — PBKDF2-SHA256 (600k iterations), timing-safe comparison
  • Account linking — multiple providers share one user account via email matching (allowEmailAccountLinking)
  • Lifecycle callbackssignIn to gate OAuth sign-in + inject extra user columns; session to customize the /api/auth/session response body
  • Session management — sliding window (30-day sessions, auto-extend at 15 days), SHA-256 hashed tokens
  • RBAC — optional role-based access control with code-defined permissions; multi-role per user, role inheritance, deny rules
  • Email/domain restrictionallowedEmails config to restrict by domain or custom function
  • Route permission middlewarecreateAuthMiddleware for path-based permission gating
  • Database adapters — D1, bun:sqlite, pg, mysql2, Bun.sql, ClickHouse — bring your own driver
  • ClickHouse extras (experimental) — multi-org / department / supervisor permission graph (adapter.rbac) + approval-flow state machine (adapter.approvals); integration-tested against CH 24.8 / 25.3 / 25.10
  • Table prefixtablePrefix: "myapp_" for shared databases
  • Non-destructive migrations — validates existing schema, never ALTER or DROP
  • Security hardened — open redirect protection, password length limits, POST-only logout
  • Edge-native — standard Request/Response, no Node.js-specific APIs

Install

bun add @1moby/just-auth
# or
npm install @1moby/just-auth

Quick Start

1. Choose a Database Adapter

// Cloudflare D1
import { createD1Adapter } from "@1moby/just-auth/adapters/d1";
const db = createD1Adapter(env.DB);

// bun:sqlite
import { createBunSQLiteAdapter } from "@1moby/just-auth/adapters/bun-sqlite";
const db = createBunSQLiteAdapter(new Database("auth.db"));

// PostgreSQL (pg)
import { createPgAdapter } from "@1moby/just-auth/adapters/pg";
const db = createPgAdapter(new Pool({ connectionString: env.DATABASE_URL }));

// MySQL (mysql2)
import { createMySQLAdapter } from "@1moby/just-auth/adapters/mysql";
const db = createMySQLAdapter(pool);

// Bun.sql (Postgres or MySQL)
import { createBunSQLAdapter } from "@1moby/just-auth/adapters/bun-sql";
const db = createBunSQLAdapter(Bun.sql);
const db = createBunSQLAdapter(Bun.sql, { dialect: "mysql" });

Only the adapter you import gets bundled. Drivers are peer dependencies — install what you need.

2. Run Migrations

import { migrate } from "@1moby/just-auth";

await migrate(db);
// With table prefix:
await migrate(db, { tablePrefix: "myapp_" });

Migrations are non-destructive: creates tables if missing, validates existing schema, never alters or drops existing tables.

3. Server Setup

import {
  createReactAuth,
  createGoogleProvider,
  createGitHubProvider,
} from "@1moby/just-auth";

const auth = createReactAuth({
  providers: [
    createGoogleProvider({
      clientId: env.GOOGLE_CLIENT_ID,
      clientSecret: env.GOOGLE_CLIENT_SECRET,
      redirectURI: "https://example.com/api/auth/callback/google",
    }),
    createGitHubProvider({
      clientId: env.GITHUB_CLIENT_ID,
      clientSecret: env.GITHUB_CLIENT_SECRET,
      redirectURI: "https://example.com/api/auth/callback/github",
    }),
  ],
  database: db,
  credentials: true,
  oauthAutoCreateAccount: true,
  allowEmailAccountLinking: true,
});

// Handle auth routes
const response = await auth.handleRequest(request);
if (response) return response;

4. Protect Server Routes

const session = await auth.auth(request);
if (!session) {
  return new Response("Unauthorized", { status: 401 });
}
// session.user = { id, email, name, avatarUrl, role }

5. React Client

import { SessionProvider, useSession, signIn, signOut } from "@1moby/just-auth/client";

function App() {
  return (
    <SessionProvider>
      <Profile />
    </SessionProvider>
  );
}

function Profile() {
  const { data, status } = useSession();

  if (status === "loading") return <p>Loading...</p>;
  if (status === "unauthenticated") return <button onClick={() => signIn("google")}>Sign In</button>;

  return (
    <div>
      <p>Hello, {data.user.name}</p>
      <button onClick={() => signOut()}>Sign Out</button>
    </div>
  );
}

6. Email/Password

import { signIn, signUp } from "@1moby/just-auth/client";

// Register
const res = await signUp({ email: "[email protected]", password: "secret123" });

// Login
const res = await signIn("credentials", { email: "[email protected]", password: "secret123" });

Configuration

createReactAuth({
  providers: [...],
  database: db,

  // Auth options
  basePath: "/api/auth",               // default: "/api/auth"
  credentials: true,                   // enable email/password auth
  allowRegistration: true,             // allow self-registration (default: true when credentials enabled)
  oauthAutoCreateAccount: true,        // auto-create users on OAuth login (default: false)
  allowEmailAccountLinking: true,      // link accounts by verified email match (default: false)
  passwordMinLength: 8,               // default: 8, max: 128

  // Email restriction
  allowedEmails: ["@1moby.com"],       // domain allowlist
  // or: allowedEmails: (email) => email.endsWith("@1moby.com"),

  // Table prefix for shared databases
  tablePrefix: "myapp_",              // → myapp_users, myapp_accounts, myapp_sessions

  // Cookie options
  cookie: {
    name: "auth_session",             // default: "auth_session"
    secure: true,                     // default: true
    sameSite: "lax",                  // default: "lax"
    domain: ".example.com",           // for subdomain sharing
    path: "/",                        // default: "/"
  },

  // Session options
  session: {
    maxAge: 30 * 86400,               // 30 days (seconds)
    refreshThreshold: 15 * 86400,     // extend when < 15 days remaining
  },

  // RBAC (see full RBAC section below)
  rbac: {
    statements: {
      post: ["create", "read", "update", "delete"],
      user: ["list", "ban", "set-role"],
    },
    roles: {
      user: { post: ["read"] },
      admin: "*",  // wildcard = all permissions
    },
    defaultRole: "user",
  },

  // Custom error redirect target (used by signIn callback rejections)
  pages: { error: "/auth/error" },

  // Lifecycle callbacks (see "Hooks" section)
  callbacks: {
    signIn: async (ctx) => ({ allow: true }),
    session: async ({ user, session }) => ({ user, session }),
  },
});

Email-based account linking

When a user signs in via OAuth and their (provider_id, provider_user_id) has no matching row in accounts, but their profile email matches an existing user, the default behavior is to reject with OAuthAccountNotLinked (HTTP 403). Set allowEmailAccountLinking: true to instead link the incoming OAuth account to the existing user.

createReactAuth({
  // ...
  allowEmailAccountLinking: true,
});

Trust implication. Linking by email is safe only when you trust the identity provider to verify the email (e.g. Google Workspace with a hosted-domain restriction, or a corporate IdP). If a provider lets users sign up with unverified emails, a malicious user could claim ownership of another user's email and get their account linked.

When a link occurs, the signIn callback (if set) is invoked with ctx.emailLinked === true and ctx.existingUserId set to the linked user's id — useful for audit logs:

callbacks: {
  signIn: async (ctx) => {
    if (ctx.emailLinked) {
      await auditLog.record({ event: "oauth_account_linked", userId: ctx.existingUserId, provider: ctx.provider });
    }
    return { allow: true };
  },
}

The older allowDangerousEmailAccountLinking flag still works as an alias for backward compatibility but is deprecated; prefer allowEmailAccountLinking in new code.

Hooks

Two optional lifecycle callbacks let consumers intercept sign-in and customize the session response without forking the library. Both are plain async functions on AuthConfig.callbacks.

signIn — gate OAuth sign-in, inject extra columns

Fires inside the OAuth callback handler, after token exchange and user lookup but before any user or account row is written. Return { allow: false, reason } to abort (the user is redirected to pages.error ?? "/" with ?error=REASON). Return { allow: true, userOverrides } to continue; userOverrides is merged into the users INSERT as extra columns — only applied when a new user is being created. The base identity columns (id, email, name, avatar_url) cannot be overridden. The role column IS overridable — you can assign an initial role to the new user via userOverrides: { role: "admin" }. Use with care; the library does not validate role names.

import type { AuthConfig } from "@1moby/just-auth";

export const authConfig: AuthConfig = {
  // ... providers, database, etc.
  pages: { error: "/auth/error" },
  callbacks: {
    signIn: async (ctx) => {
      if (!ctx.profile.email?.endsWith("@1moby.com")) {
        return { allow: false, reason: "DOMAIN_BLOCKED" };
      }
      // Optional: look up invitation, attach org_id
      return {
        allow: true,
        userOverrides: { org_id: "the-org-uuid" },
      };
    },
  },
};

Extra columns must already exist on the users table — the library never ALTERs existing tables. Column names are validated against /^[a-zA-Z_][a-zA-Z0-9_]*$/ before being interpolated into the INSERT; values use parameter binding.

session — customize the /api/auth/session response

Fires on every GET /api/auth/session call, after the session + user are loaded. Whatever you return becomes the response body verbatim (the default { user, session, accounts, permissions } shape is bypassed entirely — include what you need).

callbacks: {
  session: async ({ user, session }) => {
    const roles = await fetchRolesFor(user.id);
    return { user, roles, sessionExpiresAt: session.expiresAt };
  },
}

With no callbacks.session set, the default response shape is unchanged from 0.1.x.

RBAC

Supports multi-role per user, role inheritance, and deny rules — all backward-compatible with single-role setups.

Basic (single role)

rbac: {
  statements: {
    post: ["create", "read", "update", "delete"],
    user: ["list", "ban", "set-role"],
  },
  roles: {
    viewer: { post: ["read"] },
    admin: "*",  // all permissions
  },
  defaultRole: "viewer",
}

Multi-role

Multiple roles stored as comma-separated string in the same role column ("user,editor"):

// Assign multiple roles
POST /api/auth/role
{ userId: "u1", roles: ["user", "editor"] }

// Add a role incrementally
{ userId: "u1", addRole: "editor" }

// Remove a role
{ userId: "u1", removeRole: "admin" }

Role Inheritance

roles: {
  viewer: { post: ["read"] },
  editor: {
    allow: { post: ["create", "read", "update"] },
    inherits: ["viewer"],  // gets all viewer permissions
  },
  moderator: {
    allow: { user: ["list", "ban"] },
    inherits: ["editor"],  // editor → viewer chain
  },
}

Deny Rules (deny always wins)

roles: {
  moderator: {
    allow: { user: ["list", "ban", "set-role"] },
    deny: { user: ["set-role"] },  // can ban but can't change roles
  },
  admin: {
    allow: "*",
    deny: { billing: ["manage"] },  // can view but not manage billing
  },
  superadmin: "*",  // unrestricted
}

Server API

const canEdit = await auth.hasPermission(request, "post:update");
const isAdmin = await auth.hasRole(request, "admin");     // multi-role aware
const roles = await auth.getRoles(request);                // ["user", "editor"]

Client API

import { usePermission, useRole } from "@1moby/just-auth/client";

const canEdit = usePermission("post:update");
const isAdmin = useRole("admin");  // works with "user,admin" multi-role

Route Permission Middleware

import { createAuthMiddleware } from "@1moby/just-auth/middleware";

const { handle } = createAuthMiddleware(auth, {
  publicPaths: ["/login", "/public/*"],
  loginRedirect: "/login",
  routePermissions: {
    "/admin/*": "admin:access",
    "/api/admin/*": "admin:access",
  },
  onForbidden: (req) => new Response("Forbidden", { status: 403 }),
});

// In your server:
const blocked = await handle(request);
if (blocked) return blocked;
// ...proceed with normal routing

Auto-skips static files (.js, .css, .png, etc.). Supports exact paths and glob patterns.

Auth Routes

Default basePath: "/api/auth":

| Route | Method | Description | |-------|--------|-------------| | /api/auth/login/:provider | GET | Redirect to OAuth provider | | /api/auth/callback/:provider | GET | Handle OAuth callback, create session | | /api/auth/register | POST | Register with email/password | | /api/auth/callback/credentials | POST | Login with email/password | | /api/auth/session | GET | Return session JSON + linked accounts + permissions | | /api/auth/role | POST | Set user role (requires user:set-role permission) | | /api/auth/logout | POST | Invalidate session, return { ok: true } |

Database Adapters

Bring your own driver — only the adapter you import gets bundled:

import { createD1Adapter } from "@1moby/just-auth/adapters/d1";
import { createBunSQLiteAdapter } from "@1moby/just-auth/adapters/bun-sqlite";
import { createPgAdapter } from "@1moby/just-auth/adapters/pg";
import { createMySQLAdapter } from "@1moby/just-auth/adapters/mysql";
import { createBunSQLAdapter } from "@1moby/just-auth/adapters/bun-sql";

The Pg and Bun.sql adapters auto-translate ? placeholders to $1, $2, .... Schema uses portable types (VARCHAR(255), BIGINT, TEXT) that work across SQLite, Postgres, and MySQL.

You can also implement the DatabaseAdapter interface directly for any custom driver.

ClickHouse adapter (experimental)

@1moby/just-auth/adapters/clickhouse is a drop-in replacement for the SQL adapters that backs auth state on ClickHouse ReplacingMergeTree tables, plus exposes two extra APIs on the same adapter object:

  • adapter.rbac — a multi-org / department / supervisor permission graph.
  • adapter.approvals — an approval-flow state machine (open / decide / delegate / expire).
  • adapter.migrate() — idempotent DDL setup, cluster-aware.
import { createClient } from "@clickhouse/client";
import { createClickhouseAdapter } from "@1moby/just-auth/adapters/clickhouse";
import { createReactAuth } from "@1moby/just-auth";

const ch = createClient({ url: env.CH_URL, username: env.CH_USER, password: env.CH_PASS });
const adapter = createClickhouseAdapter({ client: ch });
await adapter.migrate();

const auth = createReactAuth({
  database: adapter,
  providers: [/* ... */],
});

// RBAC
await adapter.rbac.createOrganization({ id: "org1", name: "Acme" });
await adapter.rbac.defineRole({ id: "editor", scope: "org", permissions: ["dashboard.edit"] });
await adapter.rbac.grantUserRole({ userId: "u1", roleId: "editor", orgId: "org1", grantedBy: "system" });

const decision = await adapter.rbac.resolvePermission({
  userId: "u1",
  permission: "dashboard.edit",
  resource: { orgId: "org1" },
});
// decision.allowed === true; decision.via === "role"

// Approvals
const req = await adapter.approvals.open({
  requesterUserId: "u1",
  action: "dashboard.publish",
  resource: { orgId: "org1" },
  payload: { title: "Q4" },
  chainStrategy: "supervisor_chain",
});
await adapter.approvals.decide({
  requestId: req.id,
  approverUserId: "boss",
  decision: "approved",
});

Verified versions

The adapter is integration-tested against three live ClickHouse servers. All 13 spec scenarios pass on each:

| Server | Image tag | Status | | --- | --- | --- | | 24.x LTS | clickhouse/clickhouse-server:24.8 | ✓ 13/13 | | 25.x LTS | clickhouse/clickhouse-server:25.3 | ✓ 13/13 | | 25.10+ | clickhouse/clickhouse-server:25.10 | ✓ 13/13 |

To reproduce locally:

docker compose -f examples/clickhouse/docker-compose.yml up -d
bun tests/integration/clickhouse/run.ts        # all three
bun tests/integration/clickhouse/run.ts 24     # one version
docker compose -f examples/clickhouse/docker-compose.yml down -v

OLAP-on-OLTP trade-offs (read these before adopting)

  • Hot reads use FINAL on ReplacingMergeTree to collapse to the latest version per key. This is more expensive than a typical SQL row read; budget for it.
  • sessions_dict Dictionary fronts the session lookup hot path. Default LIFETIME(MIN 5 MAX 15)up to 15s of staleness on revoked sessions. Either accept the window, force SYSTEM RELOAD DICTIONARY sessions_dict on revoke, or disable the dict via useSessionDict: false.
  • Email uniqueness is best-effort. ClickHouse has no transactional unique constraint. Two parallel createUser calls with the same email both succeed; the consumer must check via FINAL ... LIMIT 1 before insert. Even then, a residual race window remains.
  • Cascade deletes are app-level. Deleting a user emits tombstone rows across users, accounts, sessions, and user_role_grants sequentially. There is a small window where the user is gone but related rows are not.
  • No multi-table transactions. The approval state machine writes a tombstone row + a fresh row for each transition. A crash between the audit-log append and the request-row update can leave the audit slightly ahead of state.
  • Cluster mode. Pass cluster: 'name' to wrap every DDL with ON CLUSTER '<name>' and every engine with Replicated*. Keeper paths follow /clickhouse/tables/{installation}/{shard}/<table> (uses CH macros).

The pre-existing RbacConfig-style RBAC continues to work on every adapter (including ClickHouse) — the graph RBAC API is additive, you can use either or both.

See examples/clickhouse/ for a runnable docker-compose setup.

Exports

// Main
import { createReactAuth, migrate } from "@1moby/just-auth";
import { createGoogleProvider, createGitHubProvider, createLineProvider } from "@1moby/just-auth";
import { hashPassword, verifyPassword, resolvePermissions, parseRoles } from "@1moby/just-auth";
import { createQueries, resolveTableNames } from "@1moby/just-auth";

// Client
import { SessionProvider, useSession, signIn, signUp, signOut } from "@1moby/just-auth/client";
import { usePermission, useRole } from "@1moby/just-auth/client";

// Middleware
import { createAuthMiddleware } from "@1moby/just-auth/middleware";

// Database Adapters
import { createD1Adapter } from "@1moby/just-auth/adapters/d1";
import { createBunSQLiteAdapter } from "@1moby/just-auth/adapters/bun-sqlite";
import { createPgAdapter } from "@1moby/just-auth/adapters/pg";
import { createMySQLAdapter } from "@1moby/just-auth/adapters/mysql";
import { createBunSQLAdapter } from "@1moby/just-auth/adapters/bun-sql";
import { createClickhouseAdapter } from "@1moby/just-auth/adapters/clickhouse";

// Types
import type {
  AuthConfig, AuthInstance, User, Session, Account,
  DatabaseAdapter, OAuthProvider, SessionManager,
  RbacConfig, RoleDefinition, SessionContextValue, SessionStatus,
  Queries, TableNames, MigrateOptions,
  // 0.2.x callbacks
  AuthCallbacks, SignInCallbackContext, SignInCallbackResult,
  SessionCallbackContext, PagesConfig,
} from "@1moby/just-auth";

// ClickHouse adapter types (when using @1moby/just-auth/adapters/clickhouse)
import type {
  Organization, Department, EffectiveRole, PermissionDecision,
  PermissionVia, RoleScope, RoleDefinition as CHRoleDefinition,
  RoleGrant, ApprovalRequest, ApprovalStatus, ApprovalDecision,
  RbacApi, ApprovalsApi, CHClient, CHTableNames,
} from "@1moby/just-auth/adapters/clickhouse";

Testing

bun test                                          # unit suite (331 tests across 20 files)

# ClickHouse integration matrix (requires Docker)
docker compose -f examples/clickhouse/docker-compose.yml up -d
bun tests/integration/clickhouse/run.ts           # all 13 scenarios x CH 24.8 / 25.3 / 25.10
docker compose -f examples/clickhouse/docker-compose.yml down -v

Security

What the library protects against, and what your application still has to handle.

What's enforced by the library

  • Session tokens — 256-bit random, SHA-256-hashed in storage, sliding window with 30-day TTL. Raw tokens never touch the database.
  • OAuth state — 256-bit random per flow, timing-safe equality, per-provider cookie names (oauth_state_<id>, code_verifier_<id>) so concurrent flows don't cross-contaminate. Cleared on every callback exit path (success and every error).
  • PKCE — S256 code verifier on Google and LINE; GitHub passes the challenge but its OAuth-App tokens endpoint may not enforce it.
  • Cookie prefixes — default cookie name is __Host-auth_session. If you set cookie.domain or a non-/ cookie.path, the library auto-downgrades to __Secure-… (a __Host- cookie with Domain set is silently rejected by browsers).
  • Email-based account linking — gated on profile.emailVerified === true and allowEmailAccountLinking: true. Set allowUnverifiedEmailLinking: true to opt out (don't, unless you trust the IdP). Returns EmailNotVerified when the provider didn't verify the email.
  • signIn callback userOverridesid, email, name, avatar_url, password_hash, and role are reserved and cannot be injected. Other column names pass through but are validated against /^[a-zA-Z_][a-zA-Z0-9_]*$/ before being interpolated.
  • POST /role — requires the user:set-role permission. Cannot be used to change your own role. Cannot grant a role whose permissions aren't a subset of yours (no privilege escalation in a single call).
  • Email enumeration on registrationhashPassword runs unconditionally before checking if the email exists, so taken/available paths take comparable wall time.
  • Email enumeration on credentials loginverifyPassword always runs against either the user's hash or a dummy hash of the same shape.
  • CSRF (defense in depth)Origin / Referer check on every POST, alongside SameSite=Lax cookies.
  • Open redirectonAuthSuccess and pages.error are validated against isSafeRedirect (same-origin only).
  • HTML attribute escaping&, ", ', `, <, > are all escaped in the OAuth-redirect HTML.
  • Email case — normalized to lowercase + trimmed at every ingestion point (registration, OAuth callback, allowedEmails check) so [email protected] and [email protected] collapse to one identity.
  • ClickHouse table names — every consumer-provided table name is validated against the SQL identifier regex at adapter construction time.
  • createReactAuth provider IDs — must match [a-zA-Z0-9_-]+ (cookie-name safety).

What you still need to do

  • Rate limit POST /register and POST /callback/credentials. The library enforces a passwordMinLength (default 8 — bump to 12+ for new apps) and a 128-char max to prevent PBKDF2 DoS, but it does not rate-limit. Add Cloudflare Rate Limiting rules, an upstream WAF, or a middleware in front of the auth routes.
  • Force-sign-out all sessions on password change. The library exposes queries.deleteUserSessions(userId) if you wire it; the framework doesn't auto-revoke other sessions when a single password is changed.
  • Audit your signIn callback. Anything you put in userOverrides is written to the users row. Reserved keys (id, email, name, avatar_url, password_hash, role) are rejected, but if your callback echoes user-controlled data into userOverrides for any other column, the user controls that column.
  • Trust your X-Forwarded-Host / X-Forwarded-Proto source. The library uses these headers to build absolute redirect URLs in the OAuth flow. Only trust them when the auth server is firewalled behind a known reverse proxy.
  • Provide a password-reset / magic-link flow. The library doesn't ship one. The verification_tokens table is reserved for that purpose (in the CH adapter); SQL adapters don't migrate it by default.
  • Consider GitHub PKCE limitations. GitHub's OAuth Apps don't enforce the code_verifier you send. The library still sends one for forward compatibility but don't rely on it for security against authorization-code interception.

Reporting

Open a GitHub security advisory (preferred) or email the maintainer if you find a vulnerability.

License

MIT