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

@periodic/tungsten-client

v1.0.0

Published

Framework-agnostic browser authentication engine with deterministic state management and TypeScript support

Readme

🔐 Periodic Tungsten Client

npm version License: MIT TypeScript

Framework-agnostic browser authentication engine with deterministic state management and TypeScript support

Part of the Periodic series of Node.js packages by Uday Thakur.


💡 Why Tungsten Client?

@periodic/tungsten-client is the browser-side companion to @periodic/tungsten. While @periodic/tungsten handles the server-side cryptographic primitives — signing tokens, hashing passwords, verifying HMAC signatures — this library handles the client-side lifecycle: storing tokens, refreshing them before they expire, injecting them into requests, and synchronizing auth state across browser tabs.

Most frontend authentication implementations are hand-rolled, inconsistent, and fragile. A token refresh races with a concurrent request. A tab logs out but the other tab doesn't notice. An expired token reaches the server because nobody was watching the clock. Tungsten Client eliminates all of it with a deterministic state machine that makes illegal auth states impossible to represent.

The name represents:

  • Determinism: Every auth state transition is explicit and inspectable — no undefined behaviour
  • Resilience: Token refresh races, concurrent requests, and tab synchronization are handled automatically
  • Transparency: Every state change fires an event — full observability without polling
  • Minimalism: Zero dependencies, storage-agnostic, framework-agnostic

Just as @periodic/tungsten handles server-side authentication without shortcuts, @periodic/tungsten-client handles client-side authentication without magic — explicit, auditable, and correct.


🎯 Why Choose Tungsten Client?

Browser authentication is surprisingly easy to get wrong — and most implementations have at least one of these bugs:

  • Token refresh races — two concurrent requests both try to refresh, one succeeds, one gets a 401
  • No tab synchronization — logout in one tab leaves other tabs authenticated
  • Expired tokens reaching the server — nobody was watching the clock before sending requests
  • Inconsistent loading statesisLoading is set in three different places with three different timings
  • No typed errors — auth failures are caught as unknown with instanceof guessing
  • No storage abstraction — switching from localStorage to sessionStorage requires rewriting the integration

Periodic Tungsten Client provides the perfect solution:

Zero dependencies — pure TypeScript, no runtime dependencies
Framework-agnostic — works with React, Vue, Svelte, Angular, or no framework
Deterministic State Machine — every auth state transition is explicit and typed
Automatic Token Refresh — proactive refresh before expiry, with race protection
Fetch Wrapper — auth headers injected automatically on every request
Multi-Tab Synchronization — logout in one tab propagates to all tabs instantly
Storage Abstraction — swap localStorage, sessionStorage, or custom storage
Event System — subscribe to every state transition without polling
Type-safe — strict TypeScript, zero any, throughout
No global state — multiple instances in the same app are fully isolated
Production-ready — non-blocking, never crashes your app


📦 Installation

npm install @periodic/tungsten-client

Or with yarn:

yarn add @periodic/tungsten-client

🚀 Quick Start

import { TungstenClient } from '@periodic/tungsten-client';

// 1. Create a client
const client = new TungstenClient({
  refreshEndpoint: '/api/auth/refresh',
  onStateChange: (state) => {
    console.log('Auth state:', state.status);
  },
});

// 2. Login — store tokens and transition to authenticated
client.login({
  accessToken: 'eyJhbGciOiJIUzI1NiJ9...',
  refreshToken: 'rt_01HQ4K2N...',
});

// 3. Make authenticated requests — token injected automatically
const response = await client.fetch('/api/me');
const user = await response.json();

// 4. Logout — clear tokens, notify other tabs
client.logout();

Auth state after login:

{
  "status": "authenticated",
  "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
  "expiresAt": 1708000900000,
  "authenticatedAt": 1708000000000
}

🧠 Core Concepts

The TungstenClient Class

  • TungstenClient is the single entry point — one instance manages the entire auth lifecycle
  • Holds the current auth state, manages token storage, drives the refresh cycle, and wraps fetch
  • No global instance — create one per app context, safe for SSR and multi-tenant apps
  • Accepts a storage adapter — defaults to localStorage, swap to anything
