79 lines
3.2 KiB
TypeScript
79 lines
3.2 KiB
TypeScript
import type { WebRuntimeSurface } from "@hivegrid/config";
|
|
|
|
export type ReactRouterManifestDispatchDecision = { type: "dispatch"; request: Request } | { type: "reject"; status: 404 };
|
|
|
|
const portalInternalNamespace = "/__portal";
|
|
const portalTenantHeader = "x-hivegrid-portal-tenant";
|
|
const portalCustomHostHeader = "x-hivegrid-portal-custom-host";
|
|
const portalTenantSlugPattern = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
const operationalPaths = new Set(["/healthz", "/readyz", "/healthcheck", "/api/metrics"]);
|
|
const portalFrameworkPaths = new Set(["/__manifest"]);
|
|
|
|
/**
|
|
* Crosses the final trust boundary between host routing and the selected
|
|
* React Router manifest. Portal requests must arrive in the reserved internal
|
|
* namespace with a matching trusted tenant header. The namespace is removed
|
|
* before route matching so SSR and browser hydration both see the public URL.
|
|
*/
|
|
export function prepareReactRouterManifestRequest(request: Request, runtimeSurface: WebRuntimeSurface): ReactRouterManifestDispatchDecision {
|
|
const url = new URL(request.url);
|
|
|
|
if (runtimeSurface === "core" || runtimeSurface === "admin") {
|
|
if (isPortalInternalPath(url.pathname) || request.headers.has(portalTenantHeader)) {
|
|
return { type: "reject", status: 404 };
|
|
}
|
|
return { type: "dispatch", request };
|
|
}
|
|
|
|
if (operationalPaths.has(url.pathname)) {
|
|
if (request.headers.has(portalTenantHeader)) {
|
|
return { type: "reject", status: 404 };
|
|
}
|
|
return { type: "dispatch", request };
|
|
}
|
|
|
|
if (portalFrameworkPaths.has(url.pathname)) {
|
|
return { type: "dispatch", request };
|
|
}
|
|
|
|
if (!url.pathname.startsWith(`${portalInternalNamespace}/`)) {
|
|
return { type: "reject", status: 404 };
|
|
}
|
|
|
|
const publicPathname = url.pathname.slice(portalInternalNamespace.length);
|
|
const rawTenantSegment = publicPathname.slice(1).split("/", 1)[0] ?? "";
|
|
const tenantSlug = rawTenantSegment.replace(/\.data$/, "");
|
|
const trustedTenantSlug = request.headers.get(portalTenantHeader);
|
|
if (!trustedTenantSlug || trustedTenantSlug !== tenantSlug || !portalTenantSlugPattern.test(tenantSlug)) {
|
|
return { type: "reject", status: 404 };
|
|
}
|
|
|
|
const customHostPathname = request.headers.has(portalCustomHostHeader) ? stripTrustedTenantFromCustomHostPath(publicPathname, tenantSlug) : null;
|
|
if (request.headers.has(portalCustomHostHeader) && !customHostPathname) {
|
|
return { type: "reject", status: 404 };
|
|
}
|
|
url.pathname = customHostPathname ?? publicPathname;
|
|
const init: RequestInit & { duplex?: "half" } = {
|
|
method: request.method,
|
|
headers: request.headers,
|
|
signal: request.signal,
|
|
};
|
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
init.body = request.body;
|
|
init.duplex = "half";
|
|
}
|
|
return { type: "dispatch", request: new Request(url, init) };
|
|
}
|
|
|
|
function stripTrustedTenantFromCustomHostPath(pathname: string, tenantSlug: string): string | null {
|
|
const tenantPrefix = `/${tenantSlug}`;
|
|
if (pathname === tenantPrefix) return "/";
|
|
if (pathname === `${tenantPrefix}.data`) return "/.data";
|
|
if (pathname.startsWith(`${tenantPrefix}/`)) return pathname.slice(tenantPrefix.length);
|
|
return null;
|
|
}
|
|
|
|
function isPortalInternalPath(pathname: string) {
|
|
return pathname === portalInternalNamespace || pathname.startsWith(`${portalInternalNamespace}/`);
|
|
}
|