diff --git a/tests/site-archive.test.ts b/tests/site-archive.test.ts index b8a68d1..5df6685 100644 --- a/tests/site-archive.test.ts +++ b/tests/site-archive.test.ts @@ -61,10 +61,12 @@ test("actual tar retains both the previous entry and its lazy imports", async t await check(await archive(t, members())); }); -test("v2 accepts only changing Cloudflare JSD request metadata while preserving all HTML and script checks", async t => { - const jsd = (ray: string, time: string) => ``; - const captured = shell(oldName).replace("
", jsd("a4942470f8500900", "MTc5MTc4NzUzMg==") + ""); - const active = captured.replace("a4942470f8500900", "a49426f59e37cb83").replace("MTc5MTc4NzUzMg==", "MTc5MTc4NzYzNg=="); +for (const capturedWrapper of ["legacy", "parent-location"]) for (const activeWrapper of ["legacy", "parent-location"]) test(`v2 accepts known Cloudflare JSD wrappers (${capturedWrapper} to ${activeWrapper})`, async t => { + const continuation = (wrapper: string) => wrapper === "legacy" ? "var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);" + : "var l=window.parent.location,o;try{o=new URL(l.origin).origin}catch(e){try{o=new URL(new URL(l.href).origin).origin}catch(e){o=''}}var u='/cdn-cgi/challenge-platform/scripts/jsd/main.js?ray=REQUEST_RAY',a=document.createElement('script');a.src=o?new URL(u,o).href:u;document.getElementsByTagName('head')[0].appendChild(a);"; + const jsd = (wrapper: string, ray: string, time: string) => ``; + const captured = shell(oldName).replace("", jsd(capturedWrapper, "a4942470f8500900", "MTc5MTc4NzUzMg==") + ""); + const active = shell(oldName).replace("", jsd(activeWrapper, "a49426f59e37cb83", "MTc5MTc4NzYzNg==") + ""); const probe = previousShellProbe(captured, new URL(origin)); const entries = members().map(item => item.name.endsWith("previous-shell.html") ? { ...item, text:probe } : item.name.endsWith("previous-shell.json") ? { ...item, text:JSON.stringify({version:2,capturedAt:now,sourcePath:"/next/relay-forge/",sourceSha256:sha(normalizePublicShell(captured)),probeSha256:sha(probe)}) } : item); @@ -72,7 +74,9 @@ test("v2 accepts only changing Cloudflare JSD request metadata while preserving const serve = (html: string): typeof fetch => async (input, options) => String(input).endsWith("retained-releases.json") ? server()(input, options) : new Response(html, {headers:{"content-type":"text/html"}}); await check(tar, serve(active)); - for (const altered of [active.replace("jsd/main.js", "jsd/altered.js"), active.replace("", ""), active.replace(oldName, newName), active.replace("MTc5MTc4NzYzNg==", "YWxlcnQoMSk=")]) { + const alterations = [active.replace("jsd/main.js", "jsd/altered.js"), active.replace("", ""), active.replace(oldName, newName), active.replace("MTc5MTc4NzYzNg==", "YWxlcnQoMSk="), active.replace("document.getElementsByTagName('head')[0].appendChild(a);", "document.body.appendChild(a);")]; + if (activeWrapper === "parent-location") alterations.push(active.replace("?ray=a49426f59e37cb83", "?ray=invalid")); + for (const altered of alterations) { await assert.rejects(check(tar, serve(altered)), /Previous-shell/); } const legacy = entries.map(item => item.name.endsWith("previous-shell.json") ? {...item,text:JSON.stringify({version:1,capturedAt:now,sourcePath:"/next/relay-forge/",sourceSha256:sha(captured),probeSha256:sha(probe)})}:item); diff --git a/tools/check-site-archive.mts b/tools/check-site-archive.mts index ddc92b6..8007b49 100644 --- a/tools/check-site-archive.mts +++ b/tools/check-site-archive.mts @@ -86,8 +86,9 @@ export async function assertSiteArchiveSafe(options: { if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) { assertSiteArchiveSafe({ archive: process.argv[2] || "", siteUrl: process.argv[3] || "" }).then(() => { console.log("Deployment archive retention verified."); - }).catch(() => { + }).catch((error: unknown) => { console.error("Archive verification failed; rebuild with current retained assets before uploading."); + console.error(error instanceof Error ? error.message : "Unknown archive verification error."); process.exitCode = 1; }); } diff --git a/tools/retain-site-assets.mts b/tools/retain-site-assets.mts index 6ca115b..742ad07 100644 --- a/tools/retain-site-assets.mts +++ b/tools/retain-site-assets.mts @@ -8,9 +8,14 @@ export const ASSET_RETENTION_MS = 48 * 60 * 60 * 1000; export const PREVIOUS_SHELL_PATH = "/deployment-check/previous-shell.html"; export const PREVIOUS_SHELL_META_PATH = "/deployment-check/previous-shell.json"; export function normalizePublicShell(html: string): string { - // Compare only this known Cloudflare JSD request metadata; keep its code and the stored probe intact. - return html.replace(/(d\.innerHTML="window\.__CF\$cv\$params=\{r:')[a-f0-9]{16}(',t:')([A-Za-z0-9+/]+={0,2})('\};var a=document\.createElement\('script'\);a\.src='\/cdn-cgi\/challenge-platform\/scripts\/jsd\/main\.js';)/g, - (match, before: string, between: string, time: string, after: string) => /^\d{10}$/.test(Buffer.from(time, "base64").toString("utf8")) ? `${before}REQUEST_RAY${between}REQUEST_TIME${after}` : match); + // Cloudflare alternates these two exact loaders per request; any other HTML/code change must still fail comparison. + const prefix = "d.innerHTML=\"window.__CF$cv$params={r:'REQUEST_RAY',t:'REQUEST_TIME'};"; + const legacy = "var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);"; + const parent = "var l=window.parent.location,o;try{o=new URL(l.origin).origin}catch(e){try{o=new URL(new URL(l.href).origin).origin}catch(e){o=''}}var u='/cdn-cgi/challenge-platform/scripts/jsd/main.js?ray=REQUEST_RAY',a=document.createElement('script');a.src=o?new URL(u,o).href:u;document.getElementsByTagName('head')[0].appendChild(a);"; + return html.replace(/(d\.innerHTML="window\.__CF\$cv\$params=\{r:')[a-f0-9]{16}(',t:')([A-Za-z0-9+/]+={0,2})('\};(?:var a=document\.createElement\('script'\);a\.src='\/cdn-cgi\/challenge-platform\/scripts\/jsd\/main\.js';|var l=window\.parent\.location,o;))/g, + (match, before: string, between: string, time: string, after: string) => /^\d{10}$/.test(Buffer.from(time, "base64").toString("utf8")) ? `${before}REQUEST_RAY${between}REQUEST_TIME${after}` : match) + .replace(/(var u='\/cdn-cgi\/challenge-platform\/scripts\/jsd\/main\.js\?ray=)[a-f0-9]{16}(',a=document\.createElement\('script'\);)/g, "$1REQUEST_RAY$2") + .replaceAll(prefix + parent + '"', prefix + legacy + '"'); } export function previousShellProbe(html: string, origin: URL): string { if (!/
]*)?>/i.test(html) || /