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

tokensmith

v0.1.0

Published

Framework-agnostic JWT token management — storage, auto-refresh, cross-tab sync, and reactive auth state

Downloads

196

Readme

You handle authentication. We handle the tokens after.

npm CI Bundle Size License: MIT TypeScript

Every app using JWTs ends up writing the same client-side logic: store tokens securely, refresh them before expiry, deduplicate concurrent refresh calls, keep browser tabs in sync, update the UI reactively. TokenSmith handles all of it — framework-agnostic, zero dependencies.


Table of Contents

Why TokenSmith

  • Zero boilerplate — one createTokenManager() call replaces hundreds of lines of manual token handling written fresh for every project
  • Auto-refresh that just works — tokens refresh before expiry; concurrent callers share one request, no matter how many
  • Cross-tab sync — logout in one tab logs out all tabs instantly via BroadcastChannel, with automatic localStorage fallback
  • React-readyuseSyncExternalStore-backed useAuth() hook; concurrent-mode safe, no extra renders, no tearing
  • Zero dependencies — no supply chain risk; tree-shakeable ESM + CJS; cookie storage with SameSite=Strict; Secure by default; ~5 KB gzipped

Install

npm install tokensmith

The React adapter ships in the same package — no separate install needed.

What TokenSmith replaces

Without TokenSmith, every project ships some version of this:

let refreshPromise: Promise<string> | null = null;

async function getValidToken(): Promise<string | null> {
  const token = localStorage.getItem('access_token');
  if (token && !isExpired(token)) return token;

  if (!refreshPromise) {
    refreshPromise = fetch('/api/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken: localStorage.getItem('refresh_token') }),
    })
      .then((r) => r.json())
      .then((data) => {
        localStorage.setItem('access_token', data.accessToken);
        localStorage.setItem('refresh_token', data.refreshToken);
        refreshPromise = null;
        return data.accessToken;
      });
  }
  return refreshPromise;
}

With TokenSmith:

const auth = createTokenManager({ refresh: { endpoint: '/api/auth/refresh' } });
const token = await auth.getAccessToken(); // auto-refreshes, deduplicates, retries with backoff

Plus: cross-tab sync, React hooks, typed errors, SSR support, and offline-awareness — all included.

Quick Start

import { createTokenManager } from 'tokensmith';

const auth = createTokenManager({
  refresh: { endpoint: '/api/auth/refresh' },
});

// After login — store the token pair
auth.setTokens({ accessToken: '...', refreshToken: '...' });

// Get a valid token (auto-refreshes if expired)
const token = await auth.getAccessToken();

// Or use the built-in fetch wrapper
const authFetch = auth.createAuthFetch();
const res = await authFetch('/api/data'); // Authorization: Bearer <token> attached automatically

React

import { createTokenManager } from 'tokensmith';
import { TokenProvider, useAuth } from 'tokensmith/react';

// Create once at module scope — not inside a component
const auth = createTokenManager({
  refresh: { endpoint: '/api/auth/refresh' },
});

function App() {
  return (
    <TokenProvider manager={auth}>
      <Dashboard />
    </TokenProvider>
  );
}

function Dashboard() {
  const { isAuthenticated, user, logout } = useAuth();

  if (!isAuthenticated) return <Login />;

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

Protected routes

import { Navigate } from 'react-router-dom';
import { useAuth } from 'tokensmith/react';

function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { isAuthenticated } = useAuth();
  return isAuthenticated ? <>{children}</> : <Navigate to="/login" replace />;
}

Core API

createTokenManager<TUser>(config?) returns a TokenManager<TUser>:

| Method | Returns | Description | |--------|---------|-------------| | setTokens(tokens) | void | Store access + optional refresh token after login | | getAccessToken() | Promise<string \| null> | Get a valid token; auto-refreshes if expired | | getUser() | TUser \| null | Decoded JWT payload; null if expired or unauthenticated | | isAuthenticated() | boolean | true when a non-expired access token is stored | | getState() | AuthState<TUser> | Current auth state snapshot | | onAuthChange(listener) | () => void | Subscribe to state changes; returns unsubscribe function | | logout() | void | Clear all tokens, cancel refresh timer, notify listeners | | getAuthHeader() | Promise<{ Authorization: string } \| {}> | Bearer header for axios interceptors and similar | | createAuthFetch() | typeof fetch | Fetch wrapper: auto-attaches Authorization, retries once on 401 | | fromCookieHeader(header) | TokenPair \| null | Extract tokens from a raw Cookie: header string (SSR; works with any storage backend) | | destroy() | void | Cancel timers, close channels, clear all listeners |

