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

@iqauth/sdk

v2.8.1

Published

TypeScript SDK for IQAuth — the canonical way for all IQ projects to integrate with IQAuthService

Readme

@iqauth/sdk

npm Node Types Module Format License

The canonical TypeScript SDK for IQAuthService — DispositionIQ's multi-tenant identity and authorization platform. One package covers the React client, the four major Node frameworks (Express, Fastify, Hono, Next.js), native mobile, and headless service automation.

New in 2.7.0IQAuthError now exposes a typed code taxonomy (token_expired, jwks_unavailable, rate_limited, …) plus err.is(code) / IQAuthError.isIQAuthError(value) helpers, and tokens.verify<T>() accepts a custom-claims generic so the returned object is fully typed. Both changes are purely additive — existing catch (e: Error) and untyped verify() callers keep compiling. See What's new in 2.7.0.


Table of contents


Install

npm install @iqauth/sdk
  • Node >= 18 (the SDK uses native fetch and Web Crypto).
  • Ships CJS + ESM + .d.ts for every entry point.
  • React is an optional peer (>=18). You only need it if you import from @iqauth/sdk/react.

You'll need two values from the IQAuth admin dashboard for any app you integrate:

  • Publishable key (pk_live_… / pk_test_…) — safe to ship to the browser. Self-describes {iss, appId, tenantId, kid}, so the SDK auto-discovers your issuer; you almost never need to set it manually.
  • Secret key (sk_live_… / sk_test_…) — server-only. Keep it in env vars, never in client code.

Create both in one call from the admin Quickstart wizard, or run npx iqauth init (see CLI).


Five-line integration

React (browser)

import {
  IQAuthProvider,
  IQAuthLoading,
  IQAuthLoaded,
  SignedIn,
  SignedOut,
  RedirectToSignIn,
} from "@iqauth/sdk/react";

export default function App() {
  return (
    <IQAuthProvider publishableKey={import.meta.env.VITE_IQAUTH_PUBLISHABLE_KEY}>
      <IQAuthLoading><Spinner /></IQAuthLoading>
      <IQAuthLoaded>
        <SignedIn><Dashboard /></SignedIn>
        <SignedOut><RedirectToSignIn /></SignedOut>
      </IQAuthLoaded>
    </IQAuthProvider>
  );
}

Wrapping the gating components in <IQAuthLoading/> / <IQAuthLoaded/> is the slow-network-safe pattern: until bootstrap() finishes, both <SignedIn/> and <SignedOut/> render null, which on a slow mobile connection is several seconds of blank page. The loading slot fills that gap, mirroring Clerk's <ClerkLoading/> / <ClerkLoaded/>.

Available hooks: useUser(), useSession(), useAuth(), useOrganization(). Each returns { data, isLoading, error }. Drop-in components: <SignIn/>, <SignUp/>, <UserButton/>, <UserProfile/>, <OrganizationSwitcher/>, <AuthCallback/>.

Silent SSO is opt-in as of 2.6.1. <SignIn/> always renders the form on first paint by default — even for returning users with an active issuer-side iq_sso session. To restore the old auto-resume behavior, add silentSso to the provider or a specific <SignIn/> instance: <IQAuthProvider publishableKey={…} silentSso> or <SignIn silentSso />. See "What's new in 2.6.1" below.

Express

import express from "express";
import { iqAuth } from "@iqauth/sdk/express";

const app = express();
app.use(express.json());

const auth = iqAuth({
  publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!,
  secretKey: process.env.IQAUTH_SECRET_KEY!,
});

auth.attachHelpers(app);  // mounts /api/iqauth/{callback,refresh,signout} (HttpOnly cookies)
app.use(auth);            // verifies Bearer header OR iqauth_at cookie; populates req.auth

Fastify

import Fastify from "fastify";
import { iqAuth } from "@iqauth/sdk/fastify";

const app = Fastify();
const auth = iqAuth({ publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!, secretKey: process.env.IQAUTH_SECRET_KEY! });
await app.register(auth.plugin);   // mounts helpers + decorates request.auth

Hono

import { Hono } from "hono";
import { iqAuth } from "@iqauth/sdk/hono";

const app = new Hono();
const auth = iqAuth({ publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!, secretKey: process.env.IQAUTH_SECRET_KEY! });
app.route("/api/iqauth", auth.helpers);
app.use("*", auth.middleware);

Next.js (App Router)

// app/api/iqauth/[...iqauth]/route.ts
import { iqAuth } from "@iqauth/sdk/next";
const auth = iqAuth({ publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!, secretKey: process.env.IQAUTH_SECRET_KEY! });
export const { GET, POST } = auth.handlers;
// In any Server Component / Route Handler / Server Action
import { getAuth } from "@iqauth/sdk/next";
const session = await getAuth();   // null when signed-out

The middleware is cookie-aware (Bearer header OR iqauth_at cookie) and the issuer is auto-discovered from your publishable key — no extra config.


Pick your environment

The SDK is not "one auth model for every environment". The safe pattern depends on where credentials live.

