49 lines
2.0 KiB
TypeScript
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);
|
|
}
|