@patchstack/connect 0.5.30 → 0.5.32
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 +42 -7
- package/dist/cli.js +16 -5
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/AGENT-INSTALL.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
This versioned reference ships inside `@patchstack/connect` and documents each setup command and its project changes.
|
|
4
4
|
|
|
5
|
+
## What this package is
|
|
6
|
+
|
|
7
|
+
- **An npm package for JavaScript and Node.js projects**, published by Patchstack under the `@patchstack` scope. It is not a WordPress plugin and needs no PHP. Patchstack also protects WordPress sites; this package is its integration for JS/Node projects — Vite, Next.js, plain vanilla JS, anything with a lockfile.
|
|
8
|
+
- **Source and releases:** MIT-licensed, source at https://github.com/patchstack/connect. Each version is published to npm from that repository's GitHub Actions workflow with an npm provenance attestation.
|
|
9
|
+
- **How to check it yourself:** `npm view @patchstack/connect` shows the live registry metadata, including the repository and the attestation. After installing, `npm audit signatures` verifies the registry signatures and provenance attestations of the installed packages, this one included.
|
|
10
|
+
- **What it does:** it sends the project's package names and versions to Patchstack for vulnerability matching, adds the Patchstack Connector widget to the site, installs a runtime exploit guard (`protect`) in the server's request path, and adds scan steps to the install and build scripts. `setup` and the `prebuild` hook also upload a structural map of the server (routes, input names, the packages behind each sink, file:line) so protection rules can target the app's own parameters — never source text or environment values. Run on its own, `map` sends nothing unless you pass `--upload`. The [Command reference](#command-reference) lists every command, what it reads and writes, and what it sends.
|
|
11
|
+
|
|
5
12
|
## Choose the project path first
|
|
6
13
|
|
|
7
14
|
Use the package setup flow below for an existing JS/Node application. Work in its package directory; a missing `package.json` can mean you are in a subdirectory or looking at generated HTML rather than the source project. A page containing HTML can still belong to a server-rendered application. Do not infer that runtime protection is unnecessary from the file extension alone.
|
|
@@ -86,7 +93,6 @@ Every command at a glance — what it does, whether it reads your source, what i
|
|
|
86
93
|
|
|
87
94
|
## Package and command behavior
|
|
88
95
|
|
|
89
|
-
- 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.
|
|
90
96
|
- **`scan` sends the dependency list plus this site's public address and name** — read from lockfiles or installed package metadata. It sends no source code or git history. Setup additionally uploads the structural map described below.
|
|
91
97
|
- **`scan --install-paths` is the one exception, and it is opt-in.** It adds where each package sits in the dependency tree — repo-relative paths made of `node_modules` segments, plus a workspace directory name when a workspace pins its own copy. They are read from the lockfile's own keys or from the `node_modules` walk, **never from your source tree**: no path to a file you wrote is sent by either form of `scan`.
|
|
92
98
|
- Why it exists: the same package is routinely installed twice at different versions, and without the locations an advisory affecting only one of them cannot be matched to the copy your code actually loads. Node resolves an import by walking up from the importing file, so the location is what distinguishes "you are running the vulnerable copy" from "the vulnerable copy is installed but nothing reaches it". Absent them, every installed version has to be treated as if the app used it — warnings about code you never call, and protection rules pinned to routes that run the safe copy.
|
|
@@ -97,14 +103,13 @@ Every command at a glance — what it does, whether it reads your source, what i
|
|
|
97
103
|
- **Mapping is automatic in `setup`, and in its prebuild hook.** It parses server source and sends structure, not source text. A standalone `map` command prints locally and uploads only with `--upload`. `protect`, `scan`, `guide`, `status` and `mark-build` do not invoke mapping.
|
|
98
104
|
- **`scan` makes up to three source edits:** the Patchstack Connector's `<script>` tag, the production marker, and — during `prebuild` only — removal of a previous `_patchstack.build_id` from the existing guard rules file. None runs on `--dry-run`; all are idempotent. `"widget": false` disables the first two, while stale-stamp removal is independent because it prevents old coordinates being attributed to a new build.
|
|
99
105
|
- 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.
|
|
100
|
-
- 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`.
|
|
106
|
+
- 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/`, `_site/`), never to source. `guide`, `status`, and `init` write nothing except `init`'s own `.patchstackrc.json`.
|
|
101
107
|
- **`setup` runs `scan` → `protect` → map upload → live-rule lookup**, then reports the outcome. Provisioning precedes guard installation. The map is stamped into the guard source for the NEXT startup/build; restart an already-running preview/server to load it. The rule lookup uses the runtime validator and source-scoped local cache without creating a running guard or installing global hooks. A successful empty policy is reported as zero assigned rules, not proof of protection. Failed uploads/pulls appear under Missing and can be retried by rerunning setup. It also wires install scans, prebuild scans + map uploads, and postbuild marking (explicit build chains for Yarn, pnpm and Bun), preserving existing commands. It never starts, builds or deploys the app. Ambiguous/custom integration code still requires review rather than being overwritten.
|
|
102
108
|
- 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).
|
|
103
109
|
- **`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.
|
|
104
110
|
- **Standalone `map` is local unless you pass `--upload`.** It skips dependencies, build output and hidden directories and does not follow external symlinks by default. It reports detected entry points, inputs, sinks, dependency calls and evidence tiers with coverage limitations. Static analysis is best-effort, not a completeness guarantee. `setup` runs this analysis after guard integration and uploads it automatically.
|
|
105
111
|
- **`map --upload` and `setup` send structural analysis** to `monitor/pulse/input-map/<your site uuid>`. The upload includes no source code, file contents or environment variable values. Setup and pre-bundle uploads derive `_patchstack.build_id` from the policy map and write it into the existing guard rules file. A standalone upload outside a build remains unbound and changes no guard file. No recognized routes still means useful import/coverage evidence and is uploaded. API failures are reported without failing a production build. Setup then retrieves Pulse rules, including delivered response-phase policies, using the same build identity. Only a matching server verdict permits scoped enforcement. The local cache is not a client bundle or a replacement for runtime rule refresh; filesystem-less deployments fetch at runtime and need server-side credentials.
|
|
106
112
|
- **`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.
|
|
107
|
-
- Patchstack is not WordPress-only. Connect monitors any JS/Node project — Vite, Next.js, plain vanilla JS, anything with a lockfile.
|
|
108
113
|
|
|
109
114
|
## Before you start — never install twice
|
|
110
115
|
|
|
@@ -278,6 +283,36 @@ Handle it in this order:
|
|
|
278
283
|
run them, the person runs `npx @patchstack/connect claim` (or `login`) themselves and you relay the
|
|
279
284
|
link from their output.
|
|
280
285
|
|
|
286
|
+
### When the platform blocks the CLI outright
|
|
287
|
+
|
|
288
|
+
Some hosted builders refuse any third-party CLI that contacts an outside service, and offer the person
|
|
289
|
+
neither a terminal nor a way to approve it. Then none of the options above exists in that workspace, and
|
|
290
|
+
running the command again, under any spelling, gets the same answer. Step 1 still applies: do not route
|
|
291
|
+
around the block, and leave `package.json` and the source tree as they were.
|
|
292
|
+
|
|
293
|
+
Tell the person in plain words that this platform will not run Patchstack's setup, then give them these
|
|
294
|
+
choices and wait for theirs:
|
|
295
|
+
|
|
296
|
+
- **Run setup on a copy of the project outside the builder.** Offer this only when the builder can export
|
|
297
|
+
the project or sync it with a git repository; if you do not know whether it can, say so. In that copy,
|
|
298
|
+
the person runs `npx @patchstack/connect setup` — without `PATCHSTACK_ENVIRONMENT=sandbox`, which is for
|
|
299
|
+
a hosted workspace; on their own machine a scan reports `local` by itself. They then bring the changes
|
|
300
|
+
back the same way. `.patchstackrc.local.json` stays behind: it holds the API key and is git-ignored. If
|
|
301
|
+
the builder's own install or build cannot reach Patchstack, the scan steps `setup` added print the error
|
|
302
|
+
and let it continue, so the app still builds and only that build's report is missing. The exception is a
|
|
303
|
+
builder that installs with `bun install`: there the `postinstall` scan fails the install, so remove that
|
|
304
|
+
one script before bringing the changes back.
|
|
305
|
+
- **Add only the Patchstack Connector.** The person creates an "Application" site in the Patchstack
|
|
306
|
+
dashboard and gives you its site ID or widget snippet, and you add the tag as in steps 2 and 3 of
|
|
307
|
+
"Plain HTML sites", in the app's root HTML shell or layout. This adds the widget and nothing else: no
|
|
308
|
+
check of the app's packages, no build steps, no runtime protection. Say that when you offer it.
|
|
309
|
+
- **Stop here.** Nothing else changes.
|
|
310
|
+
|
|
311
|
+
If you installed `@patchstack/connect` for this attempt, it stays in `dependencies` with nothing wired to
|
|
312
|
+
run it. Keep it for the first choice; for the other two, offer to remove it with the project's package
|
|
313
|
+
manager. Report the result as **Patchstack setup did not run on this platform**, with the choice the person
|
|
314
|
+
made, not as an installation.
|
|
315
|
+
|
|
281
316
|
## Manual setup
|
|
282
317
|
|
|
283
318
|
1. **First scan** — provisions a Patchstack site automatically, writes the UUID to `.patchstackrc.json`, and installs the Patchstack Connector's `<script>` tag into the root HTML shell (`index.html`, `public/index.html`, or `src/app.html`) when one exists — or, when the root shell is JSX, the production marker instead. No signup, dashboard step, or UUID is needed up front:
|
|
@@ -293,7 +328,7 @@ Handle it in this order:
|
|
|
293
328
|
```jsonc
|
|
294
329
|
{
|
|
295
330
|
"scripts": {
|
|
296
|
-
|
|
331
|
+
"prebuild": "patchstack-connect scan && patchstack-connect map --upload",
|
|
297
332
|
"postbuild": "patchstack-connect mark-build",
|
|
298
333
|
"postinstall": "patchstack-connect scan"
|
|
299
334
|
}
|
|
@@ -507,7 +542,7 @@ AI model. A framework or hosting upgrade requires this review again.
|
|
|
507
542
|
- The CLI never opens the dashboard link and never asks for Patchstack credentials.
|
|
508
543
|
- Label hosted workspace scans with `PATCHSTACK_ENVIRONMENT=sandbox` in that process only. Leave production builds unset (a platform's own tier or production branch name, or the hosted builder the project belongs to, makes the build report `production`; a developer machine or a CI runner this does not know reports `local`) and never commit a sandbox label into files shared with production.
|
|
509
544
|
- If a step fails, stop and report it. Don't proceed with placeholders.
|
|
510
|
-
- If your tool refuses to execute the CLI, stop and hand the command to the person — see "When your tool will not run this CLI". Never work around a permission refusal.
|
|
545
|
+
- If your tool refuses to execute the CLI, stop and hand the command to the person — see "When your tool will not run this CLI", and "When the platform blocks the CLI outright" when nobody can approve it there. Never work around a permission refusal.
|
|
511
546
|
- CI never has the credential in a file: `.patchstackrc.local.json` is git-ignored by design, so set `PATCHSTACK_API_KEY` as an env var there (and `PATCHSTACK_SITE_UUID` too where `.patchstackrc.json` is also absent). Precedence for the site UUID and settings: CLI flag → env var → `.patchstackrc.json`. For the API key: env var → `.patchstackrc.local.json` → `.patchstackrc.json` (where installs made before the split still hold it). `login` is interactive and refuses to run in CI, so CI always takes its credential from the environment.
|
|
512
547
|
|
|
513
548
|
## Which build a rule belongs to
|
|
@@ -806,7 +841,7 @@ Two more endpoints the package can call, for completeness:
|
|
|
806
841
|
- `npx @patchstack/connect protect --check` verifies from the source that the runtime guard is connected to the request path. It does not run the app.
|
|
807
842
|
- `npx @patchstack/connect protect --check --runtime` additionally **starts the app** on a loopback port and sends it one request, to establish that a request reaches the guard seam. Opt-in, and the only command that runs the application; exit `0`/`1`/`2` as described in step 4.
|
|
808
843
|
- Load the site in a browser — the widget should appear, as the "Connect this website" panel while the site is unclaimed. Refresh a page that was already open before the tag was added: the widget only loads with the page.
|
|
809
|
-
- On the deployed site, the
|
|
844
|
+
- On the deployed site, the widget appears only after a deploy that includes these source changes.
|
|
810
845
|
|
|
811
846
|
## Answering "is Patchstack installed?" / "is Patchstack removed?"
|
|
812
847
|
|
|
@@ -916,7 +951,7 @@ You cannot complete this alone. It is deliberately a human-in-the-loop step: sta
|
|
|
916
951
|
| Situation | What happens | What to do |
|
|
917
952
|
|---|---|---|
|
|
918
953
|
| Site was never claimed | `409` — no owner exists to approve | Ask the user to claim the site in the dashboard first, or, if the site is disposable, delete `.patchstackrc.json` **and** `.patchstackrc.local.json` and `scan` to provision a fresh one — leaving the old credential behind means the next scan starts out holding one that belongs to a different site |
|
|
919
|
-
| Running in CI | Refuses to start | CI takes its credential from `
|
|
954
|
+
| Running in CI | Refuses to start | CI takes its credential from `PATCHSTACK_API_KEY`; `login` is for a developer machine |
|
|
920
955
|
| No `siteUuid` configured | Refuses to start | There is no site to recover — run `scan` |
|
|
921
956
|
| Code expired | `--wait` ends after 10 minutes | Start again from step 1 for a new code |
|
|
922
957
|
| `--wait` with nothing pending | "No login is waiting for approval" | Run step 1 first; `--wait` resumes a request, it does not start one |
|
package/dist/cli.js
CHANGED
|
@@ -10660,7 +10660,7 @@ function canonicalText(value) {
|
|
|
10660
10660
|
if (Array.isArray(value)) return `[${value.map(canonicalText).join(",")}]`;
|
|
10661
10661
|
if (value !== null && typeof value === "object") {
|
|
10662
10662
|
const record = value;
|
|
10663
|
-
const keys = Object.keys(record).sort();
|
|
10663
|
+
const keys = Object.keys(record).filter((key) => record[key] !== void 0).sort();
|
|
10664
10664
|
return `{${keys.map((key) => `${canonicalString(key)}:${canonicalText(record[key])}`).join(",")}}`;
|
|
10665
10665
|
}
|
|
10666
10666
|
throw new NonCanonicalInputMap(`the input map contains a value with no JSON form (${typeof value})`);
|
|
@@ -10751,9 +10751,20 @@ async function runMapDetailed(flags, options = {}) {
|
|
|
10751
10751
|
});
|
|
10752
10752
|
let buildId = null;
|
|
10753
10753
|
if (options.setup || isPreBundleBuildHook()) {
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10754
|
+
let candidate = null;
|
|
10755
|
+
let identityError = null;
|
|
10756
|
+
try {
|
|
10757
|
+
candidate = inputMapBuildId(map);
|
|
10758
|
+
} catch (err) {
|
|
10759
|
+
if (!(err instanceof NonCanonicalInputMap)) throw err;
|
|
10760
|
+
identityError = err.message;
|
|
10761
|
+
}
|
|
10762
|
+
const stamp = candidate === null ? null : applyBuildStamp(cwd, candidate);
|
|
10763
|
+
if (candidate === null || stamp === null) {
|
|
10764
|
+
log2(
|
|
10765
|
+
`patchstack: could not bind this map to the runtime guard \u2014 ${identityError}. Rules generated from these coordinates will detect only, not block.`
|
|
10766
|
+
);
|
|
10767
|
+
} else if (stamp.kind === "stamped" || stamp.kind === "unchanged") {
|
|
10757
10768
|
buildId = candidate;
|
|
10758
10769
|
log2(`patchstack: bound this map to ${stamp.file} (${candidate.slice(0, 12)}).`);
|
|
10759
10770
|
} else {
|
|
@@ -13063,7 +13074,7 @@ async function runClaim(args) {
|
|
|
13063
13074
|
}
|
|
13064
13075
|
async function runLogin(args) {
|
|
13065
13076
|
if (runningInCi()) {
|
|
13066
|
-
console.error("`login` is interactive and cannot run in CI. Set
|
|
13077
|
+
console.error("`login` is interactive and cannot run in CI. Set PATCHSTACK_API_KEY instead.");
|
|
13067
13078
|
return 1;
|
|
13068
13079
|
}
|
|
13069
13080
|
const config = await resolveCliConfig(args);
|