| Environment | Recommended pattern | Refresh-token owner | Entry point | |---|---|---|---| | First-party browser app with a backend | Backend proxy + HttpOnly cookies | Backend only | @iqauth/sdk/{express,fastify,hono,next} + @iqauth/sdk/react (with serverManagedSession) | | First-party browser app without a backend (SPA only) | Authorization-code + PKCE in-browser, refresh in JS-readable cookie | Browser (in cookie) | @iqauth/sdk/browser + @iqauth/sdk/react | | Native mobile | Authorization-code + PKCE, secure OS storage | Mobile app secure storage | @iqauth/sdk/mobile | | Server-side / API resource server | Bearer verification via JWKS | Server (per request) | @iqauth/sdk/server | | Service / automation / cron | API key | Service | @iqauth/sdk/service |

Rule of thumb: if your app has its own backend, use a framework adapter and turn on serverManagedSession: true in the React provider. Don't store refresh tokens in localStorage/sessionStorage as your durable session model.


What's new in 2.7.0

1. Typed IQAuthError taxonomy

Every SDK-originated throw now carries a code from a fixed 10-value union, so callers can stop string-matching on err.message or guessing whether err.code is upper-snake or lowercase. Two helpers ship alongside it: IQAuthError.isIQAuthError(value) (instanceof-safe across realms) and err.is(code) (narrow-friendly).

import { IQAuthError, type IQAuthErrorCode } from "@iqauth/sdk";

try {
  const claims = await client.tokens.verify(token);
} catch (err) {
  if (IQAuthError.isIQAuthError(err)) {
    if (err.is("token_expired"))      return refreshAndRetry();
    if (err.is("jwks_fetch_failed"))  return retryAfterBackoff();
    if (err.is("rate_limited"))       return showRateLimitToast();
    if (err.is("network"))            return showOfflineBanner();
    if (err.is("config_invalid"))     throw err; // boot-time misconfig
  }
  throw err;
}

The full union:

type IQAuthErrorCode =
  | "token_expired"
  | "token_invalid"
  | "jwks_unavailable"
  | "jwks_fetch_failed"
  | "rate_limited"
  | "network"
  | "config_invalid"
  | "app_not_found"
  | "permission_denied"
  | "unknown";

Back-compat: the field is widened to IQAuthErrorCode | (string & {}), so server-rethrown codes (TOKEN_REVOKED, SESSION_EXPIRED_INACTIVITY, …) still flow through unchanged. The framework adapters (/express, /fastify, /hono) map both upper-snake and the new lowercase codes to 401, so this rollout is invisible to existing app code. IQAuthError also gains a cause accessor (alias for the legacy raw).

See docs/error-handling.md for the full recipe book.

2. IQAuthClaims<T> generic on tokens.verify

verify() now accepts a custom-claims generic so your app's bespoke claims show up typed on the result — no index-signature widening, no as any:

interface MyClaims { plan: "free" | "pro"; orgId: string }

const claims = await client.tokens.verify<MyClaims>(token);
//    ^? IQAuthBaseClaims & MyClaims & JwtClaims

if (claims.plan === "pro") doProThing(claims.orgId);
console.log(claims.tenantId, claims.sub); // base claims still typed

IQAuthBaseClaims is exported separately for callers composing their own envelope. JwtClaims continues to be exported and remains the return type of tokens.decode() / tokens.getClaims() for back-compat. Calls to verify() without a generic argument behave exactly as before.


What's new in 2.6.5

Server-managed userinfo (mountUserinfo: true)

The framework adapters can now auto-mount GET /api/iqauth/me so server-managed integrators don't have to hand-roll a userinfo handler that calls tokens.verify and shapes a data.user / data.claims envelope.

app.use(iqAuth({
  publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!,
  secretKey:     process.env.IQAUTH_SECRET_KEY!,
  mountUserinfo: true,
  // Optional — shallow-merged over the claim-derived SessionUser defaults.
  userinfoEnricher: async (claims, req) => ({
    name: await loadDisplayName(claims.sub),
  }),
}));

Returns the documented UserinfoResponse envelope — { success: true, data: { user, claims, tenantId } } — which is exactly the shape the browser SDK's SessionManager.bootstrap() already accepts (data.user is preferred; claimsToSessionUser(data.claims) is the fallback). The token is read from Authorization: Bearer … OR the iqauth_at cookie; verification reuses a per-issuer cached TokensModule so JWKS fetches are amortized.

Two new framework-neutral exports for integrators who'd rather mount their own route but still emit the canonical envelope: buildUserinfoResponse(claims, { enrich? }) and handleUserinfo(config, { accessToken, req? }). Re-exported from both @iqauth/sdk and @iqauth/sdk/server. See the Server-managed userinfo section below for the full reference.


What's new in 2.6.2

Card grows responsively on desktop (no more phone-sized form)

