learning_ai_common_plat/packages/auth-client/src/client.ts
saravanakumardb1 b400c76c0a feat(packages): add @bytelyst/auth-client + telemetry-client, extend react-auth lifecycle
- @bytelyst/auth-client: browser/RN-safe auth API wrapper (17 tests)
- @bytelyst/telemetry-client: shared telemetry with configurable transport (11 tests)
- @bytelyst/react-auth: add register, forgotPw, changePw, deleteAccount, token refresh (10 tests)
- eslint.config: add missing browser globals
2026-02-28 04:49:46 -08:00

266 lines
7.5 KiB
TypeScript

/**
* Browser/React Native-safe auth API client for platform-service.
*
* Replaces hand-rolled auth clients in ChronoMind web, MindLyst web, NomGap, etc.
* No Node.js dependencies — uses globalThis.fetch and configurable storage.
*
* @example
* ```ts
* import { createAuthClient } from '@bytelyst/auth-client';
*
* const auth = createAuthClient({
* baseUrl: 'http://localhost:4003/api',
* productId: 'chronomind',
* });
*
* const result = await auth.login('user@example.com', 'password123');
* console.log(result.user.displayName);
* ```
*/
import type { AuthClient, AuthClientConfig, AuthResult, AuthUser, TokenStorage } from './types.js';
// ── Default localStorage adapter ─────────────────────────────────
const noopStorage: TokenStorage = {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
};
function getDefaultStorage(): TokenStorage {
if (
typeof globalThis.localStorage !== 'undefined' &&
typeof globalThis.localStorage?.getItem === 'function'
) {
return globalThis.localStorage;
}
return noopStorage;
}
// ── UUID helper (browser + RN safe) ──────────────────────────────
function uuid(): string {
if (typeof globalThis.crypto?.randomUUID === 'function') {
return globalThis.crypto.randomUUID();
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0;
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
});
}
// ── Factory ──────────────────────────────────────────────────────
export function createAuthClient(config: AuthClientConfig): AuthClient {
const { baseUrl, productId, timeoutMs = 15_000 } = config;
const storage = config.storage ?? getDefaultStorage();
const prefix = config.storagePrefix ?? productId;
const KEYS = {
accessToken: `${prefix}-auth-token`,
refreshToken: `${prefix}-refresh-token`,
} as const;
// ── Token management ────────────────────────────
function getAccessToken(): string | null {
return storage.getItem(KEYS.accessToken);
}
function getRefreshToken(): string | null {
return storage.getItem(KEYS.refreshToken);
}
function setTokens(accessToken: string, refreshToken: string): void {
storage.setItem(KEYS.accessToken, accessToken);
storage.setItem(KEYS.refreshToken, refreshToken);
}
function clearTokens(): void {
storage.removeItem(KEYS.accessToken);
storage.removeItem(KEYS.refreshToken);
}
function isAuthenticated(): boolean {
return getAccessToken() !== null;
}
// ── HTTP helper ─────────────────────────────────
async function request<T>(
path: string,
method: string,
body?: unknown,
opts?: { skipAuth?: boolean }
): Promise<T> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-product-id': productId,
'x-request-id': uuid(),
};
if (!opts?.skipAuth) {
const token = getAccessToken();
if (token) headers['Authorization'] = `Bearer ${token}`;
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await globalThis.fetch(`${baseUrl}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(
(data as Record<string, string>).message ||
(data as Record<string, string>).error ||
`HTTP ${res.status}`
);
}
if (res.status === 204) return undefined as T;
return res.json() as Promise<T>;
} finally {
clearTimeout(timer);
}
}
// ── Singleton refresh guard ─────────────────────
let _refreshPromise: Promise<boolean> | null = null;
async function refreshAccessToken(): Promise<boolean> {
if (_refreshPromise) return _refreshPromise;
_refreshPromise = (async () => {
const rt = getRefreshToken();
if (!rt) return false;
try {
const data = await request<{ accessToken: string; refreshToken: string }>(
'/auth/refresh',
'POST',
{ refreshToken: rt },
{ skipAuth: true }
);
setTokens(data.accessToken, data.refreshToken);
return true;
} catch {
clearTokens();
return false;
}
})();
try {
return await _refreshPromise;
} finally {
_refreshPromise = null;
}
}
// ── Auth operations ─────────────────────────────
async function login(email: string, password: string): Promise<AuthResult> {
const result = await request<AuthResult>('/auth/login', 'POST', {
email,
password,
productId,
});
setTokens(result.accessToken, result.refreshToken);
return result;
}
async function register(
email: string,
password: string,
displayName: string
): Promise<AuthResult> {
const result = await request<AuthResult>('/auth/register', 'POST', {
email,
password,
displayName,
productId,
});
setTokens(result.accessToken, result.refreshToken);
return result;
}
async function getMe(): Promise<AuthUser> {
return request<AuthUser>('/auth/me', 'GET');
}
// ── Password management ─────────────────────────
async function forgotPassword(email: string): Promise<{ message: string }> {
return request<{ message: string }>('/auth/forgot-password', 'POST', {
email,
productId,
});
}
async function resetPassword(token: string, newPassword: string): Promise<{ message: string }> {
return request<{ message: string }>('/auth/reset-password', 'POST', {
token,
newPassword,
});
}
async function changePassword(
currentPassword: string,
newPassword: string
): Promise<{ message: string }> {
return request<{ message: string }>('/auth/change-password', 'POST', {
currentPassword,
newPassword,
});
}
// ── Account management ──────────────────────────
async function deleteAccount(password: string): Promise<{ message: string }> {
const result = await request<{ message: string }>('/auth/account', 'DELETE', {
password,
});
clearTokens();
return result;
}
// ── Email verification ──────────────────────────
async function verifyEmail(token: string): Promise<{ message: string }> {
return request<{ message: string }>('/auth/verify-email', 'POST', { token });
}
async function resendVerification(email: string): Promise<{ message: string }> {
return request<{ message: string }>('/auth/resend-verification', 'POST', {
email,
productId,
});
}
return {
getAccessToken,
getRefreshToken,
setTokens,
clearTokens,
isAuthenticated,
login,
register,
getMe,
refreshAccessToken,
forgotPassword,
resetPassword,
changePassword,
deleteAccount,
verifyEmail,
resendVerification,
};
}