const client = new TungstenClient({
  refreshEndpoint: '/api/auth/refresh',
  storage: localStorage,        // or sessionStorage, or your own adapter
  refreshThresholdMs: 60_000,   // refresh 60s before expiry
  onStateChange: (state) => updateUIState(state),
});

The Auth State Machine

  • Every auth state is a named, typed value — no boolean soup (isLoading && !isError && !!user)
  • Illegal transitions are impossible — you cannot go from unauthenticated to refreshing
  • The state machine drives the UI — your components react to state, they don't manage it

Design principle:

The client holds exactly one auth state at any moment. Every action either transitions to a new state or is rejected. Nothing happens outside the machine.

unauthenticated → authenticating → authenticated ⟶ refreshing → authenticated
                                                 ↘ expired
                                                 ↘ error
authenticated   → unauthenticated  (logout)

Auth States

| State | Meaning | |-------|---------| | unauthenticated | No tokens — user is logged out | | authenticating | Login in progress | | authenticated | Valid access token present | | refreshing | Access token expired — refresh in progress | | expired | Refresh failed or refresh token expired | | error | Unrecoverable auth error |


✨ Features

🔄 Automatic Token Refresh

The client watches the access token expiry and refreshes proactively — before the token actually expires, not after the next request fails:

const client = new TungstenClient({
  refreshEndpoint: '/api/auth/refresh',
  refreshThresholdMs: 60_000, // refresh when < 60s remaining (default)
});

// Refresh happens automatically in the background
// client.fetch() waits for refresh to complete before sending

🌐 Authenticated Fetch Wrapper

Every request through client.fetch() gets the current access token injected — no manual header management:

// Before: manual, inconsistent, error-prone
const res = await fetch('/api/orders', {
  headers: { Authorization: `Bearer ${getTokenSomehow()}` },
});

// After: automatic, consistent, always fresh
const res = await client.fetch('/api/orders');

If a refresh is in progress when client.fetch() is called, it waits for the refresh to complete before sending — eliminating token refresh races.

🗂️ Multi-Tab Synchronization

Logout in one tab propagates to all open tabs immediately via the storage event:

// Tab A
client.logout(); // clears tokens, fires storage event

// Tab B — automatically receives the event
// onStateChange fires with { status: 'unauthenticated' }
// No polling, no extra setup

💾 Storage Abstraction

Swap the storage backend without changing any other code:

// localStorage (default)
new TungstenClient({ refreshEndpoint: '...', storage: localStorage });

// sessionStorage — tokens cleared on tab close
new TungstenClient({ refreshEndpoint: '...', storage: sessionStorage });

// Custom adapter — any object with getItem/setItem/removeItem
new TungstenClient({
  refreshEndpoint: '...',
  storage: {
    getItem: (key) => mySecureStorage.get(key),
    setItem: (key, value) => mySecureStorage.set(key, value),
    removeItem: (key) => mySecureStorage.delete(key),
  },
});

📡 Event System

Subscribe to every state transition — drive your UI reactively without polling:

client.on('stateChange', (state) => {
  switch (state.status) {
    case 'authenticated': showApp(); break;
    case 'unauthenticated': showLoginPage(); break;
    case 'refreshing': showLoadingIndicator(); break;
    case 'expired': showSessionExpiredModal(); break;
    case 'error': showErrorBanner(state.error); break;
  }
});

📚 Common Patterns

1. Vanilla TypeScript Setup

import { TungstenClient } from '@periodic/tungsten-client';

export const authClient = new TungstenClient({
  refreshEndpoint: '/api/auth/refresh',
  refreshThresholdMs: 60_000,
  onStateChange: (state) => {
    document.dispatchEvent(new CustomEvent('auth:stateChange', { detail: state }));
  },
});

// Login after form submit
async function handleLogin(email: string, password: string) {
  const res = await fetch('/api/auth/login', {
    method: 'POST',
    body: JSON.stringify({ email, password }),
    headers: { 'Content-Type': 'application/json' },
  });
  const { accessToken, refreshToken } = await res.json();
  authClient.login({ accessToken, refreshToken });
}

2. React Integration

import { createContext, useContext, useEffect, useState } from 'react';
import { TungstenClient, AuthStateData } from '@periodic/tungsten-client';