.iqauth-sdk-card used to cap at max-width: 460px at every viewport, which meant a desktop user staring at a 1440px monitor saw a thin mobile column floating inside a 720px-wide pane. The cap is now responsive:

  • mobile (default): 480px
  • tablet / desktop (@container ≥ 768px): 540px
  • extra-wide (@container ≥ 1280px): 580px

Inner header/body padding and the title size also bump up at the desktop breakpoint so the form actually fills its half of the split layout. No opt-in required — the change applies to every consumer of <SignIn/> and <SignUp/> after upgrading.


What's new in 2.6.1

1. Silent SSO is now opt-in (default off)

Previously, <SignIn/> would detect an active issuer-side iq_sso session on mount and silently redirect through /oidc/sso-resume — users never saw the form, never clicked anything, and were transparently signed back in. That was surprising for embedded use cases and made it impossible to switch accounts without reaching for ?prompt=login. As of 2.6.1, silent SSO is disabled by default and must be explicitly enabled:

// Provider-wide opt-in (covers every <SignIn/> below it)
<IQAuthProvider publishableKey={…} silentSso>
  …
</IQAuthProvider>

// Per-instance opt-in (overrides the provider value)
<SignIn silentSso />

When silent SSO is off (default), <SignIn/> always renders the form on first paint regardless of iq_sso cookie state. prompt="login" and ?prompt=login continue to work as additional force-form switches.

2. Embedded card no longer forces full-viewport height

The internal .iqauth-sdk-pane declared min-height: 100vh unconditionally, which worked for hosted full-page sign-in but broke when <SignIn/> was embedded in a card, modal, or sidebar — pushing content below the fold and creating large empty areas. The 100vh height now applies only inside the wide side-by-side layout (@container iqauth-sdk (min-width: 768px)) where the hero pane needs height parity. Narrow embeds size naturally to their content.

3. Better error reporting on misconfigured <SignIn/>

  • useIQAuthSignInContext now detects HTML responses (CORS preflight, wrong iqAuthBaseUrl, wrong appKey) and prints an actionable console.error naming the three likely causes — instead of the cryptic Unexpected token '<' in JSON.
  • The "returnTo not in allowed origins" console error now also lists the app's actual allowedOrigins, so the diff with the rejected returnTo is visible at a glance instead of requiring a Network-tab spelunk.

For older versions see CHANGELOG.md.


What's new in 2.0.3

1. serverManagedSession: true for SessionManager / IQAuthProvider

For any app whose backend uses one of the framework adapters (@iqauth/sdk/{express,fastify,hono,next}), the backend owns the HttpOnly iqauth_at + iqauth_rt cookies and should be the sole authority on token rotation. Opt into that explicitly:

<IQAuthProvider
  publishableKey={import.meta.env.VITE_IQAUTH_PUBLISHABLE_KEY}
  serverManagedSession
>
  …
</IQAuthProvider>

What changes when this is on:

  • bootstrap() learns session state with a single read-only GET /api/v1/auth/me (override with userinfoPath) instead of POSTing to /refresh. No rotation, no race surface.
  • The proactive-refresh timer is suppressed — the server middleware refreshes on real navigation, single-flight per request.
  • The browser tokenStore defaults to a no-op store, so JS never tries to read the (HttpOnly, invisible) refresh cookie.

This eliminates the multi-tab + React StrictMode + proactive-timer race that previously produced silent forced sign-outs.

2. clearCookiesOnRefreshFailure: "terminal-only" | "always" | "never"

The cookie helper handleRefresh (used by every framework adapter) now defaults to "terminal-only". Cookies are only cleared when the issuer signals the session is unrecoverable:

  • TOKEN_REVOKED, SESSION_REVOKED, INVALID_GRANT
  • USER_DEACTIVATED, USER_DISABLED, TENANT_SUSPENDED
  • HTTP 410 Gone

Transient failures — TOKEN_INVALID from a rotated-out token, TOKEN_EXPIRED, network blips, 5xx — return 401 with cookies intact, so the next legitimate request can either succeed against a still-valid access cookie or be redirected to sign-in cleanly by the middleware.

iqAuth({
  publishableKey, secretKey,
  clearCookiesOnRefreshFailure: "terminal-only", // default; "always" restores pre-2.0.3 behavior
});

3. Next.js 15+ async cookies

getAuth() in @iqauth/sdk/next now awaits next/headers#cookies() — fixes the runtime warning and the occasional null-session on the first request after build on Next 15 / 16.


Browser apps with a backend (recommended)

This is the integration path you want for any product with its own server. The browser never sees a refresh token; the backend owns rotation; cookies are HttpOnly.

// client/src/main.tsx
import { IQAuthProvider, SignedIn, SignedOut, RedirectToSignIn } from "@iqauth/sdk/react";

createRoot(document.getElementById("root")!).render(
  <IQAuthProvider
    publishableKey={import.meta.env.VITE_IQAUTH_PUBLISHABLE_KEY}
    serverManagedSession
  >
    <SignedIn><App /></SignedIn>
    <SignedOut><RedirectToSignIn /></SignedOut>
  </IQAuthProvider>
);
// server/index.ts
import express from "express";
import { iqAuth } from "@iqauth/sdk/express";

