From 7f764217e0240c7a9e39464322a15b4541cf1f3a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 17 May 2026 14:16:24 +0000 Subject: [PATCH 1/3] patch workshop splat route post actions Co-authored-by: Kent C. Dodds --- epicshop/package-lock.json | 1 + epicshop/package.json | 4 ++ epicshop/patch-workshop-app.js | 103 ++++++++++++++++++++++++++++ epicshop/patch-workshop-app.test.js | 68 ++++++++++++++++++ package.json | 1 + 5 files changed, 177 insertions(+) create mode 100644 epicshop/patch-workshop-app.js create mode 100644 epicshop/patch-workshop-app.test.js diff --git a/epicshop/package-lock.json b/epicshop/package-lock.json index 80839fb..4f7fb35 100644 --- a/epicshop/package-lock.json +++ b/epicshop/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "hasInstallScript": true, "dependencies": { "@epic-web/workshop-app": "^6.90.3", "@epic-web/workshop-utils": "^6.90.3", diff --git a/epicshop/package.json b/epicshop/package.json index bbb37fa..90298fd 100644 --- a/epicshop/package.json +++ b/epicshop/package.json @@ -1,5 +1,9 @@ { "type": "module", + "scripts": { + "postinstall": "node ./patch-workshop-app.js", + "test:patch-workshop-app": "node --test ./patch-workshop-app.test.js" + }, "dependencies": { "@epic-web/workshop-app": "^6.90.3", "@epic-web/workshop-utils": "^6.90.3", diff --git a/epicshop/patch-workshop-app.js b/epicshop/patch-workshop-app.js new file mode 100644 index 0000000..fc1f25f --- /dev/null +++ b/epicshop/patch-workshop-app.js @@ -0,0 +1,103 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const here = path.dirname(fileURLToPath(import.meta.url)) +const defaultServerBuildPath = path.join( + here, + 'node_modules', + '@epic-web', + 'workshop-app', + 'build', + 'server', + 'index.js', +) + +const notFoundActionFunctionName = 'action$splatNotFound' +const notFoundActionFunction = `async function ${notFoundActionFunctionName}() { + throw new Response("Not found", { + status: 404 + }); +} +` + +const loaderEndNeedle = ` throw new Response("Not found", { + status: 404 + }); +} +const $ = UNSAFE_withComponentProps` + +const patchedLoaderEndNeedle = ` throw new Response("Not found", { + status: 404 + }); +} +${notFoundActionFunction}const $ = UNSAFE_withComponentProps` + +const routeModuleNeedle = ` ErrorBoundary: ErrorBoundary$7, + default: $, + loader: loader$L +}, Symbol.toStringTag` + +const patchedRouteModuleNeedle = ` ErrorBoundary: ErrorBoundary$7, + action: ${notFoundActionFunctionName}, + default: $, + loader: loader$L +}, Symbol.toStringTag` + +export function patchWorkshopAppServerBuild(source) { + const hasActionFunction = source.includes( + `async function ${notFoundActionFunctionName}()`, + ) + const hasActionExport = source.includes( + `action: ${notFoundActionFunctionName}`, + ) + + if (hasActionFunction && hasActionExport) { + return { patched: false, source } + } + + if (hasActionFunction || hasActionExport) { + throw new Error( + 'Found a partial workshop-app splat action patch. Reinstall dependencies and rerun the patch.', + ) + } + + if (!source.includes(loaderEndNeedle)) { + throw new Error( + 'Could not find the workshop-app splat route loader to patch.', + ) + } + + if (!source.includes(routeModuleNeedle)) { + throw new Error( + 'Could not find the workshop-app splat route module to patch.', + ) + } + + return { + patched: true, + source: source + .replace(loaderEndNeedle, patchedLoaderEndNeedle) + .replace(routeModuleNeedle, patchedRouteModuleNeedle), + } +} + +export async function patchWorkshopApp({ + serverBuildPath = defaultServerBuildPath, +} = {}) { + const source = await fs.readFile(serverBuildPath, 'utf8') + const result = patchWorkshopAppServerBuild(source) + + if (result.patched) { + await fs.writeFile(serverBuildPath, result.source) + console.log( + 'Patched @epic-web/workshop-app splat route to return a normal 404 for POST scanner traffic.', + ) + } + + return result +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + await patchWorkshopApp() +} diff --git a/epicshop/patch-workshop-app.test.js b/epicshop/patch-workshop-app.test.js new file mode 100644 index 0000000..af1059e --- /dev/null +++ b/epicshop/patch-workshop-app.test.js @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { + patchWorkshopApp, + patchWorkshopAppServerBuild, +} from './patch-workshop-app.js' + +const unpatchedServerBuildFixture = `async function loader$L({ + params +}) { + const splat = params["*"]; + const segments = splat?.split("/") ?? []; + if (segments.length > 0 && !isNaN(Number(segments[0]))) { + const newPath = \`/exercise/\${splat}\`; + return new Response(null, { + status: 302, + headers: { + Location: newPath + } + }); + } + throw new Response("Not found", { + status: 404 + }); +} +const $ = UNSAFE_withComponentProps(function NotFound() { + return /* @__PURE__ */ jsx(ErrorBoundary$7, {}); +}); +const ErrorBoundary$7 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary2() { + return null; +}); +const route1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + ErrorBoundary: ErrorBoundary$7, + default: $, + loader: loader$L +}, Symbol.toStringTag, { value: "Module" })); +` + +test('adds a 404 action to the workshop-app splat route module', () => { + const result = patchWorkshopAppServerBuild(unpatchedServerBuildFixture) + + assert.equal(result.patched, true) + assert.match(result.source, /async function action\$splatNotFound\(\)/) + assert.match(result.source, /action: action\$splatNotFound/) +}) + +test('does not modify an already patched server build', () => { + const firstResult = patchWorkshopAppServerBuild(unpatchedServerBuildFixture) + const secondResult = patchWorkshopAppServerBuild(firstResult.source) + + assert.equal(secondResult.patched, false) + assert.equal(secondResult.source, firstResult.source) +}) + +test('fails loudly when the expected route shape changes', () => { + assert.throws( + () => patchWorkshopAppServerBuild('const route1 = {}'), + /Could not find the workshop-app splat route loader/, + ) +}) + +test('patches the installed workshop-app build', async () => { + const result = await patchWorkshopApp() + + assert.equal(typeof result.patched, 'boolean') + assert.match(result.source, /action: action\$splatNotFound/) +}) diff --git a/package.json b/package.json index 83bb4b9..208622e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "test:e2e": "pkgmgr run test:e2e --silent --prefix playground", "test:e2e:dev": "pkgmgr run test:e2e:dev --silent --prefix playground", "test:e2e:run": "pkgmgr run test:e2e:run --silent --prefix playground", + "test:patch-workshop-app": "npm run test:patch-workshop-app --prefix epicshop", "setup": "pkgmgrx epicshop setup", "setup:custom": "node ./epicshop/setup-custom.js", "lint": "eslint . --concurrency=auto", From c98aee32b7232c5e30998ff0165c9f7924159cb5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 17 May 2026 14:18:28 +0000 Subject: [PATCH 2/3] mark splat route action in workshop manifest Co-authored-by: Kent C. Dodds --- epicshop/patch-workshop-app.js | 55 ++++++++++++++++++++++------- epicshop/patch-workshop-app.test.js | 3 ++ 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/epicshop/patch-workshop-app.js b/epicshop/patch-workshop-app.js index fc1f25f..e2d36f9 100644 --- a/epicshop/patch-workshop-app.js +++ b/epicshop/patch-workshop-app.js @@ -44,6 +44,12 @@ const patchedRouteModuleNeedle = ` ErrorBoundary: ErrorBoundary$7, loader: loader$L }, Symbol.toStringTag` +const routeManifestNeedle = + '"routes/$": { "id": "routes/$", "parentId": "root", "path": "*", "index": void 0, "caseSensitive": void 0, "hasAction": false,' + +const patchedRouteManifestNeedle = + '"routes/$": { "id": "routes/$", "parentId": "root", "path": "*", "index": void 0, "caseSensitive": void 0, "hasAction": true,' + export function patchWorkshopAppServerBuild(source) { const hasActionFunction = source.includes( `async function ${notFoundActionFunctionName}()`, @@ -51,34 +57,57 @@ export function patchWorkshopAppServerBuild(source) { const hasActionExport = source.includes( `action: ${notFoundActionFunctionName}`, ) + const hasActionManifest = source.includes(patchedRouteManifestNeedle) - if (hasActionFunction && hasActionExport) { + if (hasActionFunction && hasActionExport && hasActionManifest) { return { patched: false, source } } - if (hasActionFunction || hasActionExport) { + if (hasActionFunction !== hasActionExport) { throw new Error( 'Found a partial workshop-app splat action patch. Reinstall dependencies and rerun the patch.', ) } - if (!source.includes(loaderEndNeedle)) { - throw new Error( - 'Could not find the workshop-app splat route loader to patch.', - ) + let patchedSource = source + let patched = false + + if (!hasActionFunction) { + if (!patchedSource.includes(loaderEndNeedle)) { + throw new Error( + 'Could not find the workshop-app splat route loader to patch.', + ) + } + + if (!patchedSource.includes(routeModuleNeedle)) { + throw new Error( + 'Could not find the workshop-app splat route module to patch.', + ) + } + + patchedSource = patchedSource + .replace(loaderEndNeedle, patchedLoaderEndNeedle) + .replace(routeModuleNeedle, patchedRouteModuleNeedle) + patched = true } - if (!source.includes(routeModuleNeedle)) { - throw new Error( - 'Could not find the workshop-app splat route module to patch.', + if (!hasActionManifest) { + if (!patchedSource.includes(routeManifestNeedle)) { + throw new Error( + 'Could not find the workshop-app splat route manifest entry to patch.', + ) + } + + patchedSource = patchedSource.replace( + routeManifestNeedle, + patchedRouteManifestNeedle, ) + patched = true } return { - patched: true, - source: source - .replace(loaderEndNeedle, patchedLoaderEndNeedle) - .replace(routeModuleNeedle, patchedRouteModuleNeedle), + patched, + source: patchedSource, } } diff --git a/epicshop/patch-workshop-app.test.js b/epicshop/patch-workshop-app.test.js index af1059e..ed5219e 100644 --- a/epicshop/patch-workshop-app.test.js +++ b/epicshop/patch-workshop-app.test.js @@ -35,6 +35,7 @@ const route1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper default: $, loader: loader$L }, Symbol.toStringTag, { value: "Module" })); +const serverManifest = { "routes": { "routes/$": { "id": "routes/$", "parentId": "root", "path": "*", "index": void 0, "caseSensitive": void 0, "hasAction": false, "hasLoader": true } } }; ` test('adds a 404 action to the workshop-app splat route module', () => { @@ -43,6 +44,7 @@ test('adds a 404 action to the workshop-app splat route module', () => { assert.equal(result.patched, true) assert.match(result.source, /async function action\$splatNotFound\(\)/) assert.match(result.source, /action: action\$splatNotFound/) + assert.match(result.source, /"routes\/\$": .*"hasAction": true/) }) test('does not modify an already patched server build', () => { @@ -65,4 +67,5 @@ test('patches the installed workshop-app build', async () => { assert.equal(typeof result.patched, 'boolean') assert.match(result.source, /action: action\$splatNotFound/) + assert.match(result.source, /"routes\/\$": .*"hasAction": true/) }) From fb8de48e13559adb84c1d1024252146ec394ba6b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 17 May 2026 14:20:43 +0000 Subject: [PATCH 3/3] handle repeated slash scanner posts Co-authored-by: Kent C. Dodds --- epicshop/patch-workshop-app.js | 73 ++++++++++++++++++++++++++--- epicshop/patch-workshop-app.test.js | 29 +++++++++++- 2 files changed, 94 insertions(+), 8 deletions(-) diff --git a/epicshop/patch-workshop-app.js b/epicshop/patch-workshop-app.js index e2d36f9..33fd6ad 100644 --- a/epicshop/patch-workshop-app.js +++ b/epicshop/patch-workshop-app.js @@ -12,6 +12,15 @@ const defaultServerBuildPath = path.join( 'server', 'index.js', ) +const defaultServerRuntimePath = path.join( + here, + 'node_modules', + '@epic-web', + 'workshop-app', + 'dist', + 'server', + 'index.js', +) const notFoundActionFunctionName = 'action$splatNotFound' const notFoundActionFunction = `async function ${notFoundActionFunctionName}() { @@ -50,6 +59,21 @@ const routeManifestNeedle = const patchedRouteManifestNeedle = '"routes/$": { "id": "routes/$", "parentId": "root", "path": "*", "index": void 0, "caseSensitive": void 0, "hasAction": true,' +const repeatedSlashMiddleware = `app.use((req, res, next) => { + const requestPath = req.originalUrl.split("?")[0]; + if (req.method !== "GET" && req.method !== "HEAD" && req.method !== "OPTIONS" && /^\\/{2,}$/.test(requestPath)) { + res.status(404).send("Not found"); + return; + } + next(); +}); +` + +const requestContextMiddlewareNeedle = + 'app.use((_req, _res, next) => requestContext.run({}, next));\n' + +const patchedRequestContextMiddlewareNeedle = `${requestContextMiddlewareNeedle}${repeatedSlashMiddleware}` + export function patchWorkshopAppServerBuild(source) { const hasActionFunction = source.includes( `async function ${notFoundActionFunctionName}()`, @@ -111,20 +135,57 @@ export function patchWorkshopAppServerBuild(source) { } } +export function patchWorkshopAppServerRuntime(source) { + if (source.includes(repeatedSlashMiddleware)) { + return { patched: false, source } + } + + if (!source.includes(requestContextMiddlewareNeedle)) { + throw new Error( + 'Could not find the workshop-app request context middleware to patch.', + ) + } + + return { + patched: true, + source: source.replace( + requestContextMiddlewareNeedle, + patchedRequestContextMiddlewareNeedle, + ), + } +} + export async function patchWorkshopApp({ serverBuildPath = defaultServerBuildPath, + serverRuntimePath = defaultServerRuntimePath, } = {}) { - const source = await fs.readFile(serverBuildPath, 'utf8') - const result = patchWorkshopAppServerBuild(source) - - if (result.patched) { - await fs.writeFile(serverBuildPath, result.source) + const [serverBuildSource, serverRuntimeSource] = await Promise.all([ + fs.readFile(serverBuildPath, 'utf8'), + fs.readFile(serverRuntimePath, 'utf8'), + ]) + const serverBuildResult = patchWorkshopAppServerBuild(serverBuildSource) + const serverRuntimeResult = patchWorkshopAppServerRuntime(serverRuntimeSource) + + await Promise.all([ + serverBuildResult.patched + ? fs.writeFile(serverBuildPath, serverBuildResult.source) + : null, + serverRuntimeResult.patched + ? fs.writeFile(serverRuntimePath, serverRuntimeResult.source) + : null, + ]) + + if (serverBuildResult.patched || serverRuntimeResult.patched) { console.log( 'Patched @epic-web/workshop-app splat route to return a normal 404 for POST scanner traffic.', ) } - return result + return { + patched: serverBuildResult.patched || serverRuntimeResult.patched, + serverBuild: serverBuildResult, + serverRuntime: serverRuntimeResult, + } } if (import.meta.url === pathToFileURL(process.argv[1]).href) { diff --git a/epicshop/patch-workshop-app.test.js b/epicshop/patch-workshop-app.test.js index ed5219e..07ccbd7 100644 --- a/epicshop/patch-workshop-app.test.js +++ b/epicshop/patch-workshop-app.test.js @@ -3,6 +3,7 @@ import test from 'node:test' import { patchWorkshopApp, patchWorkshopAppServerBuild, + patchWorkshopAppServerRuntime, } from './patch-workshop-app.js' const unpatchedServerBuildFixture = `async function loader$L({ @@ -38,6 +39,13 @@ const route1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper const serverManifest = { "routes": { "routes/$": { "id": "routes/$", "parentId": "root", "path": "*", "index": void 0, "caseSensitive": void 0, "hasAction": false, "hasLoader": true } } }; ` +const unpatchedServerRuntimeFixture = `app.use((_req, _res, next) => requestContext.run({}, next)); +app.options("*splat", (_req, res) => { + res.set("Allow", "GET, HEAD, POST, OPTIONS"); + res.sendStatus(204); +}); +` + test('adds a 404 action to the workshop-app splat route module', () => { const result = patchWorkshopAppServerBuild(unpatchedServerBuildFixture) @@ -62,10 +70,27 @@ test('fails loudly when the expected route shape changes', () => { ) }) +test('adds a narrow repeated-slash POST guard before React Router', () => { + const result = patchWorkshopAppServerRuntime(unpatchedServerRuntimeFixture) + + assert.equal(result.patched, true) + assert.match(result.source, /requestPath = req\.originalUrl\.split/) + assert.match(result.source, /\^\\\/\{2,\}\$/) +}) + +test('does not modify an already patched server runtime', () => { + const firstResult = patchWorkshopAppServerRuntime(unpatchedServerRuntimeFixture) + const secondResult = patchWorkshopAppServerRuntime(firstResult.source) + + assert.equal(secondResult.patched, false) + assert.equal(secondResult.source, firstResult.source) +}) + test('patches the installed workshop-app build', async () => { const result = await patchWorkshopApp() assert.equal(typeof result.patched, 'boolean') - assert.match(result.source, /action: action\$splatNotFound/) - assert.match(result.source, /"routes\/\$": .*"hasAction": true/) + assert.match(result.serverBuild.source, /action: action\$splatNotFound/) + assert.match(result.serverBuild.source, /"routes\/\$": .*"hasAction": true/) + assert.match(result.serverRuntime.source, /\^\\\/\{2,\}\$/) })