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

@mapnests/gateway-web-sdk

v1.0.9

Published

Session token management SDK with automatic refresh for React/Next.js applications

Readme

Gateway Session SDK

A lightweight, production-ready session token management SDK for React and Next.js applications. Handles automatic token refresh with configurable intervals to prevent race conditions and token expiration issues.

Features

  • Automatic token refresh with configurable background intervals
  • HttpOnly cookie support for secure token storage
  • React hook (useSession) for seamless integration
  • Singleton pattern ensuring a single session instance across your app
  • Zero runtime dependencies (react and react-dom as peer dependencies)
  • Next.js compatible (SSR-safe)
  • Built-in Fetch and Axios interceptors with automatic session readiness gating and 401 handling
  • Resilient to browser close/reopen — stale cookies are cleared and tokens refresh before any API call
  • Full TypeScript definitions included

Installation

npm install @mapnests/gateway-web-sdk

Implementation Guide

Choose one of the three approaches below based on your preferred HTTP client. All three approaches share the same Step 1 (environment setup) and Step 2 (session initialization).

Next.js users: Skip the Common Setup below and go directly to the Next.js Integration section, which provides its own Steps 1–2. Then return here for Approach A, B, or C.


Common Setup (Vite / CRA / React Apps)

Step 1 — Environment Variables

Create a .env file in your project root with the following variables:

VITE_API_BASE_URL=https://your-gateway.example.com
VITE_BOOTSTRAP_PATH=/api/session/bootstrap
VITE_TOKEN_COOKIE_NAME=token

| Variable | Description | |----------|-------------| | VITE_API_BASE_URL | Base URL of your API gateway | | VITE_BOOTSTRAP_PATH | Path to the session bootstrap endpoint | | VITE_TOKEN_COOKIE_NAME | Name of the token cookie set by your server |

Then create a config helper to read these values:

// src/config.js
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
const BOOTSTRAP_PATH = import.meta.env.VITE_BOOTSTRAP_PATH;
const TOKEN_COOKIE_NAME = import.meta.env.VITE_TOKEN_COOKIE_NAME;

if (!API_BASE_URL) throw new Error('VITE_API_BASE_URL is not defined');
if (!BOOTSTRAP_PATH) throw new Error('VITE_BOOTSTRAP_PATH is not defined');
if (!TOKEN_COOKIE_NAME) throw new Error('VITE_TOKEN_COOKIE_NAME is not defined');

export { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME };

Step 2 — Initialize the Session Manager

Configure and initialize the SDK once at your app's entry point:

// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { SessionManager } from '@mapnests/gateway-web-sdk';
import { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME } from './config.js';
import App from './App';

const sessionManager = SessionManager.getInstance();

sessionManager.configure({
  bootstrapUrl: `${API_BASE_URL}${BOOTSTRAP_PATH}`,
  tokenCookieName: TOKEN_COOKIE_NAME,
});

sessionManager.initialize().catch(err =>
  console.error('Failed to initialize session:', err)
);

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Now proceed with one of the three approaches below.


Approach A — Fetch Interceptor

The simplest option. Drop-in replacement for fetch that automatically handles session headers and 401 retry.

Step 3 — Create the API layer

// src/api.js
import { fetchInterceptor } from '@mapnests/gateway-web-sdk';
import { API_BASE_URL } from './config.js';

export const getUser = () =>
  fetchInterceptor(`${API_BASE_URL}/api/user`);

Step 4 — Use in a component

// src/Dashboard.jsx
import { useEffect, useState } from 'react';
import { useSession } from '@mapnests/gateway-web-sdk';
import { getUser } from './api.js';

export default function Dashboard() {
  const { isInitialized, isLoading, error } = useSession();
  const [user, setUser] = useState(null);

  useEffect(() => {
    if (!isInitialized) return;

    getUser()
      .then(res => res.json())
      .then(setUser)
      .catch(err => console.error('Failed to fetch user:', err));
  }, [isInitialized]);

  if (isLoading) return <p>Loading session...</p>;
  if (error) return <p>Session error: {error}</p>;
  if (!user) return <p>Loading data...</p>;

  return <pre>{JSON.stringify(user, null, 2)}</pre>;
}