TokenPair

interface TokenPair {
  accessToken: string;
  refreshToken?: string; // optional — omit if your backend only issues access tokens
}

AuthState<TUser>

interface AuthState<TUser = Record<string, unknown>> {
  isAuthenticated: boolean;
  user: TUser | null;
  accessToken: string | null;
  isRefreshing: boolean;
  error: TokenSmithError | null;
}

error is set when a refresh fails and cleared on the next successful state transition (e.g. setTokens, logout). To handle auth failures durably, use the onAuthFailure callback in your config.

Subscribing to changes

onAuthChange registers a listener that fires on every auth state transition. It returns an unsubscribe function.

const unsubscribe = auth.onAuthChange((state) => {
  console.log('authenticated:', state.isAuthenticated);
  console.log('user:', state.user);

  if (state.error) {
    console.error('auth error:', state.error.code);
  }
});

// Remove the listener when no longer needed
unsubscribe();

Axios interceptors

Use getAuthHeader() to attach a Bearer token to every Axios request:

import axios from 'axios';

const client = axios.create({ baseURL: '/api' });

client.interceptors.request.use(async (config) => {
  const header = await auth.getAuthHeader(); // {} when unauthenticated
  Object.assign(config.headers, header);
  return config;
});

Cleanup

Call destroy() when the manager is no longer needed to cancel background timers, close the BroadcastChannel, and clear all listeners:

// In a Vite app with hot module replacement
if (import.meta.hot) {
  import.meta.hot.dispose(() => auth.destroy());
}

// In a framework with a teardown lifecycle
onUnmount(() => auth.destroy());

React API

<TokenProvider manager={auth}>

Provides the TokenManager to the component tree. Pass the manager as a prop — TokenSmith never creates it internally, so manager creation stays outside React's lifecycle.

interface TokenProviderProps {
  manager: TokenManager;
  children: ReactNode;
}

useAuth<TUser>()

const {
  isAuthenticated, // boolean
  user,            // TUser | null
  accessToken,     // string | null
  isRefreshing,    // boolean — true during an active token refresh
  error,           // TokenSmithError | null — set when all refresh retries fail
  getAccessToken,  // () => Promise<string | null>
  logout,          // () => void
} = useAuth<UserPayload>();

Backed by useSyncExternalStore — concurrent-mode safe, no tearing, no extra renders.

useTokenManager<TUser>()

Returns the raw TokenManager instance from context. Use this to build custom hooks on top of TokenSmith.

const manager = useTokenManager();
const authFetch = manager.createAuthFetch();

Configuration

createTokenManager(config?) accepts a TokenManagerConfig object with the following options:

const auth = createTokenManager({
  // Storage backend: 'cookie' (default), 'memory', 'localStorage', or a custom StorageAdapter
  storage: 'cookie',

  // Cookie-specific options (only applies when storage is 'cookie')
  cookie: {
    path: '/',
    domain: '.example.com',
    sameSite: 'strict',        // 'strict' | 'lax' | 'none'
    secure: true,              // auto-detected from location.protocol when omitted
    maxAge: 'auto',            // derives expiry from JWT exp claim; omit for session cookie
  },

  // Auto-refresh configuration
  refresh: {
    endpoint: '/api/auth/refresh', // URL — sends POST with { refreshToken } body
    handler: async (rt) => {},     // or a custom async handler (use one, not both)
    buffer: 60,                    // seconds before expiry to refresh (default: 60)
    maxRetries: 3,                 // retry attempts on transient failure (default: 3)
    retryDelay: 1000,              // base delay in ms with exponential backoff + jitter (default: 1000)
    headers: {},                   // extra headers on refresh requests
    fetchOptions: {},              // extra RequestInit options (e.g. { credentials: 'include' })
  },

  // Cross-tab sync (default: true; automatically disabled for memory storage)
  syncTabs: true,

  // BroadcastChannel name for sync (default: 'tokensmith')
  syncChannelName: 'tokensmith',

  // Called when a refresh fails (retries exhausted, 401/403 rejection, or no refresh token)
  onAuthFailure: () => {},
});

Storage Backends

| Value | Description | XSS Risk | Persistence | |-------|-------------|----------|-------------| | 'cookie' (default) | document.cookie with SameSite=Strict; Secure | Low (readable by JS) | Persists | | 'memory' | In-memory Map | None | Lost on page refresh | | 'localStorage' | window.localStorage with tk_ prefix | High | Persists | | Custom StorageAdapter | Implement get / set / remove / clear | Yours | Yours |

