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:
parent
e3add90f87
commit
bde5cb792d
18
web/package-lock.json
generated
18
web/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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
31
web/src/lib/auth-api.ts
Normal 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;
|
||||
}
|
||||
@ -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 ───────────────────────────────────────────
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user