type StoredFileCapture = { url?: string; path?: string; }; /** * Reduce any stored file reference to the canonical relative storage path * (e.g. `files/claim-captures/x.jpg`). * * Handles values that were persisted as absolute URLs on a different portal * (e.g. `https://host/car-damage/user/api/files/...`) so we never leak a * baked-in host/prefix into responses served from another origin. */ function toRelativeFilePath(value: string | null | undefined): string { const s = String(value ?? "").trim(); if (!s) return ""; // Prefer the `files/...` segment wherever it appears in the string. const match = s.match(/(?:^|\/)(files\/.+)$/i); if (match) return match[1]; // No recognizable file root: keep external URLs intact, otherwise strip // any leading slashes so we can safely prefix the base URL. if (/^https?:\/\//i.test(s)) return s; return s.replace(/^\/+/, ""); } /** * Build a public file URL from a stored path (relative or absolute). * * The link is always rebuilt from the current `URL` env, so deployments with * custom routing (different sub-paths per portal) stay correct even when the * persisted value was created elsewhere. */ export function buildFileLink(path: string): string { const relative = toRelativeFilePath(path); if (!relative) return ""; // Already an absolute external URL with no recognizable file root. if (/^https?:\/\//i.test(relative)) return relative; const baseUrl = (process.env.URL ?? "").replace(/\/+$/, ""); if (process.env.NODE_ENV === "local") { return `${baseUrl}/${relative}`; } return `${baseUrl}/api/${relative}`; } /** * Resolve a file URL from persisted capture metadata. * * Prefer `path`, fall back to `url`, and always rebuild through * {@link buildFileLink} so stale portal-prefixed values (e.g. `.../user/api/...`) * are normalized to the current server origin. */ export function resolveStoredFileUrl( stored?: StoredFileCapture | null, ): string | undefined { const source = stored?.path?.trim() || stored?.url?.trim(); if (!source) return undefined; return buildFileLink(source); }