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

@digitwhale_innovations/digitwhale-auth

v0.10.0

Published

Digitwhale Auth SDK - OAuth 2.0 Authorization Code Flow with PKCE

Readme

Digitwhale Auth — TypeScript SDK

OAuth 2.0 Authorization Code Flow with PKCE for TypeScript/JavaScript applications.

Installation

npm install @digitwhale_innovations/digitwhale-auth

Quick Start

import { DigitwhaleAuth, MemoryTokenStore } from "@digitwhale_innovations/digitwhale-auth";

const auth = new DigitwhaleAuth(new MemoryTokenStore());

const { url, pkce, state } = await auth.buildAuthorizeUrl(
  "your_client_id",
  "yourapp://callback",
  "read write"
);

console.log("Open this URL:", url);

const tokens = await auth.exchangeCode(
  "authorization_code_from_callback",
  "yourapp://callback",
  "your_client_id",
  pkce
);

console.log("Logged in:", tokens.user);

Import Paths

| What you need | Import from | |---|---| | Core SDK (any runtime) | "@digitwhale_innovations/digitwhale-auth" | | React components / hooks | "@digitwhale_innovations/digitwhale-auth/react" |

The @digitwhale_innovations/digitwhale-auth entry point has zero React dependency — safe for Node backends, React Native without Expo, Deno, and browsers without React.

React components live in @digitwhale_innovations/digitwhale-auth/react and require react >= 17 as a peer dependency.

Extending Types

All types (UserInfo, StoredTokens, TokenResponse) include an index signature so you can add custom fields from your own system:

import type { UserInfo } from "@digitwhale_innovations/digitwhale-auth";

// Extend with your own fields
interface MyUser extends UserInfo {
  department?: string;
  employee_id?: number;
  roles?: string[];
}

// The server returns extra fields — they're captured automatically
const user: MyUser = await auth.user();
console.log(user.department);  // typed as string | undefined
console.log(user.roles);       // typed as string[] | undefined

Or access custom fields directly without extending:

const user = await auth.user();
const department = user.department as string | undefined;

Scenarios

1. One-Click Button (Recommended)

A React component that handles everything — popup, redirect, code exchange:

import { DigitwhaleAuth, LocalStorageTokenStore } from "@digitwhale_innovations/digitwhale-auth";
import { DigitwhaleAuthButton } from "@digitwhale_innovations/digitwhale-auth/react";

const auth = new DigitwhaleAuth(new LocalStorageTokenStore());

<DigitwhaleAuthButton
  auth={auth}
  clientId="my_web_app"
  redirectUri="https://myapp.com/auth/callback"
  scope="read write"
  onSuccess={(tokens) => {
    console.log("Logged in:", tokens.user?.email);
    router.push("/dashboard");
  }}
  onError={(error) => {
    toast.error(error.message);
  }}
/>

2. React Hook

Full control with useDigitwhaleAuth:

import { DigitwhaleAuth, LocalStorageTokenStore } from "@digitwhale_innovations/digitwhale-auth";
import { useDigitwhaleAuth } from "@digitwhale_innovations/digitwhale-auth/react";

const auth = new DigitwhaleAuth(new LocalStorageTokenStore());

function LoginPage() {
  const { login, logout, user, isAuthenticated, isLoading, error } =
    useDigitwhaleAuth(auth);

  if (isAuthenticated) {
    return (
      <div>
        <p>Welcome, {user?.email}</p>
        <button onClick={logout}>Sign out</button>
      </div>
    );
  }

  return (
    <div>
      {error && <p className="error">{error}</p>}
      <button
        onClick={() => login("my_app", "https://myapp.com/auth/callback")}
        disabled={isLoading}
      >
        {isLoading ? "Signing in..." : "Sign in with Digitwhale"}
      </button>
    </div>
  );
}

3. Web — Popup Flow

Opens a popup window for the auth flow, closes on success:

const { url, pkce, state } = await auth.buildAuthorizeUrl(
  "my_app",
  "https://myapp.com/auth/callback"
);

const popup = window.open(url, "auth", "width=600,height=700,popup=yes");

// Poll for redirect
const interval = setInterval(async () => {
  if (popup?.closed) {
    clearInterval(interval);
    await auth.restoreSession();
    return;
  }

  try {
    if (popup.location.href.startsWith("https://myapp.com/auth/callback")) {
      const params = new URLSearchParams(popup.location.search);
      const code = params.get("code");
      if (code) {
        await auth.exchangeCode(code, redirectUri, clientId, pkce);
        popup.close();
        clearInterval(interval);
      }
    }
  } catch {
    // Cross-origin
  }
}, 500);

4. Web — Redirect Flow

Redirects the browser to the auth page:

