You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

176 lines
4.9 KiB

/**
* 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<ClockSyncConfig> = {}) {
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<void> {
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<ClockSyncConfig>): 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;