npm package: @glitch_protocol/auth-client
Description: Framework-agnostic HTTP and Socket.IO client for glitch_protocol
Author: drcount-root
Latest version: 0.2.0

# @glitch_protocol/auth-client

Browser-side HTTP client, Zustand auth store, and Socket.IO client for real-time session events. Works with any frontend framework.

## Install

```bash
npm install @glitch_protocol/auth-client socket.io-client zustand
```

---

## API

### createglitch_protocolClient

Creates an HTTP client with automatic token refresh on 401.

```ts
function createglitch_protocolClient(config: glitch_protocolClientConfig): {
  fetch<T>(url: string, init?: RequestInit): Promise<ApiResponse<T>>;
  refreshAccessToken(): Promise<boolean>;
}
```

**Config:**
```ts
interface glitch_protocolClientConfig {
  baseUrl: string;
  credentials?: "include" | "same-origin" | "omit"; // Default: "include"
  getAccessToken: () => string | null;    // Must return current access token
  onTokenRefreshed: (token: string) => void;  // Called when token is refreshed
  onAuthCleared: () => void;              // Called on 401 or refresh failure
}
```

**Usage:**
```ts
const client = createglitch_protocolClient({
  baseUrl: "http://localhost:4000",
  getAccessToken: () => store.accessToken,
  onTokenRefreshed: (token) => store.setAccessToken(token),
  onAuthCleared: () => store.clearAuth(),
});

// Automatic Bearer token injection + 401 auto-refresh
const result = await client.fetch<User>("/api/auth/me");
if (result.success) {
  console.log(result.data);
}
```

**Features:**
- Attaches `Authorization: Bearer {accessToken}` header automatically
- On 401: calls `/api/auth/refresh`, gets new token, retries request
- Only one refresh request in flight at a time (deduplication)
- Swallows network errors and returns `{ success: false }`

---

### createAuthStore

Zustand store factory for auth state management.

```ts
function createAuthStore(config: AuthStoreConfig): ReturnType<typeof create<AuthStore>>
```

**Config:**
```ts
interface AuthStoreConfig {
  client: glitch_protocolClient;          // From createglitch_protocolClient
  refreshUrl?: string;              // Default: "/api/auth/refresh"
  meUrl?: string;                   // Default: "/api/auth/me"
}
```

**Store State:**
```ts
interface AuthStore {
  // State
  user: AuthUser | null;
  accessToken: string | null;
  sessions: SessionInfo[];
  isLoading: boolean;
  isInitialized: boolean;

  // Actions
  setAuth(user: AuthUser, accessToken: string, sessions: SessionInfo[]): void;
  setAccessToken(token: string): void;
  setSessions(sessions: SessionInfo[]): void;
  clearAuth(): void;
  initialize(): Promise<void>;
}
```

**Usage:**
```ts
const useAuthStore = createAuthStore({ client });

// In a component:
const { user, accessToken, isInitialized } = useAuthStore();
const { setAuth, clearAuth, initialize } = useAuthStore();

// Bootstrap on app startup
useEffect(() => {
  initialize(); // Restores session from httpOnly cookie
}, []);

// Login
const response = await client.fetch("/api/auth/login", {
  method: "POST",
  body: JSON.stringify({ email, password }),
});
if (response.success) {
  setAuth(response.data.user, response.data.accessToken, response.data.sessions);
}
```

**How initialize() works:**
1. Calls `/api/auth/refresh` (uses httpOnly refresh cookie if valid)
2. Gets back new access token
3. Calls `/api/auth/me` with the token
4. Gets back user profile + active sessions
5. Stores in Zustand (or clears auth if refresh failed)

---

### createSocketClient

Socket.IO wrapper for real-time session events.

```ts
function createSocketClient(config: SocketClientConfig): {
  connect(): Promise<void>;
  disconnect(): void;
  getSocket(): Socket | null;
  onSessionNew(callback: (session: SessionInfo) => void): void;
  onSessionTerminated(callback: (data: { sessionId: string; reason: string }) => void): void;
  onSessionListUpdated(callback: (sessions: SessionInfo[]) => void): void;
}
```

**Config:**
```ts
interface SocketClientConfig {
  serverUrl: string;               // Base URL of auth server
  getAccessToken: () => string | null;  // Access token for auth
}
```