const app = express();
app.use(express.json());

const auth = iqAuth({
  publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!,
  secretKey: process.env.IQAUTH_SECRET_KEY!,
});
auth.attachHelpers(app); // mounts /api/iqauth/{callback,refresh,signout}
app.use(auth);           // populates req.auth on every request

app.get("/api/me", (req, res) => res.json(req.auth));

That's the whole integration. The hosted sign-in page on auth.dispositioniq.com redirects back to /api/iqauth/callback, which sets HttpOnly cookies and bounces the user to your return_to. Subsequent API calls succeed against the access cookie; on the rare expiry the same cookie endpoint refreshes silently.

Calling your own API from the browser

Use auth.fetch() from the React provider — it adds credentials and retries once on 401:

import { useAuth } from "@iqauth/sdk/react";

function ProductList() {
  const { fetch } = useAuth();
  const { data } = useQuery({
    queryKey: ["products"],
    queryFn: () => fetch("/api/products").then(r => r.json()),
  });
  // …
}

Calling another IQAuth-protected API (cross-origin)

Add the calling app's origin to the target app's app_allowed_origins in the admin dashboard. The unified CORS allow-list (per-app origins ∪ global cors_originsCORS_ORIGINS env) refreshes within 60 seconds.


Server reference

Middleware behavior

  • Accepts Authorization: Bearer <jwt> or the iqauth_at HttpOnly cookie.
  • Verifies RS256 against the issuer's JWKS (cached with stale-while-revalidate).
  • Populates req.auth (Express/Fastify), c.get("auth") (Hono), or getAuth() return value (Next).
  • The shape of req.auth is the verified JWT claims plus normalized { sub, email, tenantId, roles, permissions, scopes }.

The SDK uses req.auth (not req.user) so it never collides with Passport.

Requiring roles or entitlements

app.use("/admin",
  auth.require({
    roles: ["tenant_admin", "platform_admin"],
    entitlements: ["iqcapture"],
  }),
);

auth.require() is composable — chain it on individual routes, route groups, or globally. Failures throw IQAuthError with code: "INSUFFICIENT_PERMISSIONS" and the middleware returns 403.

Helper routes mounted by attachHelpers / register / auth.handlers

| Method | Path | Purpose | |---|---|---| | GET | /api/iqauth/callback | Receives the OIDC redirect, exchanges the code, sets iqauth_at + iqauth_rt, redirects to return_to | | POST | /api/iqauth/refresh | Reads iqauth_rt, rotates, sets new cookies. Honors clearCookiesOnRefreshFailure | | POST | /api/iqauth/signout | Revokes the refresh token upstream and clears both cookies | | GET | /api/iqauth/me | Opt-in via mountUserinfo: true. Verifies the access token and returns the documented userinfo envelope. See Server-managed userinfo |

All three set cookies with HttpOnly; Secure; SameSite=lax; Path=/ by default. Override per-app:

iqAuth({
  publishableKey, secretKey,
  cookieDomain: ".example.com",  // for cross-subdomain SSO
  sameSite: "none",              // pair with secure: true
  secure: true,
  cookiePath: "/",
  accessCookieName: "iqauth_at",
  refreshCookieName: "iqauth_rt",
});

Server-managed userinfo

When you're doing the cookie-managed pattern (browser SDK proxies through your own backend), your frontend needs some endpoint to learn "who am I" on first paint. Before 2.6.5 you had to hand-roll that handler — call tokens.verify, shape a data.user envelope, and remember to read the token from either Authorization: Bearer … OR the iqauth_at cookie.

Opt into the auto-mounted route instead:

app.use(iqAuth({
  publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!,
  secretKey:     process.env.IQAUTH_SECRET_KEY!,
  mountUserinfo: true,
  // Optional — shallow-merged over the claim-derived SessionUser defaults.
  userinfoEnricher: async (claims, req) => ({
    name: await loadDisplayName(claims.sub),
  }),
}));

GET /api/iqauth/me returns the documented UserinfoResponse envelope:

{
  "success": true,
  "data": {
    "user":     { "sub": "...", "email": "...", "name": "...", "tenantId": "...", "roles": [...], "entitlements": [...] },
    "claims":   { /* full verified JWT payload */ },
    "tenantId": "ten_..." // or null
  }
}

This is exactly the shape SessionManager.bootstrap() already accepts: data.user is preferred when present, claimsToSessionUser(data.claims) is the documented fallback. Same option works on Express, Fastify, Hono, and the Next.js handler. Token is verified with a per-issuer cached TokensModule so JWKS fetches are amortized across requests.

Want to mount your own route but still emit the canonical envelope? Use the framework-neutral helpers:

import { buildUserinfoResponse, type UserinfoResponse } from "@iqauth/sdk/server";

const envelope: UserinfoResponse = await buildUserinfoResponse(verifiedClaims, {
  enrich: (c) => ({ name: lookupName(c.sub) }),
});

Token verification without a framework adapter

