diff --git a/AGENTS.md b/AGENTS.md index 48184d6..5cc28df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,8 @@ side quests, discovery, and personal history. It owns the existing `significanthobbies` Cloudflare Worker and D1 data authority. The Worker retains apex compatibility routes for the existing web application. -The Hub path allowlist is delegated through `HUB_SERVICE` on apex hosts. +The Hub path allowlist, including the `/landing/` asset prefix (not exact +`/landing`), is delegated through `HUB_SERVICE` on apex hosts. Exactly `/hub` also delegates on the Live host to preserve its host-only session. The canonical Significant Hobbies Hub retains rendering and data ownership. Journal and Hub Backend source belong in their independent repositories and must not be reintroduced here. diff --git a/hub-routing.mjs b/hub-routing.mjs index fff12f0..f36ae20 100644 --- a/hub-routing.mjs +++ b/hub-routing.mjs @@ -33,7 +33,7 @@ const HUB_SERVICE_EXACT_PATHS = new Set([ '/mcp', ...HUB_AGENT_CONTRACT_PATHS, ]); -const HUB_SERVICE_PREFIX_PATHS = ['/.well-known/agent-skills/']; +const HUB_SERVICE_PREFIX_PATHS = ['/.well-known/agent-skills/', '/landing/']; const PERSONAL_PLATFORM_INTERNAL_HOST = 'personal-auth.internal'; export const PERSONAL_PLATFORM_INTERNAL_HEADER = 'X-Personal-Platform-Internal'; diff --git a/src/server/hub-routing.test.ts b/src/server/hub-routing.test.ts index df1c4de..7b77f9d 100644 --- a/src/server/hub-routing.test.ts +++ b/src/server/hub-routing.test.ts @@ -23,6 +23,61 @@ describe('Hub edge routing', () => { expect(isHubServicePath(pathname)).toBe(false); }); + describe('Hub landing assets', () => { + it.each([ + '/landing/', + '/landing/x', + '/landing/fonts/home.woff2', + '/landing/home.css', + '/landing/images/home.webp', + ])('delegates the precise prefix %s only on apex hosts', (pathname) => { + expect(isHubServicePath(pathname)).toBe(true); + for (const hostname of ['significanthobbies.com', 'www.significanthobbies.com']) { + const url = new URL(`https://${hostname}${pathname}`); + expect(shouldDelegateHub(url)).toBe(true); + expect(legacyLiveRedirect(url)).toBeNull(); + } + expect(shouldDelegateHub(new URL(`https://live.significanthobbies.com${pathname}`))).toBe( + false + ); + expect(shouldDelegateHub(new URL(`https://other.example${pathname}`))).toBe(false); + }); + + it.each(['/landing', '/landing-extra/home.css', '/landings/home.css'])( + 'redirects the non-prefix path %s to Live', + (pathname) => { + expect(isHubServicePath(pathname)).toBe(false); + for (const hostname of ['significanthobbies.com', 'www.significanthobbies.com']) { + const url = new URL(`https://${hostname}${pathname}`); + expect(shouldDelegateHub(url)).toBe(false); + expect(legacyLiveRedirect(url)?.href).toBe( + `https://live.significanthobbies.com${pathname}` + ); + } + } + ); + + it('forwards apex assets and preserves their Hub cache headers', async () => { + const request = new Request('https://significanthobbies.com/landing/home.css'); + const response = await fetchHubRoute(request, { + HUB_SERVICE: { + fetch: async (forwarded: Request) => { + expect(forwarded).toBe(request); + return new Response('body {}', { + headers: { 'Cache-Control': 'public, max-age=3600' }, + }); + }, + }, + }); + expect(await response?.text()).toBe('body {}'); + expect(response?.headers.get('cache-control')).toBe('public, max-age=3600'); + expect(response?.headers.has('cdn-cache-control')).toBe(false); + expect( + await fetchHubRoute(new Request('https://live.significanthobbies.com/landing/home.css'), {}) + ).toBeNull(); + }); + }); + it('keeps every Live path on the Live host', () => { expect(legacyLiveRedirect(new URL('https://live.significanthobbies.com/live-more'))).toBeNull(); });