**Usage:**
```ts
const socketClient = createSocketClient({
  serverUrl: "http://localhost:4000",
  getAccessToken: () => useAuthStore.getState().accessToken,
});

await socketClient.connect();

socketClient.onSessionNew((session) => {
  console.log("New login detected:", session);
  useAuthStore.getState().setSessions([...useAuthStore.getState().sessions, session]);
});

socketClient.onSessionTerminated(({ sessionId, reason }) => {
  console.log(`Session ${sessionId} terminated: ${reason}`);
});

socketClient.onSessionListUpdated((sessions) => {
  useAuthStore.getState().setSessions(sessions);
});

// Cleanup on logout
socketClient.disconnect();
```

**How it works:**
- Connects to Socket.IO with JWT auth on handshake
- Joins room `user:{userId}` (automatically set on successful auth)
- Listens for `session:new`, `session:terminated`, `session:list-updated` events
- Heartbeat: sends heartbeat every 30s to keep session alive
- Auto-reconnect: retries up to 5 times with exponential backoff

---

## Breaking Circular Dependencies

The client package needs the store for tokens; the store needs the client for HTTP calls. Solve this with **deferred closures**:

```ts
// lib/auth.ts

// Step 1: Create placeholder functions
let getToken: () => string | null = () => null;
let onRefreshed: (t: string) => void = () => {};
let onCleared: () => void = () => {};

// Step 2: Create client with placeholders
export const client = createglitch_protocolClient({
  baseUrl: "http://localhost:4000",
  getAccessToken: () => getToken(),
  onTokenRefreshed: (t) => onRefreshed(t),
  onAuthCleared: () => onCleared(),
});

// Step 3: Create store with client
export const useAuthStore = createAuthStore({ client });

// Step 4: Wire closures to real store
getToken = () => useAuthStore.getState().accessToken;
onRefreshed = (t) => useAuthStore.getState().setAccessToken(t);
onCleared = () => useAuthStore.getState().clearAuth();
```

This avoids "maximum call stack exceeded" errors from circular imports.

---

## Example: Complete Integration

```tsx
// src/lib/auth.ts
import { createglitch_protocolClient, createAuthStore, createSocketClient } from "@glitch_protocol/auth-client";

let getToken = () => null as string | null;
let onRefreshed = (t: string) => {};
let onCleared = () => {};

export const client = createglitch_protocolClient({
  baseUrl: "http://localhost:4000",
  getAccessToken: () => getToken(),
  onTokenRefreshed: (t) => onRefreshed(t),
  onAuthCleared: () => onCleared(),
});

export const useAuthStore = createAuthStore({ client });
getToken = () => useAuthStore.getState().accessToken;
onRefreshed = (t) => useAuthStore.getState().setAccessToken(t);
onCleared = () => useAuthStore.getState().clearAuth();

export const socketClient = createSocketClient({
  serverUrl: "http://localhost:4000",
  getAccessToken: () => useAuthStore.getState().accessToken,
});
```

```tsx
// src/app.tsx
import { useEffect } from "react";
import { useAuthStore, socketClient } from "./lib/auth";

export default function App() {
  const initialize = useAuthStore((s) => s.initialize);

  useEffect(() => {
    initialize();
  }, [initialize]);

  useEffect(() => {
    socketClient.connect();
    return () => socketClient.disconnect();
  }, []);

  return <Dashboard />;
}

function Dashboard() {
  const { user, isInitialized } = useAuthStore();

  if (!isInitialized) return <div>Loading...</div>;
  if (!user) return <Login />;

  return <div>Welcome, {user.name}</div>;
}

function Login() {
  const { setAuth } = useAuthStore();

  const handleLogin = async (email: string, password: string) => {
    const result = await fetch("http://localhost:4000/api/auth/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      credentials: "include",
      body: JSON.stringify({ email, password }),
    }).then((r) => r.json());

    if (result.success) {
      setAuth(result.data.user, result.data.accessToken, result.data.sessions);
    }
  };

  return <LoginForm onSubmit={handleLogin} />;
}
```

---

## Peer Dependencies

- `socket.io-client: ^4.8.0` — Real-time events
- `zustand: ^5.0.0` — State management (optional, but recommended)

## Node Version

**ESM-only.** Requires Node.js >=18.