xtablo-source/apps/main/worker/index.ts
Arthur Belleville 78875f7a9e
fix: rename static icons so worker can intercept all icon requests
Cloudflare serves static assets before the worker runs, so the icon
redirect logic was never reached. Renamed the default icon files to
default-* prefix. The worker now handles all requests for the original
icon paths: redirects to org-specific icons when cookie is set, or to
the renamed defaults otherwise.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:55:21 +02:00

104 lines
3.4 KiB
TypeScript

export function parseOrgIdFromCookie(cookieHeader: string | null): number | null {
if (!cookieHeader) return null;
const match = cookieHeader.match(/(?:^|;\s*)x-org-id=(\d+)/);
if (!match) return null;
const id = Number.parseInt(match[1], 10);
return Number.isNaN(id) ? null : id;
}
interface ManifestIcon {
src: string;
sizes: string;
type: string;
purpose?: string;
}
interface WebAppManifest {
name: string;
short_name: string;
description: string;
start_url: string;
display: string;
orientation: string;
theme_color: string;
background_color: string;
icons: ManifestIcon[];
}
const ASSETS_BASE_URL = "https://assets.xtablo.com";
export function buildManifest(orgId: number | null): WebAppManifest {
const base = {
name: "XTablo",
short_name: "XTablo",
description: "Collaborative project management for construction teams",
start_url: "/",
display: "standalone",
orientation: "any",
theme_color: "#1e1b2e",
background_color: "#1e1b2e",
};
if (orgId !== null) {
return {
...base,
icons: [
{ src: `${ASSETS_BASE_URL}/org-icons/${orgId}/icon-192.png`, sizes: "192x192", type: "image/png" },
{ src: `${ASSETS_BASE_URL}/org-icons/${orgId}/icon-512.png`, sizes: "512x512", type: "image/png" },
{ src: `${ASSETS_BASE_URL}/org-icons/${orgId}/icon-512-maskable.png`, sizes: "512x512", type: "image/png", purpose: "maskable" },
],
};
}
return {
...base,
icons: [
{ src: "/pwa-icons/pwa-192x192.png", sizes: "192x192", type: "image/png" },
{ src: "/pwa-icons/pwa-512x512.png", sizes: "512x512", type: "image/png" },
{ src: "/pwa-icons/pwa-512x512-maskable.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
],
};
}
export default {
fetch(request: Request) {
const url = new URL(request.url);
// Serve org-specific or default PWA icons
// Static files are renamed to default-* so the worker handles all requests for these paths
const iconMap: Record<string, { orgFile: string; defaultFile: string }> = {
"/pwa-icons/apple-touch-icon-180x180.png": { orgFile: "icon-180.png", defaultFile: "/pwa-icons/default-apple-touch-icon-180x180.png" },
"/pwa-icons/favicon-32x32.png": { orgFile: "icon-32.png", defaultFile: "/pwa-icons/default-favicon-32x32.png" },
"/pwa-icons/favicon-16x16.png": { orgFile: "icon-16.png", defaultFile: "/pwa-icons/default-favicon-16x16.png" },
};
if (iconMap[url.pathname]) {
const cookieHeader = request.headers.get("cookie");
const orgId = parseOrgIdFromCookie(cookieHeader);
const entry = iconMap[url.pathname];
if (orgId !== null) {
return Response.redirect(`${ASSETS_BASE_URL}/org-icons/${orgId}/${entry.orgFile}`, 302);
}
// No cookie — serve default icon
return Response.redirect(`${url.origin}${entry.defaultFile}`, 302);
}
if (url.pathname === "/manifest.webmanifest") {
const cookieHeader = request.headers.get("cookie");
const orgId = parseOrgIdFromCookie(cookieHeader);
const manifest = buildManifest(orgId);
return new Response(JSON.stringify(manifest), {
headers: {
"Content-Type": "application/manifest+json",
"Cache-Control": "no-cache",
},
});
}
if (url.pathname.startsWith("/api/")) {
return Response.json({ name: "Cloudflare" });
}
return new Response(null, { status: 404 });
},
};