Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/api/internalWebApiProxy.server.ts
T

49 lines
2.0 KiB
TypeScript

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