Approach B — Axios Interceptor

Best if you already use Axios. Wraps an Axios instance with automatic session headers and 401 retry.

Requires axios as a dependency: npm install axios

Step 3 — Create the Axios instance and API layer

// src/api.js
import axios from 'axios';
import { setupAxiosInterceptor } from '@mapnests/gateway-web-sdk';
import { API_BASE_URL } from './config.js';

const api = setupAxiosInterceptor(
  axios.create({
    baseURL: API_BASE_URL,
    withCredentials: true,
  })
);

export const getUser = () => api.get('/api/user');

Step 4 — Use in a component

// src/Dashboard.jsx
import { useEffect, useState } from 'react';
import { useSession } from '@mapnests/gateway-web-sdk';
import { getUser } from './api.js';

export default function Dashboard() {
  const { isInitialized, isLoading, error } = useSession();
  const [user, setUser] = useState(null);

  useEffect(() => {
    if (!isInitialized) return;

    getUser()
      .then(res => setUser(res.data))
      .catch(err => console.error('Failed to fetch user:', err));
  }, [isInitialized]);

  if (isLoading) return <p>Loading session...</p>;
  if (error) return <p>Session error: {error}</p>;
  if (!user) return <p>Loading data...</p>;

  return <pre>{JSON.stringify(user, null, 2)}</pre>;
}

Approach C — Manual Implementation

Full control over request construction and 401 handling. Use this when you need custom logic or don't want to use the built-in interceptors.

Step 3 — Create a manual fetch wrapper

// src/api.js
import { SessionManager } from '@mapnests/gateway-web-sdk';
import { API_BASE_URL } from './config.js';

const sm = SessionManager.getInstance();

async function request(url, init = {}) {
  // Ensure session is bootstrapped before sending (handles browser close/reopen, tab wake, etc.)
  await sm.ensureReady();

  const opts = {
    ...init,
    credentials: 'include',
    headers: { ...(init.headers || {}) },
  };

  // Attach session headers
  opts.headers['cf-session-id'] = sm.getSessionId();
  opts.headers['x-client-platform'] = 'web';

  if (sm.shouldUseTokenHeader()) {
    const token = sm.getToken(sm.config.tokenCookieName);
    if (token) opts.headers[sm.config.tokenCookieName] = token;
  }

  let res = await fetch(url, opts);

  // Handle 401 with the configured invalidSessionError
  if (res.status === 401) {
    const cloned = res.clone();
    try {
      const body = await cloned.json();
      if (body.error_msg === sm.config.invalidSessionError) {
        // Wait for any in-progress refresh, or trigger a new one
        if (sm.isRefreshing()) {
          await sm.waitForRefresh();
        } else {
          await sm.refreshToken();
        }

        // Update headers with refreshed session
        opts.headers['cf-session-id'] = sm.getSessionId();
        if (sm.shouldUseTokenHeader()) {
          const newToken = sm.getToken(sm.config.tokenCookieName);
          if (newToken) opts.headers[sm.config.tokenCookieName] = newToken;
        }

        // Retry the request
        res = await fetch(url, opts);
      }
    } catch {
      // Response was not JSON — return the original response
    }
  }

  return res;
}

export const getUser = () => request(`${API_BASE_URL}/api/user`);

Step 4 — Use in a component

// src/Dashboard.jsx
import { useEffect, useState } from 'react';
import { useSession } from '@mapnests/gateway-web-sdk';
import { getUser } from './api.js';

export default function Dashboard() {
  const { isInitialized, isLoading, error } = useSession();
  const [user, setUser] = useState(null);

  useEffect(() => {
    if (!isInitialized) return;

    getUser()
      .then(res => res.json())
      .then(setUser)
      .catch(err => console.error('Failed to fetch user:', err));
  }, [isInitialized]);

  if (isLoading) return <p>Loading session...</p>;
  if (error) return <p>Session error: {error}</p>;
  if (!user) return <p>Loading data...</p>;

  return <pre>{JSON.stringify(user, null, 2)}</pre>;
}

Next.js Integration

Note: For Next.js, use NEXT_PUBLIC_ prefixed environment variables instead of VITE_, and replace the common Step 1 config helper and Step 2 initialization with the Next.js-specific setup below.