If you're not using Express/Fastify/Hono/Next (custom Node server, AWS Lambda, Cloudflare Worker, etc.):

import { createServerClient } from "@iqauth/sdk/server";

const client = createServerClient({
  publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!, // issuer auto-discovered
});

const claims = await client.tokens.verify(bearerToken);
// → { sub, email, tenantId, roles, permissions, scopes, iss, aud, exp, … }

tokens.verify() enforces RS256, checks exp / nbf / iss / aud, and uses the cached JWKS. Throws IQAuthError with codes like TOKEN_EXPIRED, TOKEN_INVALID, TOKEN_REVOKED.


Native mobile (PKCE)

import { createMobileClient } from "@iqauth/sdk/mobile";

const client = createMobileClient({
  publishableKey: PUBLISHABLE_KEY,
  redirectUri: "myapp://auth/callback",
  storage: {
    get: (k) => SecureStore.getItemAsync(k),
    set: (k, v) => SecureStore.setItemAsync(k, v),
    delete: (k) => SecureStore.deleteItemAsync(k),
  },
});

// 1. begin
const { url } = await client.signIn.start();
await WebBrowser.openAuthSessionAsync(url, "myapp://auth/callback");

// 2. handle redirect
await client.signIn.complete(redirectUrl);

// 3. use the session
const me = await client.users.me();

Tokens are written to whichever storage adapter you provide — use Keychain on iOS, Keystore / EncryptedSharedPreferences on Android. Never persist them to AsyncStorage / localStorage.


Service automation / API keys

For cron jobs, batch scripts, server-to-server calls — anything that isn't acting on behalf of an interactive user:

import { createServiceClient } from "@iqauth/sdk/service";

const client = createServiceClient({
  publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!,
  apiKey: process.env.IQAUTH_API_KEY!,
});

const users = await client.users.list({ tenantId: "…" });
const key = await client.apiKeys.create({ name: "nightly-sync", scopes: ["users:read"] });

API-key calls are scoped to the permissions granted at creation time and are subject to per-key rate limits. Rotate with client.apiKeys.rotate({ id }); revoke with client.apiKeys.revoke({ id }).


Realtime: WebSocket upgrade verification

@iqauth/sdk/ws exposes a single verifyWsUpgrade(req, options) helper for Node WebSocket servers. Same option shape as the framework middlewares — publishableKey, audience, issuer, clockTolerance, cookie name. Returns { claims } on success or null on missing/invalid/expired tokens.

It accepts a token from any of:

  1. Authorization: Bearer <jwt> header.
  2. The iqauth_at cookie on the upgrade request (override with cookieName).
  3. The Sec-WebSocket-Protocol subprotocol value iqauth.bearer.<jwt> — browser WebSocket can't set custom headers, so the convention is to publish the token as a subprotocol value alongside the real one (e.g. new WebSocket(url, ["iqauth.bearer." + token, "graphql-transport-ws"])).
import { WebSocketServer } from "ws";
import { verifyWsUpgrade } from "@iqauth/sdk/ws";

const wss = new WebSocketServer({ noServer: true });

httpServer.on("upgrade", async (req, socket, head) => {
  const result = await verifyWsUpgrade(req, {
    publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!,
    audience: "dispositioniq",
  });
  if (!result) {
    socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
    socket.destroy();
    return;
  }
  wss.handleUpgrade(req, socket, head, (ws) => {
    wss.emit("connection", ws, req, result.claims);
  });
});

A consumer can replace IQValidate's hand-rolled packages/shared/src/websocket-server.ts authenticateUser() with one import and one call.


Integration testing with createTestIssuer

@iqauth/sdk/test spawns an in-process HTTP server that exposes a JWKS endpoint, an OIDC discovery doc, a token endpoint that accepts code-exchange calls, and a /api/v1/auth/me userinfo endpoint. It mints valid RS256 JWTs against a freshly generated keypair, so integration tests don't need a live IQAuth.

import { createTestIssuer } from "@iqauth/sdk/test";

let issuer;
beforeAll(async () => { issuer = await createTestIssuer({ port: 0 }); });
afterAll(async () => { await issuer.close(); });

it("admin can list users", async () => {
  const token = issuer.mintToken({ sub: "u1", roles: ["tenant_admin"] });
  const r = await fetch("http://localhost:3000/api/users", {
    headers: { Authorization: `Bearer ${token}` },
  });
  expect(r.status).toBe(200);
});

Point your SDK / React provider at issuer.baseUrl — or pass issuer.publishableKey, which already encodes the right iss. To exercise the full code-exchange flow:

const code = issuer.mintAuthCode({ sub: "u1", roles: ["tenant_admin"] });
const tokens = await fetch(`${issuer.baseUrl}/oidc/token`, {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({ grant_type: "authorization_code", code }),
}).then((r) => r.json());

Out of scope: the test issuer does not mock SSO providers (Google, Microsoft) and does not replicate the full DispositionIQ admin/permissions API surface — it implements exactly enough to satisfy the SDK's verify path and the common code-exchange flow.


