import type { IncomingMessage, ServerResponse } from "node:http"; import type { GatewayAuthResult } from "./auth.js"; import { readJsonBody } from "./hooks.js"; /** * Apply baseline security headers that are safe for all response types (API JSON, * HTML pages, static assets, SSE streams). Headers that restrict framing or set a * Content-Security-Policy are intentionally omitted here because some handlers * (canvas host, A2UI) serve content that may be loaded inside frames. */ export function setDefaultSecurityHeaders( res: ServerResponse, opts?: { strictTransportSecurity?: string }, ) { res.setHeader("X-Content-Type-Options", "nosniff"); res.setHeader("Referrer-Policy", "no-referrer"); res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()"); const strictTransportSecurity = opts?.strictTransportSecurity; if (typeof strictTransportSecurity === "string" && strictTransportSecurity.length > 0) { res.setHeader("Strict-Transport-Security", strictTransportSecurity); } } export function sendJson(res: ServerResponse, status: number, body: unknown) { res.statusCode = status; res.setHeader("Content-Type", "application/json; charset=utf-8"); res.end(JSON.stringify(body)); } export function sendText(res: ServerResponse, status: number, body: string) { res.statusCode = status; res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end(body); } export function sendMethodNotAllowed(res: ServerResponse, allow = "POST") { res.setHeader("Allow", allow); sendText(res, 405, "Method Not Allowed"); } export function sendUnauthorized(res: ServerResponse) { sendJson(res, 401, { error: { message: "Unauthorized", type: "unauthorized" }, }); } export function sendRateLimited(res: ServerResponse, retryAfterMs?: number) { if (retryAfterMs && retryAfterMs > 0) { res.setHeader("Retry-After", String(Math.ceil(retryAfterMs / 1000))); } sendJson(res, 429, { error: { message: "Too many failed authentication attempts. Please try again later.", type: "rate_limited", }, }); } export function sendGatewayAuthFailure(res: ServerResponse, authResult: GatewayAuthResult) { if (authResult.rateLimited) { sendRateLimited(res, authResult.retryAfterMs); return; } sendUnauthorized(res); } export function sendInvalidRequest(res: ServerResponse, message: string) { sendJson(res, 400, { error: { message, type: "invalid_request_error" }, }); } export async function readJsonBodyOrError( req: IncomingMessage, res: ServerResponse, maxBytes: number, ): Promise { const body = await readJsonBody(req, maxBytes); if (!body.ok) { if (body.error === "payload too large") { sendJson(res, 413, { error: { message: "Payload too large", type: "invalid_request_error" }, }); return undefined; } if (body.error === "request body timeout") { sendJson(res, 408, { error: { message: "Request body timeout", type: "invalid_request_error" }, }); return undefined; } sendInvalidRequest(res, body.error); return undefined; } return body.value; } export function writeDone(res: ServerResponse) { res.write("data: [DONE]\n\n"); } export function setSseHeaders(res: ServerResponse) { res.statusCode = 200; res.setHeader("Content-Type", "text/event-stream; charset=utf-8"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); res.flushHeaders?.(); }