Step 1 — Environment Variables

NEXT_PUBLIC_API_BASE_URL=https://your-gateway.example.com
NEXT_PUBLIC_BOOTSTRAP_PATH=/api/session/bootstrap
NEXT_PUBLIC_TOKEN_COOKIE_NAME=token

Step 2 — Config Helper

// src/config.js
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
const BOOTSTRAP_PATH = process.env.NEXT_PUBLIC_BOOTSTRAP_PATH;
const TOKEN_COOKIE_NAME = process.env.NEXT_PUBLIC_TOKEN_COOKIE_NAME;

if (!API_BASE_URL) throw new Error('NEXT_PUBLIC_API_BASE_URL is not defined');
if (!BOOTSTRAP_PATH) throw new Error('NEXT_PUBLIC_BOOTSTRAP_PATH is not defined');
if (!TOKEN_COOKIE_NAME) throw new Error('NEXT_PUBLIC_TOKEN_COOKIE_NAME is not defined');

export { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME };

App Router

Step 3 — Create a Session Provider

Create a client component that initializes the session. This keeps the root layout as a Server Component, preserving the benefits of React Server Components.

// app/providers/SessionProvider.jsx
'use client';

import { useEffect } from 'react';
import { SessionManager } from '@mapnests/gateway-web-sdk';
import { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME } from '@/src/config';

// Configure at module level so the SDK is ready before any child component mounts.
// configure() only sets config values — no browser APIs needed, safe during SSR.
const sessionManager = SessionManager.getInstance();
sessionManager.configure({
  bootstrapUrl: `${API_BASE_URL}${BOOTSTRAP_PATH}`,
  tokenCookieName: TOKEN_COOKIE_NAME,
});

export default function SessionProvider({ children }) {
  // initialize() calls the bootstrap API — must run in useEffect (client-only)
  useEffect(() => {
    sessionManager.initialize().catch(err =>
      console.error('Failed to initialize session:', err)
    );
  }, []);

  return children;
}

Step 4 — Add the Provider to the Root Layout

The root layout stays as a Server Component — do not add 'use client' here.

// app/layout.js
import SessionProvider from './providers/SessionProvider';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <SessionProvider>{children}</SessionProvider>
      </body>
    </html>
  );
}

Step 5 — Create an API Layer and Use in a Page

After your router setup is complete, create an API layer using any of Approach A / B / C from the Implementation Guide above. The only change needed is to replace import.meta.env.VITE_* references with imports from your src/config.js.

For example, using Approach A (Fetch Interceptor):

// src/api.js
import { fetchInterceptor } from '@mapnests/gateway-web-sdk';
import { API_BASE_URL } from './config';

export const getUser = () =>
  fetchInterceptor(`${API_BASE_URL}/api/user`);

Then use it in a page component. Page components that use hooks must be client components.

// app/dashboard/page.jsx
'use client';

import { useEffect, useState } from 'react';
import { useSession } from '@mapnests/gateway-web-sdk';
import { getUser } from '@/src/api';

export default function DashboardPage() {
  const { isInitialized, isLoading, error } = useSession();
  const [user, setUser] = useState(null);

  useEffect(() => {
    if (!isInitialized) return;

    getUser()
      .then(res => res.json())
      .then(setUser)
      .catch(err => console.error('Failed to fetch user:', err));
  }, [isInitialized]);

  if (isLoading) return <p>Loading session...</p>;
  if (error) return <p>Session error: {error}</p>;
  if (!user) return <p>Loading data...</p>;

  return <pre>{JSON.stringify(user, null, 2)}</pre>;
}

Pages Router

Step 3 — Initialize in _app.js

// pages/_app.js
import { useEffect } from 'react';
import { SessionManager } from '@mapnests/gateway-web-sdk';
import { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME } from '../src/config';

// Configure at module level — runs once when the module is first imported.
const sessionManager = SessionManager.getInstance();
sessionManager.configure({
  bootstrapUrl: `${API_BASE_URL}${BOOTSTRAP_PATH}`,
  tokenCookieName: TOKEN_COOKIE_NAME,
});

