All files / routes/internal/website-build index.ts

100% Statements 30/30
77.77% Branches 14/18
100% Functions 2/2
100% Lines 29/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80                                    1x     1x     1x 47x     47x 7x     40x 40x     40x   40x 36x 36x       4x 1x 1x     3x 1x         2x   2x 2x       47x   47x 2x 1x 1x 1x     1x       1x 1x      
// Internal Website Build Routes - Called by build worker/container
import { Hono } from "hono";
import type { Dal } from "../../../dal";
import type { Services } from "../../../services";
import { UnauthorizedError } from "../../../lib/errors";
import buildJobRoutes from "./build-jobs.routes";
import buildProDataRoutes from "./build-pro-data.routes";
import { verifyS2SRequest } from "@interioring/internal-auth";
import { logger } from "../../../lib/logger";
 
type Env = {
	Bindings: CloudflareBindings;
	Variables: {
		dal: Dal;
		services: Services;
	};
};
 
const buildRoutes = new Hono<Env>();
 
// Public endpoints that don't require API key (used by pro-sites SSR)
const PUBLIC_ENDPOINTS = ["/validate-preview-token"];
 
// Middleware to validate internal auth token (HMAC-signed, bound to jobId)
buildRoutes.use("*", async (c, next) => {
	const url = new URL(c.req.url);
 
	// Allow public endpoints without auth
	if (PUBLIC_ENDPOINTS.some((ep) => url.pathname.endsWith(ep))) {
		return next();
	}
 
	const token = c.req.header("X-Internal-API-Key");
	const secret = c.env.INTERNAL_API_KEY;
 
	// Allow bypass when ENVIRONMENT is explicitly "local" and no secret is configured.
	const isExplicitlyLocal = c.env.ENVIRONMENT === "local";
 
	if (!secret && isExplicitlyLocal) {
		logger.warn("[Security] Internal auth bypassed for local development");
		return next();
	}
 
	// In all other cases, require valid signed token
	if (!secret) {
		logger.error("[Security] INTERNAL_API_KEY not configured!");
		throw new UnauthorizedError("Internal API key not configured");
	}
 
	if (!token) {
		throw new UnauthorizedError("Missing internal auth token");
	}
 
	// Extract jobId from path for resource-bound verification
	// Paths: /website-build/:jobId/* or /website-build/next-job (no jobId)
	const pathParts = url.pathname.split("/");
	// Find the segment after "website-build"
	const buildIdx = pathParts.indexOf("website-build");
	const jobIdStr = buildIdx >= 0 ? (pathParts[buildIdx + 1] ?? "") : "";
	// Only use as jobId if it's numeric; otherwise fall back to empty string
	// (non-job paths like /next-job use empty string, which is OK — the signed
	//  token for polling uses jobId="" and the DO signing must match)
	const jobId = /^\d+$/.test(jobIdStr) ? jobIdStr : "";
 
	const valid = await verifyS2SRequest(secret, "pro-sites", jobId, token);
	if (!valid) {
		const tokenPrefix = `${token.slice(0, 8)}...`;
		logger.error(`[InternalAPI] HMAC verify failed — token=${tokenPrefix} path=${url.pathname} jobId=${jobId}`);
		throw new UnauthorizedError("Invalid or expired internal auth token");
	}
 
	return next();
});
 
// Mount sub-routers
buildRoutes.route("/", buildJobRoutes);
buildRoutes.route("/", buildProDataRoutes);
 
export default buildRoutes;