From 3789c29d9427c5c548796313ccd4b5766c555a13 Mon Sep 17 00:00:00 2001 From: Dave Jong Date: Tue, 14 Jul 2026 18:21:28 +0200 Subject: [PATCH] fix(protect): type-safe scaffolded guard.ts (strict TanStack build) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `patchstack-connect protect` install failed `tsc` in the target app with two errors in the scaffolded guard: - `mode` inferred as `string`, not `"block" | "dry-run"` → createProtection arg rejected. Pin the annotation: `const mode: "block" | "dry-run" = …`. - `screenResponse(response: unknown): Promise` made the request middleware's server fn return `Promise`, which TanStack's RequestMiddlewareServerFnResult rejects. Make it a generic passthrough `screenResponse(response: T): Promise` so `screenResponse(await next())` preserves next()'s return type. Both reproduced + fixed under `tsc --strict` in isolation. Added a scaffolder regression test asserting the guard ships these type-safe signatures. Co-Authored-By: Claude Opus 4.8 --- src/protect/templates/guard.ts | 6 +++--- tests/protect/install.test.ts | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/protect/templates/guard.ts b/src/protect/templates/guard.ts index 2e8b32a..0d2a770 100644 --- a/src/protect/templates/guard.ts +++ b/src/protect/templates/guard.ts @@ -30,7 +30,7 @@ let _protection: Awaited> | undefined; async function getProtection() { if (!_protection) { // Always-on: block by default. An explicit PATCHSTACK_MODE=dry-run downgrades to log-only. - const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block"; + const mode: "block" | "dry-run" = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block"; const token = process.env.PATCHSTACK_WAF_TOKEN; const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID; // Egress SSRF screening: block the app's outbound calls to internal / metadata addresses, @@ -80,11 +80,11 @@ export async function inspectServerFn(data: unknown): Promise<{ rule?: string; m // src/start.ts (the browser→Supabase tunnel screens its own forwarded response). Only acts on a // web Response (text/JSON/HTML) — anything else, or any error, passes through untouched (fail-open, // never breaks a response). -export async function screenResponse(response: unknown): Promise { +export async function screenResponse(response: T): Promise { try { if (!(response instanceof Response)) return response; const protection = await getProtection(); - return protection.screenResponse ? await protection.screenResponse(response) : response; + return (protection.screenResponse ? await protection.screenResponse(response) : response) as T; } catch { return response; // fail open } diff --git a/tests/protect/install.test.ts b/tests/protect/install.test.ts index 2d44015..196d554 100644 --- a/tests/protect/install.test.ts +++ b/tests/protect/install.test.ts @@ -77,6 +77,15 @@ describe('runProtect scaffolder', () => { expect(start).toContain('functionMiddleware: [patchstackFunctionGuard, attachSupabaseAuth]'); }); + it('scaffolds a type-safe guard.ts (compiles under a strict TanStack build)', () => { + runProtect(dir); + const guard = read(dir, 'src/integrations/patchstack/guard.ts'); + // Regression: a strict `tsc` build in the target app rejected a `string` mode and a + // `Promise` from screenResponse (the middleware return type). Keep both type-safe. + expect(guard).toContain('const mode: "block" | "dry-run" ='); + expect(guard).toContain('export async function screenResponse(response: T): Promise'); + }); + it('is idempotent — re-running does not duplicate wiring', () => { runProtect(dir); runProtect(dir);