npm package: @periodic/tungsten-client
Description: Framework-agnostic browser authentication engine with deterministic state management and TypeScript support
Author: Uday Thakur
Latest version: 1.0.0

# 🔐 Periodic Tungsten Client

[![npm version](https://img.shields.io/npm/v/@periodic/tungsten-client.svg)](https://www.npmjs.com/package/@periodic/tungsten-client)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue)](https://www.typescriptlang.org/)

**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 states** — `isLoading` 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

```bash
npm install @periodic/tungsten-client
```

Or with yarn:

```bash
yarn add @periodic/tungsten-client
```

---

## 🚀 Quick Start

```typescript
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:**

```json
{
  "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

```typescript
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:

```typescript
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:

```typescript
// 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:

```typescript
// 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:

```typescript
// 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:

```typescript
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

```typescript
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

```tsx
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

```typescript
// 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

```typescript
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

```typescript
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

```typescript
// 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

```typescript
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

```typescript
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

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

### `AuthTokens`

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

---

## 📋 API Reference

### `TungstenClient`

```typescript
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

```typescript
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:

```typescript
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

```bash
# 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:

- [**@periodic/tungsten**](https://www.npmjs.com/package/@periodic/tungsten) - Server-side authentication primitives (JWT, Argon2, TOTP)
- [**@periodic/iridium**](https://www.npmjs.com/package/@periodic/iridium) - Structured logging
- [**@periodic/arsenic**](https://www.npmjs.com/package/@periodic/arsenic) - Semantic runtime monitoring
- [**@periodic/zirconium**](https://www.npmjs.com/package/@periodic/zirconium) - Environment configuration
- [**@periodic/vanadium**](https://www.npmjs.com/package/@periodic/vanadium) - Idempotency and distributed locks
- [**@periodic/strontium**](https://www.npmjs.com/package/@periodic/strontium) - Resilient HTTP client
- [**@periodic/obsidian**](https://www.npmjs.com/package/@periodic/obsidian) - HTTP error handling
- [**@periodic/titanium**](https://www.npmjs.com/package/@periodic/titanium) - Rate limiting
- [**@periodic/osmium**](https://www.npmjs.com/package/@periodic/osmium) - Redis caching

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

---

## 📖 Documentation

- [Quick Start Guide](QUICKSTART.md)
- [Installation Guide](INSTALLATION.md)
- [Contributing Guide](CONTRIBUTING.md)
- [Changelog](CHANGELOG.md)

---

## 🛠️ Production Recommendations

### Environment Variables

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

### Log Aggregation

Pair with `@periodic/iridium` for structured JSON output:

```typescript
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

```typescript
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](LICENSE)

---

## 🙏 Contributing

Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on:

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

---

## 📞 Support

- 📧 **Email:** udaythakurwork@gmail.com
- 🐛 **Issues:** [GitHub Issues](https://github.com/udaythakur7469/periodic-tungsten-client/issues)
- 💬 **Discussions:** [GitHub Discussions](https://github.com/udaythakur7469/periodic-tungsten-client/discussions)

---

## 🌟 Show Your Support

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

---

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