Hosted auth pages and branding

Sign-in, sign-up, forgot-password, and account pages are hosted at auth.dispositioniq.com. They render with your app's brand once you set CSS variables in the admin dashboard:

  • --brand-primary
  • --brand-accent
  • --brand-bg
  • --brand-surface
  • --brand-text

Validate the redirect contract from your own page with GET /api/public/apps/:appKey/sign-in-context — only return_to origins listed in your app's allowlist will be accepted.

If you'd rather embed the same components in your own app shell, the React entry point publishes them directly:

import { SignIn, SignUp, UserButton, UserProfile, OrganizationSwitcher } from "@iqauth/sdk/react";

CLI

npx iqauth init                              # bootstrap a new app + write IQAUTH_* keys to .env
npx iqauth doctor                            # check .env, issuer reachability, JWKS, redirect URI
npx iqauth keys list   --app <id>
npx iqauth keys rotate --app <id> --key-id <id> --yes
npx iqauth keys revoke --app <id> --key-id <id> --yes
npx iqauth dev                               # run the bundled React example with your key

init is the fastest way to provision a new app end-to-end: it creates the OIDC client, manifest, per-app origin allowlist, and a pk_/sk_ keypair, then writes them to .env. Key rotation has a 24-hour grace window — old and new keys both validate during that window.


Error handling

Every API failure throws IQAuthError with a stable code.

import { IQAuthError, ErrorCodes } from "@iqauth/sdk";

try {
  await client.auth.login(email, password);
} catch (err) {
  if (err instanceof IQAuthError) {
    switch (err.code) {
      case ErrorCodes.ACCOUNT_LOCKED:        return showLockoutBanner();
      case ErrorCodes.MFA_REQUIRED:          return promptMfa(err.details);
      case ErrorCodes.TENANT_SELECTION_REQUIRED: return promptTenant(err.details.tenants);
      case ErrorCodes.INVALID_CREDENTIALS:   return showWrongPassword();
      default:                               return showGenericError(err.message);
    }
  }
  throw err;
}

Common codes you'll handle:

| Code | When | |---|---| | INVALID_CREDENTIALS | Wrong email/password | | ACCOUNT_LOCKED | Too many failures; show cool-off | | MFA_REQUIRED | Continue to MFA step with details.mfaToken | | TENANT_SELECTION_REQUIRED | User belongs to >1 tenant; pick one | | TOKEN_EXPIRED | Access token aged out; refresh handled by SDK helpers | | TOKEN_INVALID | Token malformed or rotated out (transient under multi-tab) | | TOKEN_REVOKED / SESSION_REVOKED | Terminal — sign the user out | | USER_DEACTIVATED / USER_DISABLED / TENANT_SUSPENDED | Terminal — sign out | | INSUFFICIENT_PERMISSIONS | 403 — caller lacks required role/entitlement |


Troubleshooting

npx iqauth doctor is the first stop. It validates that the IQAUTH_* env vars are present and well-formed, the issuer responds, the JWKS endpoint serves keys, and your registered redirect URI matches what the SDK will send.

| Symptom | Likely cause | |---|---| | Silent sign-out within ~60s of login | Pre-2.0.3 cookie-clearing on transient TOKEN_INVALID. Upgrade to ≥ 2.0.3, set serverManagedSession: true on the React side. | | getAuth() returns null after first navigation in Next 15+ | Pre-2.0.2 sync-cookies bug. Upgrade to ≥ 2.0.2. | | /api/iqauth/callback returns 400 "redirect_uri mismatch" | Your registered redirect URI doesn't match the host the browser is on. Add it in the admin app config. | | Cross-origin call to a sister IQAuth-protected app gets CORS-blocked | Add the caller's origin to the target app's app_allowed_origins. Allow up to 60s for the cache to refresh. | | req.auth is undefined under Passport | Verify you're reading req.auth, not req.user. The SDK deliberately uses req.auth to avoid Passport collision. | | Hosted sign-in page won't accept your return_to | The origin isn't in this app's allowlist. Add it in the admin dashboard. | | Server logs [AUTH-MW] Missing auth credentials on GET /api/v1/auth/me at app boot — once, immediately followed by a successful [OIDC] SSO resume issued code AND the next /auth/me returns 200 | Benign. SDK's bootstrap() probe. See "Why does the server log Missing auth credentials at startup?" below. | | [AUTH-MW] Missing auth credentials repeats in a loop (warn → resume → token → warn → resume → token …), user can't actually finish login | NOT benign. The SDK's callback handler on YOUR app's domain isn't completing the token exchange, so no cookie ever gets set. See "When the bootstrap loop never ends" below — almost always a middleware-ordering bug on the consumer app. | | Same Missing auth credentials warning on /api/v1/auth/me but no subsequent SSO resume issued code or /oidc/token for the same user within ~1s | Real problem — your fetch is dropping cookies. Check credentials: "include", CORS Access-Control-Allow-Credentials: true, SameSite/COOKIE_DOMAIN. |