const client = new TungstenClient({ refreshEndpoint: '/api/auth/refresh' });
const AuthContext = createContext<AuthStateData | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [authState, setAuthState] = useState<AuthStateData>(client.getState());

  useEffect(() => {
    return client.on('stateChange', setAuthState);
  }, []);

  return <AuthContext.Provider value={authState}>{children}</AuthContext.Provider>;
}

export const useAuth = () => useContext(AuthContext)!;

3. Making Authenticated API Calls

// All requests through client.fetch() are automatically authenticated
const orders = await authClient.fetch('/api/orders').then(r => r.json());

// POST with body
const order = await authClient.fetch('/api/orders', {
  method: 'POST',
  body: JSON.stringify({ productId: 'prod_123', quantity: 2 }),
  headers: { 'Content-Type': 'application/json' },
}).then(r => r.json());

4. Handling Session Expiry

authClient.on('stateChange', (state) => {
  if (state.status === 'expired') {
    // Clear any app state that depends on the user
    clearUserCache();
    // Redirect to login with return URL
    router.push(`/login?returnTo=${encodeURIComponent(location.pathname)}`);
  }
});

5. Custom Storage Adapter

import { TungstenClient } from '@periodic/tungsten-client';

// Cookie-based storage (for SSR-compatible auth)
const cookieStorage = {
  getItem: (key: string) => getCookie(key) ?? null,
  setItem: (key: string, value: string) => setCookie(key, value, { secure: true, sameSite: 'strict' }),
  removeItem: (key: string) => deleteCookie(key),
};

const client = new TungstenClient({
  refreshEndpoint: '/api/auth/refresh',
  storage: cookieStorage,
});

6. With @periodic/tungsten Server

// Server: @periodic/tungsten signs the tokens
app.post('/api/auth/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await db.users.findOne({ email });
  const isValid = await verifyPassword(password, user.passwordHash);
  if (!isValid) return res.status(401).json({ error: 'Invalid credentials' });

  const accessToken = await signAccessToken({ sub: user.id }, { expiresIn: '15m', keyProvider });
  const refreshToken = await signAccessToken({ sub: user.id, type: 'refresh' }, { expiresIn: '7d', keyProvider });
  res.json({ accessToken, refreshToken });
});

// Client: @periodic/tungsten-client manages the lifecycle
const client = new TungstenClient({ refreshEndpoint: '/api/auth/refresh' });
const { accessToken, refreshToken } = await loginRequest(email, password);
client.login({ accessToken, refreshToken });

7. Structured Logging Integration

import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
import { TungstenClient } from '@periodic/tungsten-client';

const logger = createLogger({
  transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
});

const client = new TungstenClient({
  refreshEndpoint: '/api/auth/refresh',
  onStateChange: (state) => {
    logger.info('tungsten.auth.state_change', { status: state.status });
  },
});

8. Production Configuration

import { TungstenClient } from '@periodic/tungsten-client';

const isDevelopment = process.env.NODE_ENV === 'development';

export const authClient = new TungstenClient({
  refreshEndpoint: '/api/auth/refresh',
  refreshThresholdMs: isDevelopment ? 5_000 : 60_000,
  storage: isDevelopment ? sessionStorage : localStorage,
  onStateChange: (state) => {
    if (state.status === 'error') {
      Sentry.captureException(state.error);
    }
    if (state.status === 'expired') {
      logger.warn('tungsten.session_expired');
    }
  },
});

export default authClient;

🎛️ Configuration Options

TungstenClient Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | refreshEndpoint | string | required | URL to call for token refresh | | storage | StorageAdapter | localStorage | Storage backend for tokens | | refreshThresholdMs | number | 60_000 | Refresh when this many ms remain before expiry | | onStateChange | (state: AuthStateData) => void | — | Shorthand state change callback |

StorageAdapter Interface

interface StorageAdapter {
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
  removeItem(key: string): void;
}

AuthTokens

interface AuthTokens {
  accessToken: string;
  refreshToken: string;
  expiresAt?: number; // Unix ms — inferred from JWT exp if not provided
}

📋 API Reference

TungstenClient

new TungstenClient(options: TungstenClientOptions): TungstenClient

client.login(tokens: AuthTokens): void
client.logout(): void
client.fetch(url: string, init?: RequestInit): Promise<Response>
client.getState(): AuthStateData
client.on(event: 'stateChange', handler: (state: AuthStateData) => void): () => void

Types

