From 5733251f776b74b6ac078b11de1dab3165368f3a Mon Sep 17 00:00:00 2001 From: Willie Chalmers III Date: Wed, 23 Sep 2026 21:07:34 -0700 Subject: [PATCH 1/3] fix: Preserve the sign-out redirect during Worker migration The sign-out URL now redirects permanently to its canonical page on Workers, matching the public Pages response. Live parity checks cover the route so the status remains consistent through cutover. Co-authored-by: Codex --- functions/sign-out/index.ts | 10 ++++++++-- scripts/audit/worker-live-parity.ts | 1 + tests/sign-out-route.test.ts | 17 +++++++++++++++++ tests/worker-live-parity.test.ts | 1 + 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 tests/sign-out-route.test.ts diff --git a/functions/sign-out/index.ts b/functions/sign-out/index.ts index 97360b3..81a9291 100644 --- a/functions/sign-out/index.ts +++ b/functions/sign-out/index.ts @@ -2,12 +2,18 @@ // /sign-out: the header's "Sign out" button posts here. It ends the session // on the server, clears both sign-in cookies and shows the signed-out page. -// GET falls through to that built page. +// GET redirects permanently to the canonical trailing-slash page. import { clearedSessionCookies, fromThisSite, signOut } from '../../platform/sign-in'; -import { redirect } from '../join/_page'; +import { redirect, SECURITY_HEADERS } from '../join/_page'; import { appendCookies, platformSignIn, refused, type SignInPagesEnv } from '../sign-in/_shared'; +export const onRequestGet: PagesFunction = () => + new Response(null, { + status: 308, + headers: { ...SECURITY_HEADERS, Location: '/sign-out/' }, + }); + export const onRequestPost: PagesFunction = async ({ env, request }) => { if (!fromThisSite(request)) return refused(); const platform = platformSignIn(env); diff --git a/scripts/audit/worker-live-parity.ts b/scripts/audit/worker-live-parity.ts index 4d18801..3f3db10 100644 --- a/scripts/audit/worker-live-parity.ts +++ b/scripts/audit/worker-live-parity.ts @@ -87,6 +87,7 @@ export function parityCases(includeApis = true): Array<{ method?: string; pathna { pathname: '/about/' }, { pathname: '/not-a-real-page' }, { pathname: '/get-involved' }, + { pathname: '/sign-out' }, { pathname: '/sitemap.xml' }, { pathname: '/week-without-driving' }, { pathname: '/projects/social-media-just-talking' }, diff --git a/tests/sign-out-route.test.ts b/tests/sign-out-route.test.ts new file mode 100644 index 0000000..4b8963e --- /dev/null +++ b/tests/sign-out-route.test.ts @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import * as signOutRoute from '../functions/sign-out/index'; + +void test('canonicalizes the sign-out GET with a permanent redirect', async () => { + const handler: unknown = Reflect.get(signOutRoute, 'onRequestGet'); + assert.equal(typeof handler, 'function'); + + const response = await (handler as () => Response | Promise)(); + assert.equal(response.status, 308); + assert.equal(response.headers.get('location'), '/sign-out/'); + assert.equal( + response.headers.get('strict-transport-security'), + 'max-age=63072000; includeSubDomains; preload', + ); +}); diff --git a/tests/worker-live-parity.test.ts b/tests/worker-live-parity.test.ts index 2bd84d4..3078529 100644 --- a/tests/worker-live-parity.test.ts +++ b/tests/worker-live-parity.test.ts @@ -87,6 +87,7 @@ void test('detects the shared analytics integration in built pages', async () => void test('keeps protected API parity out of pull request previews', () => { assert.equal(parityCases().filter(({ pathname }) => pathname.startsWith('/api/')).length, 2); + assert.ok(parityCases().some(({ pathname }) => pathname === '/sign-out')); assert.equal( parityCases(false).some(({ pathname }) => pathname.startsWith('/api/')), false, From 2dd2faf912f2c019b3b641a28729b8c41087831a Mon Sep 17 00:00:00 2001 From: Willie Chalmers III Date: Thu, 24 Sep 2026 09:55:55 -0700 Subject: [PATCH 2/3] fix: Keep the sign-out page reachable on Workers Only the slashless URL redirects permanently. The canonical page now serves normally, including in the long-text accessibility check. Co-authored-by: Codex --- functions/sign-out/index.ts | 12 +++++++----- scripts/audit/worker-live-parity.ts | 1 + tests/sign-out-route.test.ts | 25 ++++++++++++++++++++++++- tests/worker-live-parity.test.ts | 1 + 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/functions/sign-out/index.ts b/functions/sign-out/index.ts index 81a9291..a2d5bdd 100644 --- a/functions/sign-out/index.ts +++ b/functions/sign-out/index.ts @@ -8,11 +8,13 @@ import { clearedSessionCookies, fromThisSite, signOut } from '../../platform/sig import { redirect, SECURITY_HEADERS } from '../join/_page'; import { appendCookies, platformSignIn, refused, type SignInPagesEnv } from '../sign-in/_shared'; -export const onRequestGet: PagesFunction = () => - new Response(null, { - status: 308, - headers: { ...SECURITY_HEADERS, Location: '/sign-out/' }, - }); +export const onRequestGet: PagesFunction = ({ request, next }) => + new URL(request.url).pathname === '/sign-out' + ? new Response(null, { + status: 308, + headers: { ...SECURITY_HEADERS, Location: '/sign-out/' }, + }) + : next(); export const onRequestPost: PagesFunction = async ({ env, request }) => { if (!fromThisSite(request)) return refused(); diff --git a/scripts/audit/worker-live-parity.ts b/scripts/audit/worker-live-parity.ts index 3f3db10..5a76627 100644 --- a/scripts/audit/worker-live-parity.ts +++ b/scripts/audit/worker-live-parity.ts @@ -88,6 +88,7 @@ export function parityCases(includeApis = true): Array<{ method?: string; pathna { pathname: '/not-a-real-page' }, { pathname: '/get-involved' }, { pathname: '/sign-out' }, + { pathname: '/sign-out/' }, { pathname: '/sitemap.xml' }, { pathname: '/week-without-driving' }, { pathname: '/projects/social-media-just-talking' }, diff --git a/tests/sign-out-route.test.ts b/tests/sign-out-route.test.ts index 4b8963e..2145ed5 100644 --- a/tests/sign-out-route.test.ts +++ b/tests/sign-out-route.test.ts @@ -7,7 +7,15 @@ void test('canonicalizes the sign-out GET with a permanent redirect', async () = const handler: unknown = Reflect.get(signOutRoute, 'onRequestGet'); assert.equal(typeof handler, 'function'); - const response = await (handler as () => Response | Promise)(); + const response = await ( + handler as (context: { + request: Request; + next: () => Promise; + }) => Response | Promise + )({ + request: new Request('https://lasvegasfortransit.org/sign-out'), + next: () => Promise.resolve(new Response('page')), + }); assert.equal(response.status, 308); assert.equal(response.headers.get('location'), '/sign-out/'); assert.equal( @@ -15,3 +23,18 @@ void test('canonicalizes the sign-out GET with a permanent redirect', async () = 'max-age=63072000; includeSubDomains; preload', ); }); + +void test('serves the canonical sign-out page without redirecting it to itself', async () => { + const handler: unknown = Reflect.get(signOutRoute, 'onRequestGet'); + const page = new Response('signed out'); + const response = await ( + handler as (context: { + request: Request; + next: () => Promise; + }) => Response | Promise + )({ + request: new Request('https://lasvegasfortransit.org/sign-out/'), + next: () => Promise.resolve(page), + }); + assert.equal(response, page); +}); diff --git a/tests/worker-live-parity.test.ts b/tests/worker-live-parity.test.ts index 3078529..7183051 100644 --- a/tests/worker-live-parity.test.ts +++ b/tests/worker-live-parity.test.ts @@ -88,6 +88,7 @@ void test('detects the shared analytics integration in built pages', async () => void test('keeps protected API parity out of pull request previews', () => { assert.equal(parityCases().filter(({ pathname }) => pathname.startsWith('/api/')).length, 2); assert.ok(parityCases().some(({ pathname }) => pathname === '/sign-out')); + assert.ok(parityCases().some(({ pathname }) => pathname === '/sign-out/')); assert.equal( parityCases(false).some(({ pathname }) => pathname.startsWith('/api/')), false, From 4bb29a7635eef99e4bd694df0adc07136f600686 Mon Sep 17 00:00:00 2001 From: Willie Chalmers III Date: Thu, 24 Sep 2026 10:02:53 -0700 Subject: [PATCH 3/3] fix(ci): Compare Worker previews with the correct reference Before production moves to Workers, compare the candidate with the Pages hostname. Once Workers serves production, skip that stale reference and rely on preview browser acceptance. Co-authored-by: Codex --- .github/workflows/deploy-worker-preview.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-worker-preview.yml b/.github/workflows/deploy-worker-preview.yml index 0851a20..669365d 100644 --- a/.github/workflows/deploy-worker-preview.yml +++ b/.github/workflows/deploy-worker-preview.yml @@ -76,11 +76,12 @@ jobs: exit 1 - name: Compare with Pages + if: vars.LVBT_WORKERS_PRODUCTION_ENABLED != 'true' env: PREVIEW_URL: ${{ steps.upload.outputs.url }} run: >- pnpm worker:test:live - --pages https://lasvegasfortransit.org + --pages https://lvbt-website-5zh.pages.dev --worker "$PREVIEW_URL" --skip-api