Why does the server log Missing auth credentials at startup?

When <IQAuthProvider> mounts, the SDK's bootstrap() runs GET /api/v1/auth/me once to ask the issuer "do I already have a session on this app?" That call is defined to be anonymous when the user has never signed in here yet — there is no iqauth_at cookie scoped to your origin yet to send.

So the very first request you'll see for a returning user is:

WARN  [AUTH-MW] Missing auth credentials  GET /api/v1/auth/me  origin=https://your-app.com
INFO  [OIDC]    SSO resume issued code     userId=…  clientId=iq_…
INFO  [AuthMetrics] /oidc/token latency

That sequence is the happy path for silent SSO: probe → no session here → resume from the issuer's iq_sso cookie → exchange code for tokens → cookie now set on your origin → subsequent requests succeed against the cookie. The warning was misleading log severity for a defined-anonymous endpoint, and as of the next IQAuth server release the bootstrap probe on /auth/me and /auth/refresh is logged at debug rather than warn. Other routes still log at warn — so seeing this message on, say, GET /api/v1/users remains a real signal that cookies aren't traveling.

If you're on an older IQAuth server and want to silence the noise without an upgrade, add a server-side log filter on the [AUTH-MW] Missing auth credentials line where route matches /api/v1/auth/(me|refresh). Don't blanket-mute the message — you'll lose visibility on real CORS/cookie failures.

When the bootstrap loop never ends

If you see Missing auth credentialsSSO resume issued code/oidc/token happen on repeat and the user never actually gets signed in, the issuer side is doing its job perfectly — the failure is on your server. The SDK's callback helper at /api/iqauth/callback is responsible for taking the ?code= from the OAuth redirect, POSTing it to /oidc/token, and setting the iqauth_at cookie on your app's domain. If that handler doesn't run (or returns an error), no cookie ever gets set and the SDK's next bootstrap probe is anonymous again — forever.

