Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion worker/src/loop-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}`, {
Expand Down
27 changes: 27 additions & 0 deletions worker/test/loop-routes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<!doctype html><p id="results-count" aria-live="polite">Showing 0 loops</p><time datetime="2026-06-20">Updated June 20, 2026</time><tbody><!-- LOOP_DATABASE_ROWS_START --><!-- LOOP_DATABASE_ROWS_END --></tbody>`;
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;
Expand Down
Loading