diff --git a/src/protect/install/adapters/express.ts b/src/protect/install/adapters/express.ts index 76dca5f..3e4314e 100644 --- a/src/protect/install/adapters/express.ts +++ b/src/protect/install/adapters/express.ts @@ -1,11 +1,16 @@ // Adapter: Express (Node). Scaffolds a guard that matches the entry file's module format, then -// registers parsed-body middleware after express.json() and before the application's routes. +// registers the WAF middleware after the app's body parser and before its routes — the guard reads +// the express-parsed req.body, so it must run once the body is populated. import { hasDependency } from '../util.js'; import { findAppInstance } from '../find-app.js'; import { wireRegister, verifyRegister, type RegisterSpec } from '../register.js'; import type { Adapter } from '../types.js'; -const jsonParserRe = (appVar: string) => new RegExp(`^\\s*${appVar}\\.use\\(\\s*express\\.json\\(`, 'm'); +// A body parser that populates req.body, in any of the shapes AI builders emit: express.json() / +// express.urlencoded(), body-parser's bodyParser.json() / .urlencoded(), an aliased parser +// (any `x.json(` / `x.urlencoded(`), or a destructured `json(` / `urlencoded(`. +const bodyParserRe = (appVar: string) => + new RegExp(`^\\s*${appVar}\\.use\\(\\s*(?:[A-Za-z_$][\\w$]*\\.)?(?:json|urlencoded)\\(`, 'm'); const SPEC: RegisterSpec = { appRe: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*express\(\)/, @@ -14,10 +19,10 @@ const SPEC: RegisterSpec = { guardTemplateCjs: 'express-guard.cjs', importName: 'patchstackMiddleware', call: (v) => `${v}.use(patchstackMiddleware);`, - callAfter: jsonParserRe, + callAfter: bodyParserRe, requireCallAfter: true, label: 'Express app', - manualHint: 'add `app.use(patchstackMiddleware)` after your JSON body parser and before the routes', + manualHint: 'add `app.use(patchstackMiddleware)` after your body parser (express.json/urlencoded or body-parser) and before the routes', }; export const expressAdapter: Adapter = { diff --git a/tests/protect/adapters.test.ts b/tests/protect/adapters.test.ts index 445d527..8c0b840 100644 --- a/tests/protect/adapters.test.ts +++ b/tests/protect/adapters.test.ts @@ -103,6 +103,31 @@ describe('Express adapter', () => { } }); + const parserVariants: Array<{ label: string; imports: string; parser: string }> = [ + { label: 'express.urlencoded()', imports: "import express from 'express';", parser: 'app.use(express.urlencoded({ extended: true }));' }, + { label: 'body-parser', imports: "import express from 'express';\nimport bodyParser from 'body-parser';", parser: 'app.use(bodyParser.json());' }, + { label: 'a destructured json()', imports: "import express, { json } from 'express';", parser: 'app.use(json());' }, + ]; + for (const { label, imports, parser } of parserVariants) { + it(`wires after ${label} (not just express.json)`, () => { + const dir = tmp('ps-express-parser-'); + writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ type: 'module', dependencies: { express: '^4.21.2' } })); + writeFileSync( + path.join(dir, 'server.js'), + `${imports}\nconst app = express();\n${parser}\napp.get('/', (req, res) => res.end());\n`, + ); + try { + runProtect(dir); + const server = read(dir, 'server.js'); + expect(server).toContain('app.use(patchstackMiddleware);'); + expect(server.indexOf('app.use(patchstackMiddleware)')).toBeLessThan(server.indexOf("app.get('/'")); + expect(runVerify(dir).wired).toBe(true); // verify requires the guard to sit after the parser + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + } + it('scaffolds but does not claim full wiring when no JSON body parser is present', () => { const dir = tmp('ps-express-no-parser-'); writeFileSync(