Custom adapter

const auth = createTokenManager({
  storage: {
    get: (key) => sessionStorage.getItem(key),
    set: (key, value) => sessionStorage.setItem(key, value),
    remove: (key) => sessionStorage.removeItem(key),
    clear: () => ['tk_access', 'tk_refresh'].forEach((k) => sessionStorage.removeItem(k)),
  },
});

Cookie options

Token cookies are stored as tk_access and tk_refresh.

const auth = createTokenManager({
  cookie: {
    path: '/',
    domain: '.example.com',  // omit for current domain
    sameSite: 'strict',      // 'strict' | 'lax' | 'none'
    secure: true,            // auto-detected from location.protocol when omitted
    maxAge: 'auto',          // derives expiry from JWT exp claim; omit for session cookie
  },
});

Cross-Tab Sync

Cross-tab sync is enabled by default for persistent storage backends (cookie and localStorage). Logout, login, and token refresh in one tab are reflected in all other tabs instantly via BroadcastChannel, with a localStorage storage event fallback for older browsers.

Disabling sync

Set syncTabs: false to opt out. When disabled, each tab operates independently — a logout in one tab will not affect other tabs.

const auth = createTokenManager({
  syncTabs: false,
});

Sync is always disabled automatically when using memory storage, since there is no shared state between tabs.

Custom channel name

If your application creates multiple TokenManager instances (e.g. separate user and admin sessions), give each one a unique channel name to prevent cross-talk:

const userAuth = createTokenManager({
  syncChannelName: 'tokensmith:user',
});

const adminAuth = createTokenManager({
  syncChannelName: 'tokensmith:admin',
});

The default channel name is 'tokensmith'.

Auto-Refresh

Endpoint protocol

When refresh.endpoint is set, TokenSmith sends:

POST <endpoint>
Content-Type: application/json

{ "refreshToken": "<token>" }

The endpoint must respond with { "accessToken": "...", "refreshToken": "..." }. The refreshToken field in the response is optional — include it to rotate the refresh token on each use.

Configuration

const auth = createTokenManager({
  refresh: {
    // Option A: URL
    endpoint: '/api/auth/refresh',

    // Option B: Custom handler (use for non-standard request formats, auth headers, etc.)
    handler: async (refreshToken) => {
      const res = await fetch('/api/auth/refresh', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refreshToken }),
      });
      return res.json(); // must return { accessToken, refreshToken? }
    },

    buffer: 60,       // refresh 60s before expiry (default: 60)
    maxRetries: 3,    // retry attempts on failure (default: 3)
    retryDelay: 1000, // base delay in ms; exponential backoff with jitter (default: 1000)
    headers: {        // extra headers sent with every refresh request
      'x-client-id': 'my-app',
    },
    fetchOptions: {   // extra RequestInit options (credentials, cache, etc.)
      credentials: 'include',
    },
  },
  onAuthFailure: () => {
    // Called when a refresh fails for any reason (retries exhausted, 401/403, no refresh token)
    window.location.href = '/login';
  },
});

getAccessToken() and createAuthFetch() both trigger an on-demand refresh if the access token is expired. Concurrent callers share one refresh request — the underlying function is called exactly once regardless of how many callers are waiting.

Retry backoff uses exponential delay with random jitter to prevent synchronized retry storms across tabs. If the refresh endpoint returns 401 or 403, retries are skipped entirely — the token has been rejected and retrying will never succeed. onAuthFailure fires immediately in this case.

Auto-refresh is offline-aware: if navigator.onLine is false, the retry is deferred until the online event fires rather than burning through retry attempts.

SSR Support

fromCookieHeader() parses a raw Cookie: request header string and returns the token pair. It works with any storage backend, so you can safely use memory storage on the server (where document is unavailable).

import { createTokenManager } from 'tokensmith';

// Next.js, Express, or any server framework — use memory storage on the server
const auth = createTokenManager({ storage: 'memory' });

const tokens = auth.fromCookieHeader(request.headers.get('cookie') ?? '');
if (tokens) {
  auth.setTokens(tokens);
}

const user = auth.getUser(); // typed payload or null

On the client, after hydration, useAuth() immediately reflects the browser's actual auth state. For seamless SSR without a flash of unauthenticated content, initialize the server-side manager with fromCookieHeader before rendering.

TypeScript

Pass a type argument to createTokenManager<TUser> for fully typed user and getUser() return values:

interface UserPayload {
  sub: string;
  email: string;
  role: 'admin' | 'user';
}