function MyApp({ Component, pageProps }) {
  // initialize() needs the browser — must run in useEffect
  useEffect(() => {
    sessionManager.initialize().catch(err =>
      console.error('Failed to initialize session:', err)
    );
  }, []);

  return <Component {...pageProps} />;
}

export default MyApp;

Step 4 — Create an API Layer and Use in a Page

Same as the App Router — create an src/api.js using any of Approach A / B / C, then use it in your page:

// pages/dashboard.jsx
import { useEffect, useState } from 'react';
import { useSession } from '@mapnests/gateway-web-sdk';
import { getUser } from '../src/api';

export default function Dashboard() {
  const { isInitialized, isLoading, error } = useSession();
  const [user, setUser] = useState(null);

  useEffect(() => {
    if (!isInitialized) return;

    getUser()
      .then(res => res.json())
      .then(setUser)
      .catch(err => console.error('Failed to fetch user:', err));
  }, [isInitialized]);

  if (isLoading) return <p>Loading session...</p>;
  if (error) return <p>Session error: {error}</p>;
  if (!user) return <p>Loading data...</p>;

  return <pre>{JSON.stringify(user, null, 2)}</pre>;
}

API Reference

useSession(options?)

React hook for session management.

Parameters:

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | options.autoInitialize | boolean | true | Automatically initialize session on mount |

Returns:

{
  isInitialized: boolean;
  isLoading: boolean;
  error: string | null;
  lastRefreshTime: number | null;
  nextRefreshTime: number | null;
  timeUntilRefresh: number | null;
  initializationFailed: boolean;
  refresh: () => Promise<void>;
  initialize: () => Promise<void>;
}

SessionManager

Core session management class (Singleton).

SessionManager.getInstance()

Returns the singleton instance.

configure(config)

sessionManager.configure({
  bootstrapUrl: '/session/bootstrap',    // Required: Bootstrap API endpoint
  tokenCookieName: 'token',             // Optional: Token cookie name (default: 'token')
  maxRetries: 3,                         // Optional: Max retry attempts (default: 3)
  headers: {},                           // Optional: Additional headers for bootstrap calls
  credentials: true,                     // Optional: Include credentials (default: true)
  logLevel: 'WARN',                      // Optional: 'NONE' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG'
});

refreshInterval and tokenExpiry are automatically set by the server's bootstrap response (refresh_time and expire_time fields). You don't need to configure them manually.

initialize()

Initialize session by calling the bootstrap endpoint.

await sessionManager.initialize();

refreshToken()

Manually trigger a session token refresh.

await sessionManager.refreshToken();

getSessionId()

Returns the current cf-session-id.

getToken(name?)

Returns the token value from the named cookie, or null.

shouldUseTokenHeader()

Returns true if the token should be sent as a request header (i.e. when the app is served over HTTP, not HTTPS).

ensureReady()

Ensures the session is ready before making API calls. This is the recommended way to gate requests on session readiness.

  • If initialized and token is fresh, resolves immediately (zero overhead).
  • If initialization is in progress, waits for it to complete.
  • If not initialized but configured, triggers initialize() and waits.
  • If not configured or permanently failed, resolves and lets the request proceed (the 401 handler will deal with it).
await sessionManager.ensureReady();
// Session is now ready — safe to make API calls

Note: fetchInterceptor and setupAxiosInterceptor call ensureReady() automatically before every request. You only need to call it explicitly when building a manual fetch wrapper (Approach C).

needsRefresh()

Returns true if the session is initialized but the token has likely expired. Useful for proactive checks.

if (sessionManager.needsRefresh()) {
  await sessionManager.refreshToken();
}

isRefreshing()

Returns true if a refresh/initialize is currently in progress.

waitForRefresh()

Returns a promise that resolves when the in-progress refresh completes.

getSessionStatus()

Returns the current session state object.

{
  isInitialized: boolean;
  isLoading: boolean;
  lastRefreshTime: number | null;
  nextRefreshTime: number | null;
  tokenExpiry: number | null;
  error: string | null;
  errorCode: string | null;
  initializationFailed: boolean;
  timeUntilRefresh: number | null;
}

