import { createHmac, timingSafeEqual } from 'node:crypto'; // Short-lived, job-scoped token authorizing a browser terminal connection. // Minted server-side by the Next app (which has verified job ownership) and // verified here so the browser never sees the shared worker secret. Format: // `${expiresAtMs}.${jobId}.${hmacSha256Hex}` const signature = (payload: string, secret: string) => createHmac('sha256', secret).update(payload).digest('hex'); export const verifyTerminalToken = ( token: string, jobId: string, secret: string, ): boolean => { if (!token || !secret) return false; const parts = token.split('.'); if (parts.length !== 3) return false; const [expRaw, tokenJobId, provided] = parts; if (tokenJobId !== jobId) return false; const exp = Number.parseInt(expRaw ?? '', 10); if (!Number.isFinite(exp) || Date.now() > exp) return false; const expected = signature(`${expRaw}.${tokenJobId}`, secret); const providedBuf = Buffer.from(provided ?? '', 'hex'); const expectedBuf = Buffer.from(expected, 'hex'); return ( providedBuf.length === expectedBuf.length && timingSafeEqual(providedBuf, expectedBuf) ); }; // Extract the username a box token authorizes without verifying its signature. // The token itself authorizes the connection, so the upgrade handler reads the // username from it (rather than a query param) and then verifies the whole // token against that username. Returns null unless the format matches // `${expiresAtMs}.box.${username}.${hmacSha256Hex}`. export const parseBoxTokenUsername = (token: string): string | null => { const parts = token.split('.'); if (parts.length !== 4 || parts[1] !== 'box') return null; const username = parts[2]; if (!username) return null; return username; }; // User-scoped variant authorizing a terminal connection to a user's box. // Embeds a literal `box` segment so its four parts never collide with the // three-part job token above. Format: // `${expiresAtMs}.box.${username}.${hmacSha256Hex}` export const verifyBoxTerminalToken = ( token: string, username: string, secret: string, ): boolean => { if (!token || !secret) return false; const parts = token.split('.'); if (parts.length !== 4 || parts[1] !== 'box') return false; const [expRaw, , tokenUsername, provided] = parts; if (tokenUsername !== username) return false; const exp = Number.parseInt(expRaw ?? '', 10); if (!Number.isFinite(exp) || Date.now() > exp) return false; const expected = signature(`${expRaw}.box.${tokenUsername}`, secret); const providedBuf = Buffer.from(provided ?? '', 'hex'); const expectedBuf = Buffer.from(expected, 'hex'); return ( providedBuf.length === expectedBuf.length && timingSafeEqual(providedBuf, expectedBuf) ); };