From 08d5644074140a02f37a34ab1de3f7735707e054 Mon Sep 17 00:00:00 2001 From: Dave Jong Date: Wed, 15 Jul 2026 18:16:34 +0200 Subject: [PATCH 1/3] test(protect): typecheck scaffolded templates against the real API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Templates are excluded from connect's tsconfig (they import framework + JSON files that only exist after scaffolding), so a template type bug ships silently — a broken install has slipped out this way before. scripts/typecheck-templates.mjs assembles each template in a scratch dir with the real @patchstack/connect/protect declarations, a stub rules JSON, and loose framework-type shims, then runs `tsc --noEmit`. Chained into `npm run typecheck` so CI + prepublish cover it. Verified it fails on the exact bug class it targets (a string-typed `mode`) and passes on all 6 current templates. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 3 ++ package.json | 3 +- scripts/typecheck-templates.mjs | 90 +++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 scripts/typecheck-templates.mjs diff --git a/.gitignore b/.gitignore index c53e637..a5f4cfa 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ field-test/results/ # test-build throwaway fixture (recreated on every run) test-build/.work/ + +# template typecheck scratch dir +.template-typecheck/ diff --git a/package.json b/package.json index bceaaab..74687ca 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "test": "vitest run", "test:manifest": "bun scripts/test-manifest.ts", "test:watch": "vitest", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit && node scripts/typecheck-templates.mjs", + "typecheck:templates": "node scripts/typecheck-templates.mjs", "prepare": "npm run build", "prepublishOnly": "npm run typecheck && npm test && npm run build" }, diff --git a/scripts/typecheck-templates.mjs b/scripts/typecheck-templates.mjs new file mode 100644 index 0000000..3a5d553 --- /dev/null +++ b/scripts/typecheck-templates.mjs @@ -0,0 +1,90 @@ +// Typecheck the scaffolded `src/protect/templates/*.ts` the way a target app's `tsc` would. +// +// WHY: templates are deliberately excluded from connect's own tsconfig (they import framework + +// JSON files that only exist AFTER scaffolding), so a template type bug ships silently — a broken +// install has slipped out this way before (a `string`-typed `mode`, a non-generic `screenResponse` +// return that a strict app's tsc then rejected). This assembles each template in a scratch dir with +// the pieces it expects — the REAL `@patchstack/connect/protect` declarations, a stub rules JSON, +// and loose shims for the framework type-only imports — then runs `tsc --noEmit`. It catches +// template-vs-protect-API drift (that class of bug). Framework types are shimmed, not installed, so +// it does NOT verify a handler matches its framework's exact hook signature — a possible follow-up. + +import { mkdirSync, rmSync, copyFileSync, writeFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { execFileSync } from 'node:child_process'; + +const ROOT = process.cwd(); +const TEMPLATES = join(ROOT, 'src/protect/templates'); +const OUT = join(ROOT, '.template-typecheck'); + +rmSync(OUT, { recursive: true, force: true }); +mkdirSync(OUT, { recursive: true }); + +// Copy every .ts template into the scratch dir. +const templates = readdirSync(TEMPLATES).filter((f) => f.endsWith('.ts')); +for (const f of templates) copyFileSync(join(TEMPLATES, f), join(OUT, f)); + +// The JSON the templates import: the real starter rules (register-into-app templates import +// ./rules.json) + a stub for the seam templates' co-located ./patchstack.rules.json. +copyFileSync(join(TEMPLATES, 'rules.json'), join(OUT, 'rules.json')); +writeFileSync(join(OUT, 'patchstack.rules.json'), '[]\n'); + +// The real hand-authored declarations for @patchstack/connect/protect — this is what we check against. +copyFileSync(join(ROOT, 'src/protect/protect.d.ts'), join(OUT, 'protect-types.d.ts')); + +// Loose shims for the framework type-only imports (we don't install the frameworks). Signatures +// mirror the real hooks closely enough to catch obvious wiring mistakes without full fidelity. +writeFileSync( + join(OUT, 'shims.d.ts'), + `declare module "astro" { + export type MiddlewareHandler = ( + context: { request: Request }, + next: () => Promise, + ) => Promise | Response; +} +declare module "@sveltejs/kit" { + export type Handle = (input: { + event: { request: Request }; + resolve: (event: unknown) => Promise; + }) => Promise | Response; +} +`, +); + +writeFileSync( + join(OUT, 'tsconfig.json'), + JSON.stringify( + { + compilerOptions: { + target: 'ES2022', + module: 'ESNext', + moduleResolution: 'Bundler', + lib: ['ES2022', 'DOM'], + types: ['node'], + strict: true, + noUncheckedIndexedAccess: true, + esModuleInterop: true, + skipLibCheck: true, + resolveJsonModule: true, + isolatedModules: true, + noEmit: true, + baseUrl: '.', + paths: { '@patchstack/connect/protect': ['./protect-types'] }, + }, + include: ['*.ts'], + }, + null, + 2, + ), +); + +const tsc = join(ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'tsc.cmd' : 'tsc'); +try { + execFileSync(tsc, ['--noEmit', '-p', join(OUT, 'tsconfig.json')], { stdio: 'inherit' }); + console.log(`template typecheck: OK (${templates.length} templates)`); + rmSync(OUT, { recursive: true, force: true }); +} catch { + console.error(`\ntemplate typecheck FAILED — a scaffolded template would not compile in a user's app.`); + console.error(`(scratch dir left at ${OUT} for inspection)`); + process.exit(1); +} From ca2692821982f27e0c64c3aef0bfab884ec62b55 Mon Sep 17 00:00:00 2001 From: Dave Jong Date: Wed, 15 Jul 2026 18:27:34 +0200 Subject: [PATCH 2/3] feat(protect): NestJS adapter + shared register-into-app helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NestJS on the default Express platform accepts `app.use(...)` middleware in the bootstrap, so the guard wires the same way Express/Fastify do — no Nest module/decorators needed. Anchored on `NestFactory.create(...)`, reusing the framework-agnostic guard. Extracted the register-into-app pattern (three adapters now share it) into install/register.ts (wireRegister/verifyRegister + RegisterSpec) and the app-instance finder into install/find-app.ts; Express + Fastify are now thin specs on top. hasDependency() moved to util.ts. No behavior change for the existing adapters (their tests are unchanged and green). Registered most-specific-first (… → astro → nestjs → fastify → express → generic). AGENT-INSTALL lists NestJS. +2 tests, 458 total, typecheck (incl. template check) + build clean. Co-Authored-By: Claude Opus 4.8 --- AGENT-INSTALL.md | 2 +- src/protect/install/adapters/express.ts | 139 +++--------------------- src/protect/install/adapters/fastify.ts | 137 +++-------------------- src/protect/install/adapters/nestjs.ts | 25 +++++ src/protect/install/find-app.ts | 51 +++++++++ src/protect/install/index.ts | 2 + src/protect/install/register.ts | 66 +++++++++++ src/protect/install/util.ts | 10 ++ tests/protect/adapters.test.ts | 41 +++++++ 9 files changed, 228 insertions(+), 245 deletions(-) create mode 100644 src/protect/install/adapters/nestjs.ts create mode 100644 src/protect/install/find-app.ts create mode 100644 src/protect/install/register.ts diff --git a/AGENT-INSTALL.md b/AGENT-INSTALL.md index 3a59db9..c48d595 100644 --- a/AGENT-INSTALL.md +++ b/AGENT-INSTALL.md @@ -8,7 +8,7 @@ This versioned reference ships inside `@patchstack/connect` and documents each s - It reads the project's **dependency list only** — from the lockfile (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) or, on bun projects (`bun.lock`/`bun.lockb`), by enumerating the installed packages under `node_modules/` — and sends package names + versions to Patchstack for vulnerability matching. No source code, no env var values, no file paths, no git history. (`mark-build` additionally stamps built HTML with a coarse stack descriptor that may include hosting-related env variable *names* — e.g. `VERCEL`, `CF_PAGES` — never their values.) - **`scan` makes one source edit, and only after a successful post:** it adds (or updates) the disclosure widget's `