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
22 changes: 20 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,23 @@ jobs:
echo "consuming ${tarball} on $(node -v), engine-strict on"
node scripts/compat-matrix.mjs --manager npm --self-contained --tarball "${tarball}"

tanstack-consumer:
name: TanStack Start entry and middleware types
needs: pack
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version-file: .node-version
- uses: actions/download-artifact@v8
with:
name: packed-tarball
path: packed
- name: Install and check the real TanStack framework contract
run: node scripts/tanstack-consumer.mjs --tarball packed/*.tgz

next-consumers:
name: Next.js ${{ matrix.next }} (${{ matrix.bundler }}) production integration
needs: pack
Expand Down Expand Up @@ -430,6 +447,7 @@ jobs:
- validate
- production-audit
- next-consumers
- tanstack-consumer
runs-on: ubuntu-latest

steps:
Expand All @@ -447,8 +465,8 @@ jobs:
# otherwise leave a green required check that verifies nothing at all — the one failure mode a
# gate must not have, since it is indistinguishable from a working one.
count=$(printf '%s' "$RESULTS" | python3 -c 'import json, sys; print(len(json.load(sys.stdin)))')
if [ "$count" -lt 9 ]; then
echo "::error::This gate is standing on ${count} job(s); it is meant to require 9. A required check that verifies nothing passes exactly when something is broken."
if [ "$count" -lt 10 ]; then
echo "::error::This gate is standing on ${count} job(s); it is meant to require 10. A required check that verifies nothing passes exactly when something is broken."
exit 1
fi

Expand Down
25 changes: 20 additions & 5 deletions AGENT-INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Only `map` produces an attack-surface analysis, and only `map --upload` sends th
- The **widget tag** goes in the root HTML shell — the first of `index.html`, `public/index.html`, or `src/app.html` that exists — and only after a successful post, because it carries the site UUID.
- The **production marker** goes in a root shell that is JSX rather than HTML (e.g. `src/routes/__root.tsx`, `app/layout.tsx`), inside a `{/* #region patchstack */}` block placed above the widget tag. It is written *before* the post: it carries no site UUID and needs no network, and build scripts commonly chain `patchstack-connect scan || true`, where waiting on the server would mean an offline build silently ships without the flag. The marker is guarded by the framework's own production expression (`import.meta.env.PROD`, or `process.env.NODE_ENV === 'production'`), so it is inert in dev and preview builds. Without it a server-rendered site has no built HTML for `mark-build` to stamp, and the widget treats the published site as build mode. `mark-build` writes to build output only (`dist/`, `build/`, `out/`, `.output/public`), never to source. `guide`, `status`, and `init` write nothing except `init`'s own `.patchstackrc.json`.
- **`setup` runs `scan`, then `protect`, then edits `package.json` scripts:** provisioning happens first so the runtime guard can bake the real site UUID. It verifies the resulting framework seam, preserves existing commands, adds `scan` after dependency installs and before builds, adds `mark-build` after builds, and uses a direct build chain for Bun. It never runs the project build. If the widget or runtime guard needs a framework-specific manual merge, it prints the exact remaining step instead of overwriting user code.
- The package also exposes **`protect`** directly (runtime exploit guard; its templates live under `dist/protect/`). `setup` invokes it automatically; `scan`, `guide`, `status`, and `mark-build` do not. It writes only local files and auto-wires known stacks — **TanStack Start + Supabase** (patches the Supabase client + `src/start.ts`), **Next.js** (scaffolds or composes middleware and adds request/response checks to supported App Router handlers), **SvelteKit** (`src/hooks.server.ts`), **Astro** (`src/middleware.ts`), **Nuxt** (`server/middleware/`), **NestJS** (`app.use(patchstackMiddleware)` in the bootstrap), **Fastify** (`app.register(patchstackFastify)`), and **Express** (`app.use(patchstackMiddleware)`). On **any other stack** it scaffolds a framework-agnostic guard under `src/patchstack/` and prints a wiring plan — then you finish the install by importing that guard into your server entry (`protectFetch(handler)` for a Web-Fetch server, or `app.use(patchstackMiddleware)` for Node/Express) and running `patchstack-connect protect --check` to confirm it is wired (exit 1 until it is). Passing `--demo` seeds a broad sample rule set (for demonstrations, not production).
- The package also exposes **`protect`** directly (runtime exploit guard; its templates live under `dist/protect/`). `setup` invokes it automatically; `scan`, `guide`, `status`, and `mark-build` do not. It writes only local files and auto-wires known stacks — **TanStack Start + Supabase** (patches the Supabase client + `src/start.ts`), **TanStack Start** (documented server Fetch entry without requiring Supabase), **Next.js** (scaffolds or composes middleware/proxy and adds request/response checks to supported App Router handlers), **SvelteKit** (`src/hooks.server.ts`), **Astro** (`src/middleware.ts`), **Nuxt** (`server/middleware/`), **NestJS** (`app.use(patchstackMiddleware)` in the bootstrap), **Fastify** (`app.register(patchstackFastify)`), and **Express** (`app.use(patchstackMiddleware)`). On **any other stack** it scaffolds a framework-agnostic guard under `src/patchstack/` and prints a wiring plan — then you finish the install by importing that guard into your server entry (`protectFetch(handler)` for a Web-Fetch server, or `app.use(patchstackMiddleware)` for Node/Express) and running `patchstack-connect protect --check` to confirm it is wired (exit 1 until it is). Passing `--demo` seeds a broad sample rule set (for demonstrations, not production).
- **`demo node-serialize` is an explicit production-backed walkthrough.** It requires `node-serialize@0.0.4` to already be present in the lockfile; it does not install the vulnerable dependency. It runs the same production `scan`, polls the configured site's public Pulse rules endpoint until rule `18843` is served, runs `protect`, verifies the generated guard, and prints exploit/benign test requests. It writes the same manifest/widget and guard files as those underlying commands. It does not start/restart the app and does not send the printed requests.
- **`map` is local unless you pass `--upload`.** It walks the project's server source (skipping `node_modules`, build output and dot-directories; it does not follow symlinks out of the project unless you pass `--follow-symlinks`), parses it with the project's **own** `typescript`, and prints JSON describing the attack surface: entry points, the inputs each reads, the sinks they can reach (database / file system / process / outbound HTTP) with the npm package behind each, and evidence-backed input→sink flows, each labelled with how the link was established — from an exact read at the sink's own call site, through a transformed or cross-module link, down to the two being present together with no proven link. Static analysis is best-effort, so the output reports the *detected* surface with coverage counters — not a completeness guarantee. Without `--upload` it writes nothing except the file named by `--out`, and it is never invoked by `scan`, `setup`, `guide`, `protect`, or `mark-build`.
- **`map --upload` is the only command that sends a description of your source.** (The runtime guard can also report rule detections, which carry route paths and parameter names — see "Runtime guard reporting" below.) It POSTs the same JSON document to `monitor/pulse/input-map/<your site uuid>` so Patchstack can pin protection rules to your app's own parameter names instead of guessing them. During a pre-bundle build hook it hashes the policy-relevant document (all fields except analyser timing and memory observations), writes the SHA-256 value as `_patchstack.build_id` in the existing rules file imported by the scaffolded guard, and sends the same value as `build_id`. Outside that lifecycle it sends no identity and changes no file, so any generated scoped rule remains detect-only. **No source code, no file contents, no environment variable values.** A map with no recognised entry points is still uploaded because its import inventory and coverage limits are evidence; a failure to reach Patchstack is reported and ignored rather than failing your build. Omit the flag and the command stays entirely local.
Expand Down Expand Up @@ -336,14 +336,29 @@ It is server-only. Never put it in the widget tag, client bundles, or public env
check requests and filter returned responses. It writes a shared server-only `patchstack.next`
helper alongside `patchstack.rules.json`. Unsupported exports, complex matchers or handlers are
left unchanged and reported by `--check`; re-run `protect` after adding routes. An existing
`proxy.ts`/`proxy.js` requires manual integration: no competing middleware is scaffolded and
`--check` reports the gap. Middleware alone
`proxy.ts`/`proxy.js` is composed on Next 16+ using the same conservative export/matcher rules;
a new Next 16+ install uses `proxy.ts`. Conflicting entries stay untouched. Middleware/proxy alone
cannot filter downstream page bodies. Rendered pages, Server Actions and Pages API response
filtering are not verified by this adapter. Keep Next.js patched: a framework middleware bypass
also bypasses a guard in middleware. Edge middleware needs `PATCHSTACK_API_KEY` in the server
environment; it cannot read `.patchstackrc.local.json`. Do not put credentials in public variables
or commit them. Source checks do not verify rule delivery or blocking in the running deployment.

**TanStack Start:** the documented `src/server.ts` Fetch entry can be scaffolded without Supabase
or `src/start.ts`. Supported literal `createServerEntry({ fetch: ... })` configurations are wrapped
without changing host arguments or application error handling. The installed framework must expose
`server-entry`; custom entry paths, spreads, getters and competing files need manual integration.
The existing TanStack/Supabase adapter screens native requests by default and filters the response
inside TanStack's middleware result. No `PATCHSTACK_ROUTE_WAF` switch is needed. Browser-direct
services and separately deployed functions are not protected by guarding the frontend server;
keep backend authorization/RLS and install a guard at each independently exposed backend.

Generated guards refresh live rules every five minutes (15 seconds in an explicit sandbox).
Express/Node guards enable bounded response filtering; Fastify filters buffered `onSend` output,
leaving streams and bodyless replies untouched. Unmodified recognized helpers can be upgraded;
customized helpers are preserved and flagged for manual review. Wiring checks inspect executable
statements, not just marker comments, and cannot establish live delivery or complete coverage.

`--check` reads the app's source. It can establish that the guard is imported and called on a request
path; it cannot establish that a request ever reaches it — an app can wire the guard onto one server
and serve traffic from another, and that passes. To settle the difference there is an opt-in check
Expand Down Expand Up @@ -449,13 +464,13 @@ which integration API is available.
| [Solid](https://docs.solidjs.com/quick-start) | Separate the UI library from SolidStart or a custom server; keep protection out of client components. |
| [Qwik](https://qwik.dev/docs/qwikcity/) | Inspect Qwik City and the deployment adapter; component resumability does not identify the request entry. |
| [Ember](https://guides.emberjs.com/release/getting-started/quick-start/) | Inspect the deployed backend or SSR host separately; browser routes and the development server are not production coverage. |
| [Next.js](https://nextjs.org/docs/app/api-reference/file-conventions/proxy) | Inspect root or `src/` middleware/proxy, matchers, APIs and Server Actions. Next 16 renamed middleware to proxy; Connect scaffolds `middleware.ts`. Do not leave competing files or assume its source check validates `proxy.ts`. |
| [Next.js](https://nextjs.org/docs/app/api-reference/file-conventions/proxy) | Inspect root or `src/` middleware/proxy, matchers, APIs and Server Actions. Connect uses `proxy.ts` for new Next 16+ installs and composes supported existing proxies. Conflicting entries and complex routing require manual review. |
| [Nuxt](https://nuxt.com/docs/4.x/directory-structure/server) | Inspect the configured server directory and Nitro server middleware, not client navigation middleware. Distinguish a server deployment from generated static output. |
| [SvelteKit](https://svelte.dev/docs/kit/hooks) | Compose the existing server `handle` hook; check endpoints, actions, prerendering and the deployed adapter. |
| [Astro](https://docs.astro.build/en/guides/middleware/) | Compose `onRequest` in server middleware; distinguish execution during prerendering from on-demand routes behind an adapter. |
| [Remix](https://v2.remix.run/docs/discussion/runtimes/) | Inspect the adapter around `createRequestHandler`; cover document requests, loaders, actions and resource routes, not only `entry.server` rendering. |
| [React Router](https://reactrouter.com/how-to/middleware) | Determine library versus framework/SSR mode. Inspect the server adapter and version-specific server middleware; client middleware cannot guard loaders/actions on the server. |
| [TanStack Start](https://tanstack.com/start/latest/docs/framework/react/guide/middleware) | Inspect the server entry and global request middleware, including server functions. The automatic TanStack/Supabase adapter matches a particular project layout, not every Start app. |
| [TanStack Start](https://tanstack.com/start/latest/docs/framework/react/guide/middleware) | Inspect the server entry and global request middleware, including server functions. The native Fetch-entry adapter does not require Supabase. Custom entry paths need manual review; the separate TanStack/Supabase adapter matches a specific layout. |
| [SolidStart](https://docs.solidjs.com/solid-start/v1/advanced/middleware) | Inspect configured server middleware and adapter; verify API and server action paths separately rather than assuming rendering middleware covers them. |
| [Qwik City](https://qwik.dev/docs/middleware/) | Inspect deployment entry and request middleware, including endpoints, loaders and actions. Confirm route/layout scope and static output. |
| [Gatsby](https://www.gatsbyjs.com/docs/reference/functions/) | Static pages need no request guard, but `src/api` functions and SSR deployments need their own server entry review. |
Expand Down
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ npm install --save @patchstack/connect && npx @patchstack/connect setup

> **Use your project's own package manager.** On Bun-managed projects (including many Lovable projects) install with `bun add @patchstack/connect` instead — running `npm install` there plants a `package-lock.json` that the platform's native dependency flow never updates again, leaving a stale lockfile next to the live one. Connect detects and works around that (see *Stale lockfiles* below), but not creating the fossil is better. Protection imports `@patchstack/connect/protect` at runtime, so deployments that prune dev dependencies need the package in `dependencies`.

> **Hosted builders:** set `PATCHSTACK_ENVIRONMENT=sandbox` in the workspace process environment (or scope it to the setup command above), persist every file written by `setup`, and restart any already-running server so it loads the new middleware. Do not write `"environment": "sandbox"` to the committed `.patchstackrc.json`: the same project files reach production, where scans should inherit no override and default to `production`. TanStack Start + Supabase (the server shape emitted by Lovable) is auto-wired: browser Supabase traffic is tunneled through a same-origin guard, server-function arguments are inspected, and responses are screened. A client-only SPA has no server request path to protect; setup will leave a generic scaffold and `protect --check` will remain red until the host adds a server/edge seam. Set `PATCHSTACK_ROUTE_WAF=1` when the deployment should additionally screen every TanStack route request.
> **Hosted builders:** set `PATCHSTACK_ENVIRONMENT=sandbox` in the workspace process environment (or scope it to the setup command above), persist every file written by `setup`, and restart any already-running server so it loads the new middleware. Do not write `"environment": "sandbox"` to the committed `.patchstackrc.json`: the same project files reach production, where scans should inherit no override and default to `production`. TanStack Start + Supabase (the server shape emitted by Lovable) is auto-wired: browser Supabase traffic is tunneled through a same-origin guard, server-function arguments are inspected, and responses are screened. A client-only SPA has no server request path to protect; setup will leave a generic scaffold and `protect --check` will remain red until the host adds a server/edge seam. Native TanStack requests are screened by default; no additional route-WAF switch is needed. New Start apps without that Supabase layout use the documented server Fetch entry. Browser-direct services and separately deployed functions still require their own protection.

That's it. `setup`:

Expand Down Expand Up @@ -230,16 +230,26 @@ Options (for demo and demo-guide):

### Next.js request and response protection

Framework detection follows the exported application's server entry, not the builder's name.
[Lovable documents TanStack Start for new projects and React/Vite for older ones](https://docs.lovable.dev/introduction/faq);
[Hostinger Horizons offers a hosted backend](https://www.hostinger.com/blog/horizons-integrated-backend/),
and [Airo exports React/TypeScript applications](https://airo-builder.godaddy.com/discover/features).
Those frontends do not establish where backend requests execute. Browser-direct APIs, Supabase Edge
Functions and other separately deployed services need their own server-side integration; a browser
tunnel does not replace backend authorization or RLS. Tests use synthetic framework-shaped apps,
not proprietary builder templates, and do not certify a builder's live hosting environment.

`protect` composes straightforward existing middleware instead of replacing its authentication or
redirect logic. The request guard gets a catch-all matcher; the application's middleware still runs
only within its original scope. Automatic composition accepts directly exported handlers and literal
path matchers (including a terminal `/:path*`). Complex matchers, re-exports and custom URL routing
are left untouched with an integration message. Source-aware edits use the application's installed
`typescript` parser; configuration files are never executed.
are left untouched with an integration message. Source-aware edits use a TypeScript parser supplied
by the app or Connect's CLI dependency; configuration files are never executed.

Apps with an existing `proxy.ts`/`proxy.js` (including under `src/`) require manual integration.
The installer leaves them unchanged and `protect --check` reports the gap. It never adds middleware
alongside a proxy, since Next.js does not allow both.
On Next 16+, the installer creates `proxy.ts` for new wiring and composes supported existing
`proxy.ts`/`proxy.js` handlers (including under `src/`). Older versions retain middleware. Conflicting
entries, unsupported proxy exports, and custom normalization stay untouched and are reported by
`protect --check`. Middleware and proxy are never created alongside one another.

For App Router `app/**/route.ts` or `route.js` files, it also adds request checks and screens each
returned response. The shared server-only `patchstack.next` helper initializes one policy per module
Expand Down
Loading
Loading