import { createHmac, timingSafeEqual } from "node:crypto"; import { loadWebConfig } from "@toolctrl/config"; import { NOT_FOUND_ENVELOPE } from "~/utils/errors.server"; export const internalWebApiProxyHeader = "x-toolctrl-internal-web-api-proxy"; export const internalWebApiProxySignatureHeader = "x-toolctrl-internal-web-api-proxy-signature"; export function requireInternalWebApiProxyRequest(request: Request) { if (request.headers.get(internalWebApiProxyHeader) !== "1" || !isValidInternalWebApiProxySignature(request)) { throw Response.json(NOT_FOUND_ENVELOPE, { status: 404 }); } } export function createInternalWebApiProxySignature(input: { method: string; url: string | URL; secret: string }) { const url = new URL(input.url); const canonicalTarget = `${input.method.toUpperCase()}\n${url.pathname}${url.search}`; return createHmac("sha256", input.secret).update(canonicalTarget).digest("base64url"); } function getApiAccessToken() { return loadWebConfig().config.apiAccessToken; } export function createInternalWebApiProxyHeaders(request: Request, secret = getApiAccessToken()) { if (!secret) { throw new Error("API_ACCESS_TOKEN is required for internal web API proxy requests."); } return { [internalWebApiProxyHeader]: "1", [internalWebApiProxySignatureHeader]: createInternalWebApiProxySignature({ method: request.method, url: request.url, secret }), }; } function isValidInternalWebApiProxySignature(request: Request) { const secret = getApiAccessToken(); const signature = request.headers.get(internalWebApiProxySignatureHeader); if (!secret || !signature) return false; const expected = createInternalWebApiProxySignature({ method: request.method, url: request.url, secret }); return timingSafeEqualString(signature, expected); } function timingSafeEqualString(left: string, right: string) { const leftBuffer = Buffer.from(left); const rightBuffer = Buffer.from(right); return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer); }