diff --git a/frontend/src/api/runtime-config.test.ts b/frontend/src/api/runtime-config.test.ts index 5589dddb..b646d6ef 100644 --- a/frontend/src/api/runtime-config.test.ts +++ b/frontend/src/api/runtime-config.test.ts @@ -15,6 +15,10 @@ describe("resolveBackendUrl", () => { expect(backendBaseUrl).toBe("/api"); }); + it("accepts a same-origin prefixed API base for embedded deployments", () => { + expect(resolveBackendUrl({ backendBaseUrl: "/embedded/api" })).toBe("/embedded/api"); + }); + it.each(["/backend", "api", "//evil.test", "ftp://example.test", "https://user:pass@example.test"])( "rejects any browser-facing backend URL %j", (backendBaseUrl) => { diff --git a/frontend/src/api/runtime-config.ts b/frontend/src/api/runtime-config.ts index 4533bad3..f4f95639 100644 --- a/frontend/src/api/runtime-config.ts +++ b/frontend/src/api/runtime-config.ts @@ -1,6 +1,18 @@ -export function resolveBackendUrl(value?: string): string { - if (value === undefined || value === "/api") return "/api"; - throw new Error("Invalid backend URL: the browser must use the same-origin /api route"); +export interface RuntimeConfig { + backendBaseUrl?: string; +} + +declare global { + interface Window { + __THOTHII_CONFIG__?: RuntimeConfig; + } +} + +export function resolveBackendUrl(value?: string | RuntimeConfig): string { + const candidate = typeof value === "string" ? value : value?.backendBaseUrl; + if (candidate === undefined || candidate === "/api") return "/api"; + if (/^\/(?:[A-Za-z0-9._~-]+\/)+api$/.test(candidate)) return candidate; + throw new Error("Invalid backend URL: the browser must use a same-origin API route"); } export function joinBackendPath(base: string, path: string): string { @@ -9,4 +21,6 @@ export function joinBackendPath(base: string, path: string): string { return `${normalizedBase}/${normalizedPath}`; } -export const backendBaseUrl = resolveBackendUrl(); +export const backendBaseUrl = resolveBackendUrl( + typeof window === "undefined" ? undefined : window.__THOTHII_CONFIG__, +);