/** * Clock synchronization and skew handling */ import { ClockSyncConfig } from './types'; import { DEFAULT_CONFIG } from './constants'; export class ClockSyncManager { private config: ClockSyncConfig; private lastSyncTime = 0; private serverOffset = 0; // Server time - client time private syncInterval?: NodeJS.Timeout | undefined; constructor(config: Partial = {}) { this.config = { serverTimeSource: config.serverTimeSource ?? 'ntp', ntpServers: config.ntpServers ?? ['pool.ntp.org', 'time.google.com'], maxClockSkewSeconds: config.maxClockSkewSeconds ?? DEFAULT_CONFIG.maxClockSkewSeconds, skewCheckIntervalMs: config.skewCheckIntervalMs ?? DEFAULT_CONFIG.skewCheckIntervalMs, jwtClockSkewTolerance: config.jwtClockSkewTolerance ?? DEFAULT_CONFIG.jwtClockSkewTolerance, jwtMaxAge: config.jwtMaxAge ?? DEFAULT_CONFIG.jwtMaxAge }; } async syncWithServer(apiServer: string, jwtToken?: string): Promise { try { // Get server time from API const response = await fetch(`${apiServer}/api/v2/time`, { method: 'GET', headers: jwtToken ? { 'Authorization': `Bearer ${jwtToken}` } : {} }); if (!response.ok) { throw new Error(`Clock sync failed: HTTP ${response.status}`); } const serverTime = parseInt(response.headers.get('X-Server-Time') || '0'); const clientTime = Date.now(); if (serverTime === 0) { throw new Error('Invalid server time response'); } this.serverOffset = serverTime - clientTime; this.lastSyncTime = clientTime; // Validate skew is within tolerance if (Math.abs(this.serverOffset) > this.config.maxClockSkewSeconds * 1000) { console.warn(`Large clock skew detected: ${this.serverOffset}ms`); } console.log(`Clock sync successful: offset=${this.serverOffset}ms`); } catch (error) { console.error('Clock sync failed:', error); // Continue with client time, but log the issue } } getServerTime(): number { return Date.now() + this.serverOffset; } getClientTime(): number { return Date.now(); } getServerOffset(): number { return this.serverOffset; } getLastSyncTime(): number { return this.lastSyncTime; } validateJwtTimestamp(jwt: any): boolean { const now = this.getServerTime(); const iat = jwt.iat * 1000; // Convert to milliseconds const exp = jwt.exp * 1000; // Check if JWT is within valid time window const skewTolerance = this.config.jwtClockSkewTolerance * 1000; const maxAge = this.config.jwtMaxAge; const isValid = (now >= iat - skewTolerance) && (now <= exp + skewTolerance) && (now - iat <= maxAge); if (!isValid) { console.warn('JWT timestamp validation failed:', { now, iat, exp, skewTolerance, maxAge, serverOffset: this.serverOffset }); } return isValid; } isClockSkewExcessive(): boolean { return Math.abs(this.serverOffset) > this.config.maxClockSkewSeconds * 1000; } needsSync(): boolean { const timeSinceLastSync = Date.now() - this.lastSyncTime; return timeSinceLastSync > this.config.skewCheckIntervalMs; } // Periodic sync startPeriodicSync(apiServer: string, jwtToken?: string): void { if (this.syncInterval) { clearInterval(this.syncInterval); } this.syncInterval = setInterval(() => { this.syncWithServer(apiServer, jwtToken); }, this.config.skewCheckIntervalMs); } stopPeriodicSync(): void { if (this.syncInterval) { clearInterval(this.syncInterval); this.syncInterval = undefined; } } getConfig(): ClockSyncConfig { return { ...this.config }; } } /** * Create default clock sync manager */ export function createDefaultClockSyncManager(): ClockSyncManager { return new ClockSyncManager(); } /** * Create clock sync manager with custom config */ export function createClockSyncManager(config: Partial): ClockSyncManager { return new ClockSyncManager(config); } /** * Clock sync configuration presets */ export const CLOCK_SYNC_PRESETS = { // Conservative: Frequent sync, strict tolerance conservative: { maxClockSkewSeconds: 15, skewCheckIntervalMs: 180000, // 3 minutes jwtClockSkewTolerance: 15, jwtMaxAge: 1800000 // 30 minutes }, // Balanced: Good balance of sync frequency and tolerance balanced: { maxClockSkewSeconds: 30, skewCheckIntervalMs: 300000, // 5 minutes jwtClockSkewTolerance: 30, jwtMaxAge: 3600000 // 1 hour }, // Relaxed: Less frequent sync, more tolerance relaxed: { maxClockSkewSeconds: 60, skewCheckIntervalMs: 600000, // 10 minutes jwtClockSkewTolerance: 60, jwtMaxAge: 7200000 // 2 hours } } as const;