@patchstack/connect 0.3.27 → 0.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT-INSTALL.md +65 -2
- package/dist/cli.js +28 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +17 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +229 -59
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +17 -0
- package/dist/protect.edge.js +211 -59
- package/dist/protect.edge.js.map +4 -4
- package/dist/protect.js +212 -60
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-KZSQD2X3.js → refresh-manifest-VRBE6RH6.js} +18 -1
- package/dist/refresh-manifest-VRBE6RH6.js.map +1 -0
- package/package.json +1 -1
- package/dist/refresh-manifest-KZSQD2X3.js.map +0 -1
package/AGENT-INSTALL.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
This versioned reference ships inside `@patchstack/connect` and documents each setup command and its project changes.
|
|
4
4
|
|
|
5
|
+
## Command reference
|
|
6
|
+
|
|
7
|
+
Every command at a glance — what it does, whether it reads your source, what it writes, and what leaves your machine. Full behavior, flags, and edge cases follow in the sections below.
|
|
8
|
+
|
|
9
|
+
| Command | What it does | Reads your source? | Writes to your project | Sends over the network |
|
|
10
|
+
|---|---|---|---|---|
|
|
11
|
+
| `scan` | Provision (or reuse) the site and POST the dependency list for vulnerability matching. Also runs automatically via `setup` and the install/build hooks. | No — lockfile only (bun: enumerates `node_modules/`) | `.patchstackrc.json`; the widget `<script>` tag in the root HTML shell — only after a successful post | Package names + versions |
|
|
12
|
+
| `setup` | One bounded command: `scan` → manage the widget → install + verify `protect` → wire the install/build scans. Never runs the project build. | No | Config, widget tag, guard files, `package.json` scripts | Package names + versions (via `scan`) |
|
|
13
|
+
| `map` | Local, read-only attack-surface analysis (entry points → inputs → sinks → evidence-backed flows). Never run by another command. | **Yes** — via the app's own TypeScript | Nothing (only the file named by `--out`) | Nothing — **unless `--upload`**: structure only (routes, parameter names, the package behind each sink, file:line). Never source code or env values |
|
|
14
|
+
| `protect` | Install the always-on runtime guard; auto-wire known stacks, or scaffold a generic guard + print a wiring plan. `--check` verifies the guard is wired (exit 1 if not); `--demo` seeds a broad sample rule set. Runs automatically **only** via `setup` — never by `scan`, `guide`, `status`, or `mark-build`. | No — writes guard files, does not analyze your code | Guard/framework files (e.g. `middleware.ts`, `src/patchstack/`) | Nothing |
|
|
15
|
+
| `demo node-serialize` | Production-backed walkthrough: confirm the vulnerable package is present, scan, wait for live rule `18843`, install + verify the guard, print test requests. Does not install the package or start/restart the app. | No | Same files as `scan` + `protect` | `scan` payload; polls the public Pulse rules endpoint (never the printed test requests) |
|
|
16
|
+
| `demo-guide node-serialize` | Read-only companion: explains the prepare/run/prove/cleanup sequence and prints the next command. | No | Nothing | Nothing |
|
|
17
|
+
| `guide` | Print this project's live setup status (done/missing, with tailored commands), then the full guide. `--full` prints it even when setup is complete. | No | Nothing | Nothing |
|
|
18
|
+
| `status` | Re-print the site UUID + dashboard URL and check whether the site still exists (active / removed / could not verify). | No | Nothing | Site-existence check |
|
|
19
|
+
| `init <site-uuid>` | Optional: pre-seed `.patchstackrc.json` with an existing UUID. | No | `.patchstackrc.json` only | Nothing |
|
|
20
|
+
| `mark-build` | Stamp built HTML with a production flag + build fingerprint and ensure the widget tag in built pages. Run as a `postbuild` step. | No | Build output only (`dist/ build/ out/ .output/public`) — never source | Nothing |
|
|
21
|
+
| `login` | Recover a lost credential for an existing site: print an owner-approval link and poll (10 min). Approving **rotates** the credential. Not usable in CI. | No | New credential into `.patchstackrc.json` on approval | Device-code request + approval poll |
|
|
22
|
+
| `uninstall` | Signal Patchstack that the package is being removed: an unclaimed record is deleted, a claimed one is flagged. Does **not** touch local files. | No | Nothing local | Removal signal |
|
|
23
|
+
|
|
24
|
+
Only `map` reads your source, and only `map --upload` sends anything derived from it. `scan` transmits nothing but package names + versions — never source code, env var values, file paths, or git history.
|
|
25
|
+
|
|
5
26
|
## Package and command behavior
|
|
6
27
|
|
|
7
28
|
- Package: [`@patchstack/connect`](https://www.npmjs.com/package/@patchstack/connect), MIT-licensed, source at https://github.com/patchstack/connect. `npm view @patchstack/connect` shows the live registry metadata.
|
|
@@ -12,7 +33,7 @@ This versioned reference ships inside `@patchstack/connect` and documents each s
|
|
|
12
33
|
- 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 `middleware.ts`), **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).
|
|
13
34
|
- **`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.
|
|
14
35
|
- **`map` is a local, read-only analysis command.** 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. It writes nothing (except the file you name with `--out`) and is never invoked by `scan`, `setup`, `guide`, `protect`, or `mark-build`.
|
|
15
|
-
- **`map --upload` is the
|
|
36
|
+
- **`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. What is sent is exactly what `map` prints — a structural description: route paths, parameter/field names, the dependency behind each sink, and file paths with line numbers. **No source code, no file contents, no environment variable values.** It never runs without the flag, it is skipped when no entry points are detected, and a failure to reach Patchstack is reported and ignored rather than failing your build. Omit the flag and the command stays entirely local.
|
|
16
37
|
- **`demo-guide node-serialize` is the read-only companion.** It checks the Host-created site configuration and vulnerable lockfile entry, explains the complete local prepare/run/restart/prove/cleanup sequence, and prints the next exact command. It does not require a deployment and does not change files or contact Patchstack.
|
|
17
38
|
- Patchstack is not WordPress-only. This connector monitors any JS/Node project — Vite, Next.js, plain vanilla JS, anything with a lockfile.
|
|
18
39
|
|
|
@@ -78,7 +99,7 @@ This versioned reference ships inside `@patchstack/connect` and documents each s
|
|
|
78
99
|
|
|
79
100
|
Framework-specific placement patterns: https://cdn.patchstack.com/llm.html. The site UUID is public by design — it ships in client-side HTML and is not a secret. The credential is the opposite, and `scan` writes it for you — **there is no manual step, and you should never invent or ask the user for this value**:
|
|
80
101
|
|
|
81
|
-
- `apiKey` (also `PATCHSTACK_API_KEY`, WP format `{secret}-{oauth.id}`) — one credential for both paths. It authenticates **Pulse ingest** (manifest, attack-surface map, package removal), where it is exchanged for a short-lived token rather than sent directly, and **block-log reporting** through the connector `POST /api/logs/log`, so "Threats blocked" fills in the dashboard.
|
|
102
|
+
- `apiKey` (also `PATCHSTACK_API_KEY`, WP format `{secret}-{oauth.id}`) — one credential for both paths. It authenticates **Pulse ingest** (manifest, attack-surface map, package removal, rule detections), where it is exchanged for a short-lived token rather than sent directly, and **block-log reporting** through the connector `POST /api/logs/log`, so "Threats blocked" fills in the dashboard.
|
|
82
103
|
|
|
83
104
|
It is server-only. Never put it in the widget tag, client bundles, or public env vars (`NEXT_PUBLIC_*`, etc.). Prefer `PATCHSTACK_API_KEY` in production; `.patchstackrc.json` is fine for local DX. If it is lost, `npx @patchstack/connect login` recovers it via dashboard approval — do not delete the file and re-provision, which would create a second site. Opt out of reporting with `PATCHSTACK_TELEMETRY=off`. If the project must not carry the widget, persist `"widget": false` in `.patchstackrc.json`; otherwise the next scan re-adds it.
|
|
84
105
|
|
|
@@ -105,6 +126,48 @@ It is server-only. Never put it in the widget tag, client bundles, or public env
|
|
|
105
126
|
- If a step fails, stop and report it. Don't proceed with placeholders.
|
|
106
127
|
- In CI where `.patchstackrc.json` can't be committed, set `PATCHSTACK_SITE_UUID` and `PATCHSTACK_API_KEY` as env vars instead. Precedence: CLI flag → env var → `.patchstackrc.json`. `login` is interactive and refuses to run in CI, so CI always takes its credential from the environment.
|
|
107
128
|
|
|
129
|
+
## Runtime guard reporting
|
|
130
|
+
|
|
131
|
+
The runtime guard (`protect`) can report the rules that matched, so the dashboard can show what a rule
|
|
132
|
+
would have stopped while it is still in dry-run. Two separate paths, with different triggers:
|
|
133
|
+
|
|
134
|
+
- **Blocked requests** go to the connector `POST /api/logs/log`, the same path the WordPress plugin uses,
|
|
135
|
+
and fill in "Threats blocked". This runs when the guard is holding an `apiKey` and a rule blocked a
|
|
136
|
+
request. The credential is first exchanged at `POST /oauth/token` (client credentials) for a bearer
|
|
137
|
+
token; the `apiKey` itself is not sent to the log endpoint. Disable with `PATCHSTACK_TELEMETRY=off`, or
|
|
138
|
+
`reportFirewallLog: false` in `createProtection`.
|
|
139
|
+
- **Every rule that matched** goes to `monitor/pulse/detections/<your site uuid>` — including matches that
|
|
140
|
+
blocked, which are reported on both paths. This is **off unless you pass `reportDetections: true`** to
|
|
141
|
+
`createProtection`; the scaffolded guard does not pass it. It also requires a provisioned site UUID and
|
|
142
|
+
is disabled by `PATCHSTACK_TELEMETRY=off`. It exists because a rule carrying `dry-run` blocks nothing,
|
|
143
|
+
so without it nothing distinguishes a rule that is protecting from one that is quietly wrong.
|
|
144
|
+
|
|
145
|
+
What a detection report contains, per matched rule: the rule id, the request path **with any query string
|
|
146
|
+
removed**, the parameter names that rule reads (from the rule's own definition), which phase matched,
|
|
147
|
+
whether it was enforced, the identifier of the rule bundle in use, and a timestamp. Each batch also
|
|
148
|
+
carries a count of reports dropped when traffic outran the flush, so a partial sample is not read as a
|
|
149
|
+
complete one.
|
|
150
|
+
|
|
151
|
+
The parameter names are **identifiers, and they name the request region they refer to** — `post.title`,
|
|
152
|
+
`get.redirect_to`, `cookie.session`, `server.HTTP_AUTHORIZATION`. So a rule that inspects a cookie or an
|
|
153
|
+
`Authorization` header sends that cookie's or header's **name**. They are read from the rule's own
|
|
154
|
+
definition, not from your traffic, so they describe what is being screened rather than what any request
|
|
155
|
+
contained.
|
|
156
|
+
|
|
157
|
+
What it does not contain: **no values of any kind.** Not the value that matched, not the request body,
|
|
158
|
+
and not the value of any header, cookie or query-string parameter — including those of the parameters
|
|
159
|
+
named above. Reports are batched, capped in memory, and dropped rather than retried if Patchstack cannot
|
|
160
|
+
be reached — a reporting failure never delays or fails a request.
|
|
161
|
+
|
|
162
|
+
Two more endpoints the package can call, for completeness:
|
|
163
|
+
|
|
164
|
+
- `GET monitor/widget/settings/<your site uuid>` — how `status` tells "this site was deleted on
|
|
165
|
+
Patchstack" apart from "still active". It sends no credential and nothing about your project; the site
|
|
166
|
+
UUID in the path is the whole request.
|
|
167
|
+
- `GET api/get-rules/3` — the older rules path, used only when the guard is configured with a `token`
|
|
168
|
+
instead of a site UUID. The zero-configuration flow provisions a site UUID and uses
|
|
169
|
+
`monitor/pulse/rules/<uuid>` instead, so this is unreachable unless you pass `token` yourself.
|
|
170
|
+
|
|
108
171
|
## Verifying the install
|
|
109
172
|
|
|
110
173
|
- `npx @patchstack/connect status` re-prints the site UUID and dashboard URL, and checks whether the site still exists on Patchstack (`Site status: active / removed / could not be verified`).
|
package/dist/cli.js
CHANGED
|
@@ -1016,6 +1016,19 @@ function buildEndpointUrl(base, siteUuid) {
|
|
|
1016
1016
|
const trimmed = base.replace(/\/$/, "");
|
|
1017
1017
|
return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
|
|
1018
1018
|
}
|
|
1019
|
+
function authFailureMessage(status, config) {
|
|
1020
|
+
const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
|
|
1021
|
+
if (status === 401 && !hasCredential) {
|
|
1022
|
+
return "Patchstack requires an API credential for this site and none is configured. Run `npx patchstack-connect login`, or set PATCHSTACK_API_KEY.";
|
|
1023
|
+
}
|
|
1024
|
+
if (status === 401) {
|
|
1025
|
+
return "Patchstack rejected this API credential. It may have expired, been revoked, or the site may no longer exist. Run `npx patchstack-connect login` to issue a new one.";
|
|
1026
|
+
}
|
|
1027
|
+
if (status === 403) {
|
|
1028
|
+
return "This API credential is not permitted to act on this site. Check that siteUuid in .patchstackrc.json matches the credential (a credential is issued for one site).";
|
|
1029
|
+
}
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1019
1032
|
function buildRulesUrl(manifestEndpoint, siteUuid) {
|
|
1020
1033
|
const url = new URL(manifestEndpoint);
|
|
1021
1034
|
const path12 = url.pathname.replace(/\/$/, "");
|
|
@@ -1070,6 +1083,10 @@ async function postInputMap(config, map) {
|
|
|
1070
1083
|
if (response.status === 422) {
|
|
1071
1084
|
return { result: "failed", message: `Patchstack does not accept this map schema (version ${map.version}). Update @patchstack/connect.` };
|
|
1072
1085
|
}
|
|
1086
|
+
const refused = authFailureMessage(response.status, config);
|
|
1087
|
+
if (refused !== null) {
|
|
1088
|
+
return { result: "failed", message: refused };
|
|
1089
|
+
}
|
|
1073
1090
|
if (!response.ok) {
|
|
1074
1091
|
return { result: "failed", message: `Patchstack returned ${response.status}.` };
|
|
1075
1092
|
}
|
|
@@ -1100,6 +1117,13 @@ async function postPackageRemoved(config) {
|
|
|
1100
1117
|
if (response.status === 404) {
|
|
1101
1118
|
return { result: "gone", message: null };
|
|
1102
1119
|
}
|
|
1120
|
+
if (response.status === 401 && await fetchSiteStatus(config) === "removed") {
|
|
1121
|
+
return { result: "gone", message: null };
|
|
1122
|
+
}
|
|
1123
|
+
const refused = authFailureMessage(response.status, config);
|
|
1124
|
+
if (refused !== null) {
|
|
1125
|
+
return { result: "failed", message: refused };
|
|
1126
|
+
}
|
|
1103
1127
|
if (!response.ok) {
|
|
1104
1128
|
return { result: "failed", message: `Patchstack returned ${response.status}.` };
|
|
1105
1129
|
}
|
|
@@ -1181,6 +1205,10 @@ async function postManifest(config, payload) {
|
|
|
1181
1205
|
"VALIDATION_ERROR"
|
|
1182
1206
|
);
|
|
1183
1207
|
}
|
|
1208
|
+
const refused = authFailureMessage(response.status, config);
|
|
1209
|
+
if (refused !== null) {
|
|
1210
|
+
throw new PatchstackError(refused, "UNAUTHORIZED");
|
|
1211
|
+
}
|
|
1184
1212
|
if (response.status < 200 || response.status >= 300) {
|
|
1185
1213
|
throw new PatchstackError(
|
|
1186
1214
|
`Patchstack returned ${response.status}: ${text.slice(0, 200)}`,
|