refactor(web): wire @bytelyst/auth-client + telemetry-client into ChronoMind

- auth-api.ts: lazy-init shared auth client singleton
- platform-sync.ts: delegate auth ops to @bytelyst/auth-client
- telemetry.ts: delegate to @bytelyst/telemetry-client
- All 373 tests pass
This commit is contained in:
saravanakumardb1 2026-02-28 04:50:00 -08:00
parent e3add90f87
commit bde5cb792d
5 changed files with 88 additions and 173 deletions

18
web/package-lock.json generated
View File

@ -8,6 +8,8 @@
"name": "web",
"version": "0.1.0",
"dependencies": {
"@bytelyst/auth-client": "file:../../learning_ai_common_plat/packages/auth-client",
"@bytelyst/telemetry-client": "file:../../learning_ai_common_plat/packages/telemetry-client",
"@serwist/next": "^9.5.6",
"date-fns": "^4.1.0",
"idb": "^8.0.3",
@ -38,6 +40,14 @@
"vitest": "^4.0.18"
}
},
"../../learning_ai_common_plat/packages/auth-client": {
"name": "@bytelyst/auth-client",
"version": "0.1.0"
},
"../../learning_ai_common_plat/packages/telemetry-client": {
"name": "@bytelyst/telemetry-client",
"version": "0.1.0"
},
"node_modules/@acemir/cssom": {
"version": "0.9.31",
"resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@acemir/cssom/-/cssom-0.9.31.tgz",
@ -387,6 +397,14 @@
"specificity": "bin/cli.js"
}
},
"node_modules/@bytelyst/auth-client": {
"resolved": "../../learning_ai_common_plat/packages/auth-client",
"link": true
},
"node_modules/@bytelyst/telemetry-client": {
"resolved": "../../learning_ai_common_plat/packages/telemetry-client",
"link": true
},
"node_modules/@csstools/color-helpers": {
"version": "6.0.2",
"resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",

View File

@ -14,6 +14,8 @@
"test:e2e:ui": "playwright test --ui"
},
"dependencies": {
"@bytelyst/auth-client": "file:../../learning_ai_common_plat/packages/auth-client",
"@bytelyst/telemetry-client": "file:../../learning_ai_common_plat/packages/telemetry-client",
"@serwist/next": "^9.5.6",
"date-fns": "^4.1.0",
"idb": "^8.0.3",

31
web/src/lib/auth-api.ts Normal file
View File

@ -0,0 +1,31 @@
/**
* Shared auth client instance for ChronoMind web.
*
* Uses @bytelyst/auth-client to eliminate hand-rolled auth API calls.
* All auth operations (login, register, forgot password, etc.) go through this client.
* Lazy-initialized to avoid localStorage access at module load time (test compat).
*/
import { createAuthClient, type AuthClient } from '@bytelyst/auth-client';
export const PRODUCT_ID = 'chronomind';
function getBaseUrl(): string {
if (typeof window !== 'undefined' && (window as unknown as Record<string, unknown>).__PLATFORM_URL__) {
return (window as unknown as Record<string, unknown>).__PLATFORM_URL__ as string;
}
return process.env.NEXT_PUBLIC_PLATFORM_SERVICE_URL ?? 'https://api.chronomind.app';
}
let _authClient: AuthClient | null = null;
export function getAuthClient(): AuthClient {
if (!_authClient) {
_authClient = createAuthClient({
baseUrl: getBaseUrl(),
productId: PRODUCT_ID,
storagePrefix: 'chronomind',
});
}
return _authClient;
}

View File

@ -3,6 +3,8 @@
// Consumed by useSyncHook for React integration
import type { Timer } from './timer-engine';
import { getAuthClient, PRODUCT_ID as _PRODUCT_ID } from './auth-api';
import type { AuthUser, AuthResult } from '@bytelyst/auth-client';
// ── DTOs ──────────────────────────────────────────────────────
@ -63,7 +65,7 @@ const STORAGE_KEYS = {
syncEnabled: 'chronomind-platform-sync-enabled',
} as const;
export const PRODUCT_ID = 'chronomind';
export const PRODUCT_ID = _PRODUCT_ID;
function getBaseUrl(): string {
if (typeof window !== 'undefined' && (window as unknown as Record<string, unknown>).__PLATFORM_URL__) {
@ -220,77 +222,44 @@ function setLastSyncDate(date: string): void {
localStorage.setItem(STORAGE_KEYS.lastSync, date);
}
// ── Auth Operations ──────────────────────────────────────────
// ── Auth Operations (delegated to @bytelyst/auth-client) ─────
export interface AuthUser {
id: string;
email: string;
displayName: string;
role: string;
plan: string;
}
export interface AuthResult {
accessToken: string;
refreshToken: string;
user: AuthUser;
}
export type { AuthUser, AuthResult };
export async function loginUser(email: string, password: string): Promise<AuthResult> {
return apiRequest<AuthResult>('/auth/login', 'POST', { email, password, productId: PRODUCT_ID });
return getAuthClient().login(email, password);
}
export async function registerUser(
email: string,
password: string,
displayName: string
): Promise<AuthResult> {
return apiRequest<AuthResult>('/auth/register', 'POST', {
email,
password,
displayName,
productId: PRODUCT_ID,
});
export async function registerUser(email: string, password: string, displayName: string): Promise<AuthResult> {
return getAuthClient().register(email, password, displayName);
}
export async function getMe(): Promise<AuthUser> {
return apiRequest<AuthUser>('/auth/me', 'GET');
return getAuthClient().getMe();
}
export async function forgotPassword(email: string): Promise<{ message: string }> {
return apiRequest<{ message: string }>('/auth/forgot-password', 'POST', {
email,
productId: PRODUCT_ID,
});
return getAuthClient().forgotPassword(email);
}
export async function resetPassword(token: string, newPassword: string): Promise<{ message: string }> {
return apiRequest<{ message: string }>('/auth/reset-password', 'POST', { token, newPassword });
return getAuthClient().resetPassword(token, newPassword);
}
export async function changePassword(
currentPassword: string,
newPassword: string
): Promise<{ message: string }> {
return apiRequest<{ message: string }>('/auth/change-password', 'POST', {
currentPassword,
newPassword,
});
export async function changePassword(currentPassword: string, newPassword: string): Promise<{ message: string }> {
return getAuthClient().changePassword(currentPassword, newPassword);
}
export async function verifyEmail(token: string): Promise<{ message: string }> {
return apiRequest<{ message: string }>('/auth/verify-email', 'POST', { token });
return getAuthClient().verifyEmail(token);
}
export async function resendVerification(email: string): Promise<{ message: string }> {
return apiRequest<{ message: string }>('/auth/resend-verification', 'POST', {
email,
productId: PRODUCT_ID,
});
return getAuthClient().resendVerification(email);
}
export async function deleteAccount(password: string): Promise<{ message: string }> {
return apiRequest<{ message: string }>('/auth/account', 'DELETE', { password });
return getAuthClient().deleteAccount(password);
}
// ── Sync Operations ───────────────────────────────────────────

View File

@ -1,65 +1,26 @@
// ── Platform Telemetry Client ─────────────────────────────────
// Sends lightweight events to platform-service telemetry endpoint.
// Runs in the browser only. Privacy: no PII, only action names + timing.
// Delegates to @bytelyst/telemetry-client shared package.
// Privacy: no PII, only action names + timing.
import { PRODUCT_ID } from './platform-sync';
import { createTelemetryClient, type TelemetryClient } from '@bytelyst/telemetry-client';
import { PRODUCT_ID } from './auth-api';
const PLATFORM = 'web';
const OS_FAMILY = 'other';
const CHANNEL = 'pwa';
const MAX_QUEUE = 50;
const FLUSH_INTERVAL_MS = 30_000;
let _client: TelemetryClient | null = null;
interface TelemetryEvent {
id: string;
productId: string;
anonymousInstallId: string;
sessionId: string;
platform: string;
channel: string;
osFamily: string;
osVersion: string;
appVersion: string;
buildNumber: string;
releaseChannel: string;
eventType: string;
module: string;
eventName: string;
feature?: string;
message?: string;
tags?: Record<string, string>;
metrics?: Record<string, number>;
occurredAt: string;
}
let queue: TelemetryEvent[] = [];
let sessionId = '';
let installId = '';
let flushTimer: ReturnType<typeof setInterval> | null = null;
let baseUrl = '';
function uuid(): string {
if (typeof crypto?.randomUUID === 'function') return 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);
});
}
function getInstallId(): string {
if (installId) return installId;
try {
const stored = localStorage.getItem('chronomind_telemetry_install_id');
if (stored) {
installId = stored;
return installId;
}
installId = uuid();
localStorage.setItem('chronomind_telemetry_install_id', installId);
} catch {
installId = uuid();
function getClient(): TelemetryClient {
if (!_client) {
_client = createTelemetryClient({
productId: PRODUCT_ID,
baseUrl: process.env.NEXT_PUBLIC_PLATFORM_SERVICE_URL ?? 'https://api.chronomind.app',
platform: 'web',
channel: 'pwa',
transport: 'fetch',
appVersion: '0.1.0',
buildNumber: '1',
releaseChannel: 'beta',
});
}
return installId;
return _client;
}
export function trackEvent(
@ -73,35 +34,7 @@ export function trackEvent(
metrics?: Record<string, number>;
}
): void {
if (typeof window === 'undefined') return;
const event: TelemetryEvent = {
id: uuid(),
productId: PRODUCT_ID,
anonymousInstallId: getInstallId(),
sessionId,
platform: PLATFORM,
channel: CHANNEL,
osFamily: OS_FAMILY,
osVersion: navigator.userAgent.substring(0, 128),
appVersion: '0.1.0',
buildNumber: '1',
releaseChannel: 'beta',
eventType,
module,
eventName,
occurredAt: new Date().toISOString(),
...options,
};
queue.push(event);
if (queue.length > MAX_QUEUE) {
queue = queue.slice(-MAX_QUEUE);
}
if (queue.length >= 10) {
flush();
}
getClient().trackEvent(eventType, module, eventName, options);
}
export function trackTimerEvent(eventName: string, tags?: Record<string, string>, metrics?: Record<string, number>): void {
@ -113,47 +46,9 @@ export function trackPageView(path: string): void {
}
export function flush(): void {
if (typeof window === 'undefined' || queue.length === 0 || !baseUrl) return;
const events = [...queue];
queue = [];
const body = JSON.stringify({ productId: PRODUCT_ID, events });
const url = `${baseUrl}/telemetry/events`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-product-id': PRODUCT_ID,
'x-request-id': uuid(),
};
try {
// sendBeacon doesn't support custom headers — use fetch with keepalive
fetch(url, {
method: 'POST',
headers,
body,
keepalive: true,
}).catch(() => {});
} catch {
// Best effort — telemetry is non-critical
}
getClient().flush();
}
export function initTelemetry(): void {
if (typeof window === 'undefined') return;
baseUrl = process.env.NEXT_PUBLIC_PLATFORM_SERVICE_URL ?? 'https://api.chronomind.app';
sessionId = uuid();
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
flush();
}
});
if (flushTimer) clearInterval(flushTimer);
flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);
trackEvent('info', 'app_lifecycle', 'session_started');
getClient().init();
}