const { url, pkce, state } = await auth.buildAuthorizeUrl(
  "my_app",
  "https://myapp.com/auth/callback"
);

// Store pkce and state for later
sessionStorage.setItem("pkce", JSON.stringify(pkce));
sessionStorage.setItem("state", state);

// Redirect
window.location.href = url;
// On callback page (/auth/callback)
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
const state = params.get("state");

const storedState = sessionStorage.getItem("state");
if (state !== storedState) {
  throw new Error("Invalid state");
}

const pkce = JSON.parse(sessionStorage.getItem("pkce")!);
await auth.exchangeCode(code!, redirectUri, clientId, pkce);
router.push("/dashboard");

5. React Native / Expo

import * as Linking from "expo-linking";

const { url, pkce, state } = await auth.buildAuthorizeUrl(
  "my_app",
  "myapp://auth/callback"
);

// Open in browser
Linking.openURL(url);

// Listen for deep link
Linking.addEventListener("url", async ({ url }) => {
  const uri = new URL(url);
  const code = uri.searchParams.get("code");
  const returnedState = uri.searchParams.get("state");

  if (returnedState !== state) {
    console.error("CSRF detected");
    return;
  }

  if (code) {
    const tokens = await auth.exchangeCode(
      code,
      "myapp://auth/callback",
      "my_app",
      pkce
    );
    console.log("Logged in:", tokens.user?.email);
  }
});

6. Express Backend

app.get("/auth/login", async (req, res) => {
  const { url, pkce, state } = await auth.buildAuthorizeUrl(
    "server_app",
    "https://myapp.com/auth/callback"
  );
  req.session.pkce = pkce;
  req.session.state = state;
  res.redirect(url);
});

app.get("/auth/callback", async (req, res) => {
  const { code, state } = req.query;
  if (state !== req.session.state) return res.status(403).send("Invalid state");

  await auth.exchangeCode(
    code as string,
    "https://myapp.com/auth/callback",
    "server_app",
    req.session.pkce
  );
  res.redirect("/dashboard");
});

7. Error Handling

import { AuthError, ErrorType } from "@digitwhale_innovations/digitwhale-auth";

try {
  await auth.exchangeCode(code, redirectUri, clientId, pkce);
} catch (err) {
  if (err instanceof AuthError) {
    switch (err.type) {
      case ErrorType.CodeExpired:
        showNotification("Code expired, please try again");
        break;
      case ErrorType.PkceFailed:
        showNotification("Security verification failed");
        break;
      case ErrorType.MfaRequired:
        showNotification("MFA code required — re-initiate with MFA");
        break;
      case ErrorType.RefreshTokenRevoked:
        showNotification("Session compromised, logging out");
        await auth.signOut();
        break;
      case ErrorType.TokenFamilyRevoked:
        showNotification("All tokens revoked — re-authenticate");
        await auth.signOut();
        break;
      default:
        if (err.retryable) {
          showNotification("Network error, retrying...");
        } else {
          showNotification(`Auth error: ${err.message}`);
        }
    }
  }
}

8. Confidential Clients (server-side)

// For confidential clients, pass client_secret in the exchangeCode call
const tokens = await auth.exchangeCode(
  code,
  "https://myapp.com/auth/callback",
  "server_app",
  pkce,
  "sec_xxxxx" // clientSecret
);

9. Fetch User Info

const user = await auth.user();
console.log(`Logged in as: ${user.first_name} ${user.last_name}`);

10. Update User Profile

const updated = await auth.updateUser({
  first_name: "Jane",
  last_name: "Doe",
  phone_number: "+1234567890",
  date_of_birth: "1990-01-01",
  nationality: "US",
});

11. Change Password

await auth.changePassword("current_pass", "new_pass", "new_pass");
// All existing tokens are revoked — user must re-authenticate

12. Refresh Tokens

// Explicitly refresh (normally handled automatically by getAccessToken)
const refreshed = await auth.refresh();
console.log(`New token expires at: ${new Date(refreshed.expiresAt).toISOString()}`);

13. Logout

// Revoke token on server + clear local storage
await auth.logout();

14. Sign Out (local only)

// Clear local tokens without server revocation
await auth.signOut();

Session Idle Timeout (automatic token expiry)

Tokens are expired and deleted on next use if they have not been used within a configurable window. This is enforced lazily — there is no background job; every time a token is requested the SDK checks the last-used timestamp — and it protects against stolen or idle tokens (e.g. an XSS-grabbed token that is never replayed while the user is active gets purged after the window).

// Default is 10 minutes. Override with idleTimeoutMs (here: 5 minutes).
const auth = new DigitwhaleAuth(new LocalStorageTokenStore(), {
  idleTimeoutMs: 5 * 60 * 1000,
});

