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; } }