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 🙏

© 2025 – Pkg Stats / Ryan Hefner

token-refresh

v0.6.0

Published

A library for refreshing expired access tokens

Readme

token-refresh

Lightweight, framework-agnostic Axios token refresh helper with robust error handling and loop prevention.

Installation

npm install token-refresh
# or
yarn add token-refresh

Usage

import axios from 'axios';
import { installAuthRefresh, setAuthHeaders } from 'token-refresh';

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

// Token storage functions
const getTokens = async () => {
  return JSON.parse(localStorage.getItem('tokens') || 'null');
};

const setTokens = async (tokens) => {
  localStorage.setItem('tokens', JSON.stringify(tokens));
};

const { uninstall } = installAuthRefresh(api, {
  refresh: async (refreshToken) => {
    // IMPORTANT: avoid refresh loops — use a separate client OR set skipAuthRefresh: true
    const res = await api.post('/auth/refresh', { refreshToken }, { skipAuthRefresh: true });
    return res.data; // <- AuthTokens: { accessToken, refreshToken, ... }
  },
  getTokens,
  setTokens,
  isSessionExpired: (ctx) => {
    return ctx.status === 401;
  },
  isRefreshFailureTerminal: (error) => {
    const status = (error as any)?.response?.status ?? 0;
    return status === 401;
  },
  header: { 
    name: 'Authorization',        // default: 'Authorization'
    format: (t) => `Bearer ${t}`  // default: Bearer format
  },
  onBeforeRefresh: () => {
    // called right before refresh starts
    console.log('Refreshing tokens...');
  },
  onRefreshed: (tokens) => {
    // called after successful refresh with new tokens
    console.log('Tokens refreshed!', tokens);
  },
  onRefreshFailed: (error) => {
    // called when refresh fails - includes the error
    console.error('Refresh failed:', error);
    // e.g., redirect to login
  },
  maxRetries: 3,
  refreshTimeout: 10_000,
  // Enable logging (disabled by default for library usage)
  logger: {
    debug: (msg) => console.log(msg),
    error: (msg, err) => console.error(msg, err),
  },
  logout: async () => {
    // optional server-side logout
    await api.post('/auth/logout', {}, { skipAuthRefresh: true });
  }
});

// Recommended: seed the default header before making requests
// This avoids a cold-start race where the first request might leave without a header.
const existing = await getTokens();
if (existing?.accessToken) setAuthHeaders(api, existing.accessToken);

// Clean up when needed (tests, hot reloads, etc.)
// uninstall();

TypeScript Support

import type { AuthRefreshOptions, AuthTokens } from 'token-refresh';

const options: AuthRefreshOptions = {
  refresh: async (refreshToken: string): Promise<AuthTokens> => {
    // Your refresh implementation
  },
  // ... other options
};

Error Handling

The library includes specific error types for better analytics and debugging:

import { RefreshTimeoutError } from 'token-refresh';

installAuthRefresh(api, {
  // ... other options
  onRefreshFailed: (error) => {
    if (error instanceof RefreshTimeoutError) {
      // Handle timeout specifically
      analytics.track('auth_refresh_timeout');
    } else {
      // Handle other refresh failures
      analytics.track('auth_refresh_failed', { error: error.message });
    }
  }
});

Robustness Features

Infinite Loop Prevention

The library prevents infinite refresh loops at multiple levels:

  • Global: Only one refresh happens at a time across all requests
  • Per-request: Each individual request can only trigger one refresh attempt

Request Queuing

Failed requests are queued during refresh and automatically retried with the new token.

Timeout Protection

Refresh operations have configurable timeouts to prevent hanging requests.

Custom Header Example

For APIs that don't use Authorization: Bearer <token>:

installAuthRefresh(api, {
  // ... other options
  header: {
    name: 'XAuthToken',
    format: (token) => token  // no "Bearer " prefix
  }
});

How It Works

When a request fails with a condition that your isSessionExpired function returns true for, the library:

  1. Stale-token fast path: If the response matches your isSessionExpired predicate and the request was sent with an older token than the library currently holds in memory (e.g., you fixed the header or a previous refresh already updated it), the request is retried once with the current token — no refresh call is made.
  2. Loop prevention: Checks if this specific request already attempted a refresh
  3. Single-flight refresh: Only one refresh happens at a time, even with concurrent failures
  4. Request queuing: Failed requests are queued while refresh is in progress
  5. Header updates: Updates the default auth header on the Axios instance
  6. Request retry: Queued requests are retried with the new token
  7. Error handling: On failure/timeout/terminal auth loss, queued requests are rejected

API Reference

installAuthRefresh(api, options)

Parameters:

  • api: AxiosInstance - The Axios instance to install the interceptor on
  • options: AuthRefreshOptions

Returns: { uninstall: () => void }

AuthRefreshOptions

interface AuthRefreshOptions {
  refresh: (refreshToken: string) => Promise<AuthTokens>;
  getTokens: () => Promise<AuthTokens | null>;
  setTokens: (tokens: AuthTokens) => Promise<void>;
  isSessionExpired: (context: { status: number; data?: any; error: AxiosError }) => boolean;
  isRefreshFailureTerminal: (error: unknown) => boolean;
  header?: {
    name?: string;           // default: 'Authorization'
    format?: (token: string) => string; // default: (t) => `Bearer ${t}`
  };
  onBeforeRefresh?: () => void;
  onRefreshed?: (tokens: AuthTokens) => void;
  onRefreshFailed?: (error: unknown) => void;
  maxRetries?: number;       // default: 3
  refreshTimeout?: number;   // ms, default: 10000
  logger?: { debug(msg), error(msg, err?) }; // default: no-op (quiet)
  logout?: () => Promise<void>;
}

Note: isSessionExpired is required. A common implementation is:

  isSessionExpired: (ctx) => ctx.status === 401,
  isRefreshFailureTerminal: (error) => {
    // Axios example:
    const status = (error as any)?.response?.status ?? 0;
    return status === 401;
  }

RefreshTimeoutError

Custom error thrown when refresh operations timeout. Useful for analytics and specific error handling.

setAuthHeaders(axiosInstance, token, name?, format?)

Update or remove the instance's default auth header.

Advanced Usage

Custom isSessionExpired / isRefreshFailureTerminal

installAuthRefresh(api, {
  // ... other options
  // Decide when an original request failure should trigger a refresh
  isSessionExpired: (ctx) => {
    if (ctx.status === 401) return true;
    const code = ctx.data?.code;
    return ctx.status === 403 && code === 'TOKEN_EXPIRED';
  },
  // Decide when a refresh failure means auth is terminally lost
  isRefreshFailureTerminal: (error) => {
    // Axios example; adapt for your HTTP client
    const status = (error as any)?.response?.status ?? 0;
    const code = (error as any)?.response?.data?.code;
    if (status === 401) return true; // typical: invalid/expired refresh token
    return (status === 400 || status === 403 || status === 409) &&
           ['INVALID_GRANT', 'INVALID_REFRESH_TOKEN', 'TOKEN_REVOKED', 'TOKEN_REUSE_DETECTED'].includes(code);
  }
});

Multiple Axios Instances

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

// Only install on the protected API
installAuthRefresh(protectedApi, {
  refresh: (refreshToken) => publicApi.post('/auth/refresh', { refreshToken }),
  // ... other options
});

Testing & Cleanup

const { uninstall } = installAuthRefresh(api, options);

// In tests or during hot reloads
afterEach(() => {
  uninstall();
});

TypeScript

The library includes full TypeScript support and augments Axios types to support the skipAuthRefresh flag:

// This is automatically available
api.get('/data', { skipAuthRefresh: true });