import type {
  AuthState,
  AuthTokens,
  AuthStateData,
  StorageAdapter,
  TungstenClientOptions,
} from '@periodic/tungsten-client';

🧩 Architecture

@periodic/tungsten-client/
├── src/
│   ├── client.ts              # TungstenClient class — main entry point
│   ├── stateMachine.ts        # Deterministic auth state transitions
│   ├── tokenManager.ts        # Token storage, parsing, expiry tracking
│   ├── refreshEngine.ts       # Proactive refresh + race protection
│   ├── fetchWrapper.ts        # Authenticated fetch with refresh wait
│   ├── tabSync.ts             # Multi-tab synchronization via storage events
│   ├── eventEmitter.ts        # Typed event system
│   ├── types.ts               # All shared TypeScript interfaces
│   └── index.ts               # Public API

Design Philosophy:

  • State machine is the source of truth — the UI reacts to state, it never sets state directly
  • Refresh engine is proactive — tokens are refreshed before they expire, not after a 401
  • Fetch wrapper queues requests — concurrent requests during a refresh wait, they don't race
  • Tab sync is passive — uses storage events, no polling, no WebSockets
  • No global state — every instance is isolated, safe for SSR and testing

📈 Performance

  • Proactive refresh — no failed requests due to expired tokens in normal operation
  • Race protection — concurrent refresh attempts are coalesced into a single request
  • Passive tab sync — storage events are native browser push, zero polling overhead
  • Zero dependencies — no additional bundle weight beyond the library itself
  • Tree-shakeable — unused exports are excluded from your bundle

🚫 Explicit Non-Goals

This package intentionally does not include:

❌ Server-side token signing or verification (use @periodic/tungsten)
❌ React hooks or components (adapt the event system to your framework)
❌ OAuth / OpenID Connect flows — bring your own login UI
❌ Token introspection or JWK fetching
❌ Automatic retry on 401 beyond the refresh flow
❌ Magic or implicit behavior on import
❌ Configuration files (configure in code)

Focus on doing one thing well: deterministic, race-safe, framework-agnostic browser authentication state management.


🎨 TypeScript Support

Full TypeScript support with complete type safety:

import type {
  AuthState,
  AuthTokens,
  AuthStateData,
  StorageAdapter,
  TungstenClientOptions,
} from '@periodic/tungsten-client';

// AuthStateData is a discriminated union — narrow by status
const state = client.getState();
if (state.status === 'authenticated') {
  state.accessToken; // string — only present in this branch
  state.expiresAt;   // number — only present in this branch
}

// on() returns an unsubscribe function — fully typed
const unsubscribe = client.on('stateChange', (state: AuthStateData) => {
  console.log(state.status);
});
unsubscribe(); // clean up

🧪 Testing

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Run tests in watch mode
npm run test:watch

Note: All tests achieve >80% code coverage.


🤝 Related Packages

Part of the Periodic series by Uday Thakur:

Build complete, production-ready APIs with the Periodic series!


📖 Documentation


🛠️ Production Recommendations

Environment Variables

NODE_ENV=production
NEXT_PUBLIC_API_URL=https://api.example.com

Log Aggregation

Pair with @periodic/iridium for structured JSON output:

import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';

const logger = createLogger({
  transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
});

const client = new TungstenClient({
  refreshEndpoint: '/api/auth/refresh',
  onStateChange: (state) => {
    logger.info('tungsten.auth.state_change', { status: state.status });
    if (state.status === 'error') {
      logger.error('tungsten.auth.error', { error: state.error?.message });
    }
  },
});

// Pipe to Elasticsearch, Datadog, CloudWatch, etc.

Error Monitoring

const client = new TungstenClient({
  refreshEndpoint: '/api/auth/refresh',
  onStateChange: (state) => {
    if (state.status === 'error') {
      Sentry.captureException(state.error, { extra: { authStatus: state.status } });
    }
  },
});

📝 License

MIT © Uday Thakur


🙏 Contributing

Contributions are welcome! Please read CONTRIBUTING.md for details on:

  • Code of conduct
  • Development setup
  • Pull request process
  • Coding standards
  • Architecture principles

📞 Support


🌟 Show Your Support

Give a ⭐️ if this project helped you build better applications!


Built with ❤️ by Uday Thakur for production-grade Node.js applications