The single most common cause: your own auth middleware is intercepting /api/iqauth/callback before attachHelpers() can handle it. The OAuth return trip is a fresh GET from the issuer with no Authorization header (correctly so — that's the whole point of the callback), so an app.use(requireAuth) mounted globally will reject it as 401.

Confirm with these signals — if you see any of them, this is your bug:

  • Browser: GET https://your-app.com/api/iqauth/callback?code=… returns 401 (or any 4xx other than 302)
  • Browser DevTools → Application → Cookies for your app's domain: no iqauth_at cookie present after a sign-in attempt
  • Your app's server log shows your own auth middleware rejecting path=/iqauth/callback or /api/iqauth/callback with "missing authorization header"

Fix — pick one:

// ✅ Option 1 (recommended) — mount SDK helpers BEFORE your auth middleware.
const auth = iqAuth({ publishableKey, secretKey });
auth.attachHelpers(app);          // public: /api/iqauth/{callback,refresh,signout}
app.use(yourAuthMiddleware);      // anything below here requires a session

// ✅ Option 2 — exempt /api/iqauth/* from your own auth gate.
app.use((req, res, next) => {
  if (req.path.startsWith("/api/iqauth/")) return next();
  return yourAuthMiddleware(req, res, next);
});

After the fix you should see /api/iqauth/callback return 302, a Set-Cookie: iqauth_at=… header on the response, the cookie appearing under your app's domain in DevTools, and the next /api/v1/auth/me returning 200. The Missing auth credentials warning drops to the one harmless boot probe per fresh visitor.


Don't intercept /sign-in?code=…

If your app uses an SSO bridge route (e.g. your-app.com/sign-in redirects users to auth.dispositioniq.com) and that same route is also the configured OAuth redirect_uri, your bridge logic will fire on the return trip and immediately bounce the user back to the issuer before the SDK can exchange the ?code= for tokens. Symptoms: an infinite redirect loop, or a successful login that the app never sees.

Pick one:

  1. Recommended — use a dedicated callback path. Configure redirect_uri to point at /api/iqauth/callback (the helper route mounted by iqAuth({...}).attachHelpers(app) for Express, or by iqAuth({...}) for Next/Fastify/Hono). Your /sign-in route stays purely a "send user to the issuer" bridge.

  2. If you must reuse /sign-in as the redirect target, guard the bridge:

    app.get("/sign-in", (req, res, next) => {
      // OAuth return trip — let the SDK handle it, don't redirect away.
      if (req.query.code || req.query.error) return next();
      return res.redirect(buildIssuerAuthorizeUrl(req));
    });

    …and add inlineCallback: true to your iqAuth({...}) options so a GET handler is mounted on the callback path to complete the exchange and 302 to the final destination.

This is the single most common bug we see when teams add IQAuth to an app that already had its own session redirect logic.


Migrating from Clerk's backend SDK

If you're moving from @clerk/backend to @iqauth/sdk, the surface is intentionally close but not identical. The full audit lives at docs/backend-sdk-parity.md; the high-leverage deltas:

  • Vocabulary. Clerk's organization is IQAuth's tenant. Clerk.users.banUser(id)iqauth.users.deactivate(id); unbanUserreactivate. The IQAuth modules are tenants, memberships, roles, invites — together they cover what Clerk packs into organizations.
  • Tenant scoping is explicit. users.create takes (tenantId, data) rather than implying it from the API key. List endpoints (users.list, tenants.list, memberships.listForTenant) accept tenant filters.
  • Pagination + filters are still partial. users.list({ email, tenantId }) and tenants.list({ vendorId }) work today, but Clerk-style { limit, offset, query, orderBy } is on the near-term roadmap. Bulk-import scripts that page through tens of thousands of users should call the REST API directly until the helpers ship.
  • Admin-side user mutations. users.update(...) in this SDK currently updates only the calling user (name, picture). Admin-on-behalf-of update / delete / verifyPassword and a passwordDigest import path for users.create are tracked as follow-ups; until then, use the REST API or the admin console.
  • Invitations. invites.create / validate / accept are present. invites.list (pending) and invites.revoke are not in the SDK yet — same for the equivalent organization-invitation calls; tracked as follow-ups.
  • Sessions. sessions.list / revoke / revokeAll are scoped to the current user. There is no admin-side sessions.getSessionList({ userId }) yet; tracked as a follow-up.
  • Webhook signature verification. Clerk wraps Svix's Webhook.verify(payload, headers). IQAuth ships endpoint CRUD + delivery history + secret rotation, but receivers must verify HMAC signatures by hand today. A webhooks.verifySignature(...) helper is in flight under the existing rotation task.
  • Actor / impersonation tokens. Tracked separately (see internal task F23 / #86). Not yet in the SDK.
  • Won't do. JWT templates and per-instance domain CRUD are Clerk-specific and don't map to IQAuth's product surface. See the audit doc for documented alternatives.

For anything in the "won't do" / "different shape" rows of the audit, prefer the linked alternative over reaching back into REST — those alternatives are the supported path.


Bundled docs

Long-form integration guides ship inside the npm tarball at node_modules/@iqauth/sdk/docs/. List them with:

ls node_modules/@iqauth/sdk/docs
ls node_modules/@iqauth/sdk/docs/guides
ls node_modules/@iqauth/sdk/docs/integration-prompts

Highlights:

  • docs/APP_INTEGRATION_MATRIX.md — which entry point + pattern for which app archetype
  • docs/FRESH_IMPLEMENTATION_GUIDE.md — green-field walkthrough for a new app
  • docs/BROWSER_SESSION_MIGRATION.md — moving a browser-token-owning app to cookie-managed sessions (now includes the 2.0.3 serverManagedSession recipe)
  • docs/V1_TO_V2_UPGRADE_GUIDE.md — upgrading from 1.x
  • docs/TARBALL_RELEASE_WORKFLOW.md — internal: shipping prebuilt tarballs to consumer apps
  • docs/guides/auth-flows.md, session-management.md, mfa-enrollment.md, roles-and-permissions.md, scoped-authorization.md, entity-hierarchy.md, tenant-management.md, user-management.md, invitations.md, branding.md, webhooks.md, entitlements.md, api-keys.md, mobile-native.md, server-platform-integration.md, service-automation-integration.md, token-verification.md, middleware-reference.md, error-handling.md, gdpr-compliance.md, app-registration.md
  • docs/integration-prompts/{first-party-browser-app,native-mobile-app,server-platform-app,service-automation-app,install-from-tarball,migrate-from-local-packages-source}.md — drop-in prompts for AI-assisted integration

These are also kept on the IQAuth admin dashboard's documentation tab.


License

Proprietary — DispositionIQ internal use. See the LICENSE/usage terms in the bundled docs.


Deploying with Docker / Next.js (read this before you ship)

The publishable key is baked into your build. NEXT_PUBLIC_* and VITE_* variables are inlined at build time, not run time. A docker image built against your staging issuer will continue to point at staging even when you later run it with -e NEXT_PUBLIC_IQAUTH_PUBLISHABLE_KEY=pk_live_xyz — Next.js / Vite have no way to re-resolve the constant after the bundle is emitted.

Two safe patterns:

  1. Build the image per-environment (recommended for separate prod/stage stacks). Use ARG and ENV in the Dockerfile and pass --build-arg NEXT_PUBLIC_IQAUTH_PUBLISHABLE_KEY=pk_live_xyz at docker build time.

  2. Read the key at request time on the server (single image, multi-env): fetch the publishable key in getServerSideProps / a Next.js Route Handler / your Express bootstrap and pass it to as a runtime prop. Server-side env vars ARE re-read on every container start, so the same image deploys to any environment.

Verify the build before you push: the iqauth doctor CLI compares the issuer encoded in your publishable key against the issuers discovery document and warns when they disagree:

npx iqauth doctor --issuer https://auth.example.com --publishable-key $NEXT_PUBLIC_IQAUTH_PUBLISHABLE_KEY

Wire this into CI right after next build and youll catch a wrong-environment publishable key before the image ships.