Note: getSessionStatus() returns tokenExpiry and errorCode which are not exposed by the useSession hook. Use this method directly if you need those fields.

subscribe(listener)

Subscribe to session state changes. Returns an unsubscribe function.

const unsubscribe = sessionManager.subscribe((state) => {
  console.log('Session state:', state);
});

destroy()

Clean up timers, listeners, and cookies. Resets the session manager.

fetchInterceptor(url, options?)

Drop-in fetch wrapper. Automatically gates on session readiness via ensureReady(), attaches session headers, and retries on 401 INVALID_SESSION. Safe to call even before the session is initialized — the request will wait for bootstrap to complete.

import { fetchInterceptor } from '@mapnests/gateway-web-sdk';
const response = await fetchInterceptor('/api/data');

setupAxiosInterceptor(axiosInstance)

Attaches request/response interceptors to an Axios instance. Returns the same instance. Every request automatically gates on session readiness via ensureReady(), attaches session headers, and retries on 401 INVALID_SESSION.

import axios from 'axios';
import { setupAxiosInterceptor } from '@mapnests/gateway-web-sdk';
const api = setupAxiosInterceptor(axios.create({ baseURL: '/api' }));

Error Classes

The SDK exports custom error classes for typed error handling:

import {
  SessionError,       // Base error class (code, details)
  ConfigurationError, // Invalid configuration (code: 'CONFIGURATION_ERROR')
  BootstrapError,     // Bootstrap API failure (code: 'BOOTSTRAP_ERROR')
  NetworkError,       // Network-level failure (code: 'NETWORK_ERROR')
  SSRError,           // Called in non-browser environment (code: 'SSR_ERROR')
} from '@mapnests/gateway-web-sdk';

All errors extend SessionError, which provides code (string) and details (object) properties.

logger and LOG_LEVELS

The SDK's internal logger is exported for advanced use (e.g. setting log level independently of configure()):

import { logger, LOG_LEVELS } from '@mapnests/gateway-web-sdk';

logger.setLevel('DEBUG');     // or logger.setLevel(LOG_LEVELS.DEBUG)
logger.info('Custom log');    // [SessionManager] Custom log

Available levels: NONE (0), ERROR (1), WARN (2, default), INFO (3), DEBUG (4).


Security Notice

This SDK prioritizes server-set HttpOnly cookies for maximum security. The SDK includes a fallback to set cookies client-side, but these cannot be HttpOnly and are accessible to JavaScript.

Recommended: Always set cookies from your server using the Set-Cookie header with HttpOnly flag. The SDK will detect server cookies and skip client-side cookie setting automatically.


Best Practices

  1. Single Instance — Call configure() once at app startup and reuse the singleton.
  2. Token Timing — The server controls refresh and expiry timing via the bootstrap response.
  3. Error Handling — Handle errors gracefully and provide user feedback for limited mode.
  4. HTTPS — Always use HTTPS in production environments.
  5. CORS/Credentials — If cross-origin, ensure your server CORS allows credentials.
  6. Initialization Order — Always call configure() then initialize() before making API calls.

Troubleshooting

Cookies not being set

  • Verify your API returns Set-Cookie header with HttpOnly flag
  • Check CORS configuration allows credentials
  • Ensure credentials: true in configuration

Automatic refresh not working

  • Check browser console for errors
  • Verify server is sending refresh_time in bootstrap response
  • Ensure timer isn't being cleared prematurely

Multiple initializations

  • The SDK uses singleton pattern, but ensure you're not calling initialize() multiple times
  • Use autoInitialize: false in useSession() if you want manual control

401 errors after browser close and reopen

  • The SDK automatically clears stale cookies and re-bootstraps on fresh page loads
  • Both fetchInterceptor and setupAxiosInterceptor call ensureReady() before every request, which waits for bootstrap to complete
  • If using a manual fetch wrapper (Approach C), ensure you call await sm.ensureReady() before each request
  • Verify your 401 handler compares against sm.config.invalidSessionError (default: 'INVALID-GW-SESSION'), not a hardcoded string

Next.js SSR errors

  • The SDK detects SSR environments and prevents initialization
  • Always wrap initialization in useEffect or client components ('use client')
  • Do not call initialize() during server-side rendering

License

MIT