Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/protect/install/adapters/express.ts
Original file line number Diff line number Diff line change
@@ -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\(\)/,
Expand All @@ -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 = {
Expand Down
25 changes: 25 additions & 0 deletions tests/protect/adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading