diff --git a/worker/src/loop-routes.js b/worker/src/loop-routes.js index bbe6d35..8365d3f 100644 --- a/worker/src/loop-routes.js +++ b/worker/src/loop-routes.js @@ -16,6 +16,8 @@ import { const MAX_ADMIN_BYTES = 512 * 1024; const MAX_RESTORE_START_BYTES = 20 * 1024 * 1024; const MAX_RESTORE_CHUNK_BYTES = 4 * 1024 * 1024; +const LEGACY_PUBLIC_DOMAIN = "forwardfuture.ai"; +const CURRENT_PUBLIC_DOMAIN = "forwardfuture.com"; const CACHE_HEADERS = { // Publishing is expected to be immediately visible on every generated surface. // Reintroduce CDN caching only with catalog-revision cache keys or explicit purge. @@ -70,7 +72,7 @@ export async function handleLoopRoute( } const catalog = await readPublishedCatalog(env); - const loops = catalog.loops; + const loops = catalog.loops.map(normalizePublishedDomain); if (!catalog.initialized) { // A direct canonical route can safely read the legacy origin during a @@ -601,6 +603,21 @@ async function readPublishedCatalog(env) { return response.json(); } +function normalizePublishedDomain(value) { + if (typeof value === "string") { + return value.replaceAll(LEGACY_PUBLIC_DOMAIN, CURRENT_PUBLIC_DOMAIN); + } + if (Array.isArray(value)) { + return value.map(normalizePublishedDomain); + } + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [key, normalizePublishedDomain(item)]), + ); + } + return value; +} + function catalogFetch(env, path, init) { const id = env.LOOP_CATALOG.idFromName("published-loops"); return env.LOOP_CATALOG.get(id).fetch(`https://loop-catalog${path}`, { diff --git a/worker/test/loop-routes.test.js b/worker/test/loop-routes.test.js index 8e26b9b..100dccc 100644 --- a/worker/test/loop-routes.test.js +++ b/worker/test/loop-routes.test.js @@ -472,6 +472,33 @@ test("renders the mounted homepage through a here.now proxy", async () => { assert.match(await response.text(), /The database publishing loop/); }); +test("normalizes legacy Forward Future domains on every public catalog surface", async () => { + const env = makeEnv(); + await handleRequest( + adminRequest(exampleLoop({ + prompt: "Read https://signals.forwardfuture.ai/loop-library/api/loops before continuing.", + })), + env, + ); + const shell = `

Showing 0 loops

`; + const responses = await Promise.all([ + handleRequest( + new Request(`${SITE_ORIGIN}/loop-library/`), + env, + undefined, + { async fetch() { return new Response(shell, { headers: { "Content-Type": "text/html" } }); } }, + ), + handleRequest(new Request(`${SITE_ORIGIN}/loop-library/catalog.json`), env), + handleRequest(new Request(`${SITE_ORIGIN}/loop-library/api/loops`), env), + ]); + + for (const response of responses) { + const body = await response.text(); + assert.doesNotMatch(body, /forwardfuture\.ai/); + assert.match(body, /signals\.forwardfuture\.com\/loop-library\/api\/loops/); + } +}); + test("does not recurse through the here.now proxy before activation", async () => { const env = makeEnv({ active: false }); let originFetches = 0;