@glitch_protocol/auth-client
v0.2.0
Published
Framework-agnostic HTTP and Socket.IO client for glitch_protocol
Downloads
479
Maintainers
Readme
@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
npm install @glitch_protocol/auth-client socket.io-client zustandAPI
createglitch_protocolClient
Creates an HTTP client with automatic token refresh on 401.
function createglitch_protocolClient(config: glitch_protocolClientConfig): {
fetch<T>(url: string, init?: RequestInit): Promise<ApiResponse<T>>;
refreshAccessToken(): Promise<boolean>;
}Config:
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:
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.
function createAuthStore(config: AuthStoreConfig): ReturnType<typeof create<AuthStore>>Config:
interface AuthStoreConfig {
client: glitch_protocolClient; // From createglitch_protocolClient
refreshUrl?: string; // Default: "/api/auth/refresh"
meUrl?: string; // Default: "/api/auth/me"
}Store State:
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:
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:
- Calls
/api/auth/refresh(uses httpOnly refresh cookie if valid) - Gets back new access token
- Calls
/api/auth/mewith the token - Gets back user profile + active sessions
- Stores in Zustand (or clears auth if refresh failed)
createSocketClient
Socket.IO wrapper for real-time session events.
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:
interface SocketClientConfig {
serverUrl: string; // Base URL of auth server
getAccessToken: () => string | null; // Access token for auth
}Usage:
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-updatedevents - 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:
// 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
// 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,
});// 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 eventszustand: ^5.0.0— State management (optional, but recommended)
Node Version
ESM-only. Requires Node.js >=18.