When Date.now() - lastUsedAt > idleTimeoutMs, the SDK clears the stored tokens and throws an AuthError with type: ErrorType.TokenExpired (requiresReauth === true). Hook that to bounce the user back to login:

try {
  const token = await auth.getAccessToken();
  // ...use token...
} catch (err) {
  if (err instanceof AuthError && err.requiresReauth) {
    // session idle-expired (or token revoked) — send back to login
    router.push("/login");
  }
}

The idle clock only advances while the app is actively using tokens, so a genuinely active session never expires mid-use.

Passkeys (WebAuthn)

Passwordless login & registration using device biometrics / PIN / security keys. The same passkey works for both normal auth and the OAuth authorize flow (pass oauth to receive a code instead of local tokens).

Browser ceremony (recommended)

The SDK runs navigator.credentials.get/create for you — no manual byte handling required:

// Login
const res = await auth.authenticateWithPasskey({ identifier: "[email protected]" });
// res is PasskeyLoginResult (tokens) or PasskeyOAuthResult (code + redirect)

// Register a new passkey for the signed-in user
await auth.registerPasskey("My Laptop");

OAuth passkey — receive a code instead of tokens

const res = await auth.authenticateWithPasskey({
  identifier: "[email protected]",
  oauth: {
    client_id,
    redirect_uri,
    state,
    scope,
    code_challenge,
    code_challenge_method: "S256",
  },
});
// res.redirect -> send the user's browser there (it carries ?code=…)

Lower-level (manual ceremony)

Use these when you want to run the ceremony yourself (custom UI, non-browser contexts):

import { getPasskeyAssertion, createPasskey } from "@digitwhale_innovations/digitwhale-auth/webauthn";

const options = await auth.passkeyLoginOptions(identifier);
const assertion = await getPasskeyAssertion(options);
const res = await auth.passkeyLogin(assertion, oauth?);        // normal or OAuth

const regOptions = await auth.passkeyRegisterOptions();
const credential = await createPasskey(regOptions);
await auth.passkeyRegister(credential);

Manage passkeys

const passkeys = await auth.listPasskeys();
await auth.deletePasskey(passkeys[0].id);

Backend Integration (server-side / confidential clients)

If your app has its own backend, run the OAuth flow there as a confidential client: the backend holds the client_secret (never shipped to the browser) and performs the token exchange. The SDK also doubles as a typed API client for the auth service, so once you hold a user's tokens you can read their profile, manage their passkeys, etc.

1. Store tokens per user session

Never use LocalStorageTokenStore on a server. Implement TokenStore against your session/DB. Here's an in-memory map keyed by session id (swap the Map for Redis/your DB):

import type { TokenStore, StoredTokens } from "@digitwhale_innovations/digitwhale-auth";

const sessions = new Map<string, StoredTokens>();

function storeFor(sessionId: string): TokenStore {
  return {
    async save(t) { sessions.set(sessionId, t); },
    async load() { return sessions.get(sessionId) ?? null; },
    async clear() { sessions.delete(sessionId); },
  };
}

2. OAuth callback — exchange the code with the client secret

// GET /auth/callback
app.get("/auth/callback", async (req, res) => {
  const { code, state } = req.query;
  if (state !== req.session.state) return res.status(403).send("Invalid state");

  const auth = new DigitwhaleAuth(storeFor(req.session.id), {
    idleTimeoutMs: 30 * 60 * 1000, // longer window on a server
  });

  await auth.exchangeCode(
    code as string,
    "https://myapp.com/auth/callback",
    "server_app",
    req.session.pkce,   // PKCE verifier kept server-side in the session
    "sec_xxxxx",         // clientSecret — required for confidential clients
  );
  res.redirect("/dashboard");
});

PKCE still applies: the backend stores the code_verifier in the session and sends it at exchange; the client_secret is additionally required for confidential clients (oauth.py:245).

3. Use the SDK as the auth-service API client

With a user's tokens loaded, call any authenticated endpoint on their behalf:

const auth = new DigitwhaleAuth(storeFor(req.session.id));
const user = await auth.user();
const passkeys = await auth.listPasskeys();
await auth.deletePasskey(passkeys[0].id);
await auth.changePassword(current, next, next);

4. Idle timeout on the backend

On a server you usually manage expiry via your own session, so set a longer idleTimeoutMs (or rely on your session TTL). The SDK still auto-refreshes access tokens and forces re-auth (requiresReauth) when a token is truly dead.

Rate Limiting

The auth server enforces rate limits per IP address. If you hit a 429 Too Many Requests, wait and retry. The SDK does not automatically retry — handle this in your application code.

Token Revocation

When a user changes their password or deletes their account, all existing tokens are revoked. The SDK will receive a 401 Unauthorized on the next API call. Handle this by calling auth.signOut() and redirecting to the login page.