import { createHmac, randomBytes } from 'crypto'; import { Either, type IEither } from '@emprespresso/pengueno'; export interface TokenPayload { routeName: string; timestamp: number; } export class TokenSigner { private readonly secret: string; private readonly ttlSeconds: number; constructor(secret?: string, ttlSeconds: number = 30) { this.secret = secret || randomBytes(32).toString('hex'); this.ttlSeconds = ttlSeconds; } generate(routeName: string): string { const timestamp = Date.now(); const payload = JSON.stringify({ routeName, timestamp }); const signature = this.sign(payload); const token = Buffer.from(`${payload}.${signature}`).toString('base64url'); return token; } validate(token: string, expectedRoute: string): IEither { try { const decoded = Buffer.from(token, 'base64url').toString('utf-8'); const lastDotIndex = decoded.lastIndexOf('.'); if (lastDotIndex === -1) { return Either.left(new Error('Invalid token format')); } const payload = decoded.substring(0, lastDotIndex); const signature = decoded.substring(lastDotIndex + 1); // Verify signature const expectedSignature = this.sign(payload); if (signature !== expectedSignature) { return Either.left(new Error('Invalid token signature')); } // Parse payload const parsed: TokenPayload = JSON.parse(payload); // Check route name if (parsed.routeName !== expectedRoute) { return Either.left(new Error('Token route mismatch')); } // Check expiration const now = Date.now(); const age = (now - parsed.timestamp) / 1000; if (age > this.ttlSeconds) { return Either.left(new Error('Token expired')); } if (age < 0) { return Either.left(new Error('Token from future')); } return Either.right(parsed); } catch (err) { return Either.left(err instanceof Error ? err : new Error(String(err))); } } private sign(payload: string): string { return createHmac('sha256', this.secret).update(payload).digest('hex'); } getSecret(): string { return this.secret; } }