import { createHmac, timingSafeEqual } from 'node:crypto' /** * Lightweight JWT implementation using HMAC-SHA256. * No external dependencies — uses Node.js crypto. * * Token format: base64url(header).base64url(payload).base64url(signature) * Used for V2 worker authentication (session ingress / SSE / CCR). */ interface JwtPayload { session_id: string role: string iat: number exp: number } function base64url(data: string | Buffer): string { return Buffer.from(data as unknown as ArrayLike) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, '') } function base64urlDecode(str: string): string { const padded = str.replace(/-/g, '+').replace(/_/g, '/') return Buffer.from(padded, 'base64').toString('utf-8') } function getSigningKey(): string { const key = process.env.RCS_API_KEYS?.split(',').filter(Boolean)[0] if (!key) throw new Error('No API key configured for JWT signing') return key } /** Generate a JWT for worker authentication. */ export function generateWorkerJwt( sessionId: string, expiresInSeconds: number, ): string { const header = { alg: 'HS256', typ: 'JWT' } const payload: JwtPayload = { session_id: sessionId, role: 'worker', iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + expiresInSeconds, } const headerB64 = base64url(JSON.stringify(header)) const payloadB64 = base64url(JSON.stringify(payload)) const signingInput = `${headerB64}.${payloadB64}` const signature = createHmac('sha256', getSigningKey()) .update(signingInput) .digest() return `${signingInput}.${base64url(signature)}` } /** * Verify a JWT and return its payload, or null if invalid/expired. * Uses timing-safe comparison to prevent timing attacks. */ export function verifyWorkerJwt(token: string): JwtPayload | null { const parts = token.split('.') if (parts.length !== 3) return null const [headerB64, payloadB64, signatureB64] = parts // Verify signature const signingInput = `${headerB64}.${payloadB64}` const expectedSig = createHmac('sha256', getSigningKey()) .update(signingInput) .digest() const actualSig = Buffer.from( signatureB64.replace(/-/g, '+').replace(/_/g, '/'), 'base64', ) if (expectedSig.length !== actualSig.length) return null if (!timingSafeEqual(expectedSig, actualSig)) return null // Decode payload try { const payload: JwtPayload = JSON.parse(base64urlDecode(payloadB64)) if (payload.exp < Math.floor(Date.now() / 1000)) return null return payload } catch { return null } }