const auth = createTokenManager<UserPayload>({
  refresh: { endpoint: '/api/auth/refresh' },
});

const user = auth.getUser(); // UserPayload | null

In React:

const { user } = useAuth<UserPayload>(); // user: UserPayload | null

Errors

TokenSmith exports typed error classes for every failure mode:

| Class | Code | When thrown | |-------|------|-------------| | TokenSmithError | — | Base class; all errors extend this | | InvalidTokenError | INVALID_TOKEN | Malformed JWT — wrong segment count, invalid base64, or non-JSON payload | | TokenExpiredError | TOKEN_EXPIRED | Not thrown internally — exported for use in custom refresh handlers or storage adapters | | RefreshFailedError | REFRESH_FAILED | All retry attempts exhausted; has an attempts: number property | | StorageError | STORAGE_ERROR | Storage unavailable (SSR context, quota exceeded, private browsing) | | NetworkError | NETWORK_ERROR | Network request failed |

import { RefreshFailedError, TokenSmithError } from 'tokensmith';

auth.onAuthChange((state) => {
  if (state.error instanceof RefreshFailedError) {
    console.error(`Refresh failed after ${state.error.attempts} attempts`);
  }
  if (state.error instanceof TokenSmithError) {
    console.error(`Auth error [${state.error.code}]:`, state.error.message);
  }
});

Security

TokenSmith decodes JWTs client-side but does not validate signatures. Always verify token signatures server-side using a library like jose or your framework's JWT middleware. Never rely solely on the client-decoded payload for authorization decisions.

| Storage | XSS Risk | CSRF Risk | Persistence | Recommended For | |---------|----------|-----------|-------------|-----------------| | memory | None | None | Lost on refresh | SPAs with a server-side refresh endpoint | | cookie | Readable by JS | Mitigated by SameSite=Strict | Persists | Most web apps (default) | | localStorage | Full exposure | None | Persists | Only when the trade-off is acceptable |

TokenSmith cookies are not HttpOnly — the access token must be readable by JavaScript. For true HttpOnly refresh tokens, pass a custom StorageAdapter backed by a server-side token endpoint.

Memory storage is the most XSS-resistant option. Pair it with refresh.endpoint so the access token can be restored from the persisted refresh token on page load.

Maximum security configuration

The most XSS-resistant setup stores the access token in memory and relies on a server-set HttpOnly cookie as the refresh credential — the cookie is never readable by JavaScript:

const auth = createTokenManager({
  storage: 'memory',
  refresh: {
    handler: async () => {
      const res = await fetch('/api/auth/refresh', {
        method: 'POST',
        credentials: 'include', // sends the HttpOnly refresh cookie automatically
      });
      return res.json(); // { accessToken: string }
    },
  },
});
  • Access token — lives only in memory; XSS cannot read it from storage
  • Refresh token — a server-set HttpOnly cookie; invisible to JavaScript entirely
  • On page reload — memory is empty, so the consumer must bootstrap the session on mount (e.g. call the refresh endpoint directly and pass the result to setTokens). No token is ever stored in script-accessible storage

Compatibility

| Target | Requirement | |--------|-------------| | Browsers | Chrome 80+, Firefox 80+, Safari 15+, Edge 80+ | | Node.js | 18+ (SSR contexts) | | React | 18+ (useSyncExternalStore) | | TypeScript | 5.0+ | | Module formats | ESM + CommonJS |

Cross-tab sync uses BroadcastChannel. In environments where it is unavailable, TokenSmith falls back to a localStorage storage event automatically.

Bundle Size

| Entry | Gzipped | |-------|---------| | tokensmith | ~5 KB | | tokensmith/react | <1 KB |

Zero runtime dependencies. The React adapter wraps React's built-in useSyncExternalStore — no extra runtime code.

Examples

The examples/ directory contains a complete full-stack demo:

| App | Stack | What it shows | |-----|-------|---------------| | react-app | Vite + React 19 + React Router | Login, protected routes, auto-refresh countdown, cross-tab sync | | nestjs-api | NestJS 11 + Passport/JWT | Auth API: /auth/login, /auth/refresh, /auth/profile |

The React example demonstrates a hybrid storage adapter — access token kept in memory (never touches disk), refresh token persisted in localStorage — alongside how to perform a silent refresh on page load to restore the session without an authenticated flash.

Changelog

See CHANGELOG.md for release history.

Contributing

See CONTRIBUTING.md. Found a bug or have a feature request? Open an issue.

License

MIT — see LICENSE.