@patchstack/connect 0.5.0 → 0.5.2

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 CHANGED
@@ -9,7 +9,7 @@ Every command at a glance — what it does, whether it reads your source, what i
9
9
  | Command | What it does | Reads your source? | Writes to your project | Sends over the network |
10
10
  |---|---|---|---|---|
11
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; `node_modules/` is enumerated when no lockfile can be read (e.g. `bun.lockb`) or when the lockfiles present disagree. It also reads the `<title>` of the root `index.html` and the `name` in `package.json`, to report what the site is called | `.patchstackrc.json` (public: site UUID + settings); `.patchstackrc.local.json` (the API key, created owner-only) and a `.gitignore` entry for it — the CLI says so if it could not add one; the widget `<script>` tag in the root HTML shell — only after a successful post; the production marker in a code root shell — before the post, since it needs no site UUID | Package names + versions; this site's public address and name, where the project or build environment states them |
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, production marker, guard files, `package.json` scripts | Package names + versions and the site's public address and name (via `scan`) |
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, production marker, guard files, `package.json` scripts | Package names + versions and the site's public address and name (via `scan`); a claim token as a request header, only when you pass one |
13
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
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
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) |
@@ -18,6 +18,7 @@ Every command at a glance — what it does, whether it reads your source, what i
18
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
19
  | `init <site-uuid>` | Optional: pre-seed `.patchstackrc.json` with an existing UUID. | No | `.patchstackrc.json` only | Nothing |
20
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
+ | `claim` | Attach the site to a Patchstack account from the terminal: print a link the user opens to sign in (or sign up) and poll (10 min). Whoever approves becomes the owner. Does **not** rotate the credential. Same result as opening the dashboard link `scan` prints. Not usable in CI. | No | Nothing, unless the server issues a credential for a checkout that had none — then `.patchstackrc.local.json` | Device-code request + approval poll |
21
22
  | `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.local.json` on approval | Device-code request + approval poll |
22
23
  | `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
 
@@ -85,6 +86,8 @@ This is a request, not a mechanism: nothing in the install depends on it. Do it
85
86
 
86
87
  This provisions or reuses the site, manages the widget, installs and verifies runtime protection, wires dependency-install and build scans, prints a dashboard link, and finishes with the same status shown by `guide`. Re-running it reuses existing configuration, widget tags, guards, and commands rather than duplicating them.
87
88
 
89
+ **If the request you were given includes a claim token**, pass it exactly as given: `npx @patchstack/connect setup --claim-token <token>`. The site is then created in the person's own Patchstack account and `setup` prints their dashboard link for it — there is no sign-in step to relay. The token comes only from the person's Patchstack dashboard; never invent one, never write it to a file, never print it back. If `setup` reports that the token had expired or was not recognised, the site is not connected: hand over the dashboard link it prints instead, and tell the person they can copy a fresh prompt from the dashboard.
90
+
88
91
  In a hosted builder, run setup with `PATCHSTACK_ENVIRONMENT=sandbox` scoped to the workspace process/command, ensure the CLI's on-disk edits are adopted into the platform's persisted project state, then restart any already-running preview/server process so it loads the guard. Do not persist `"environment": "sandbox"` in `.patchstackrc.json`: deployed builds use the same committed files and should default to `production`. A client-only SPA has no server request path to guard; do not call it protected unless `protect --check` succeeds after a real server or edge seam is wired.
89
92
 
90
93
  **Finish by telling the user to refresh their preview.** The widget's "Report a vulnerability" button loads with the page, so a preview that was already open still shows the HTML from before setup — the button is missing there until it reloads. Nothing in the CLI can reach the user's browser, so relaying this is your job. Phrase it as a check rather than a required step: a builder that hot reloads, or a preview server you restarted, may have refreshed it already.
@@ -140,6 +143,46 @@ It is server-only. Never put it in the widget tag, client bundles, or public env
140
143
 
141
144
  `setup` performs both steps automatically. The explicit commands are for manual setup or repair. If verification reports a generic or existing framework seam, complete the printed source edit and re-run `--check`; do not report protection as active until it exits successfully.
142
145
 
146
+ `--check` reads the app's source. It can establish that the guard is imported and called on a request
147
+ path; it cannot establish that a request ever reaches it — an app can wire the guard onto one server
148
+ and serve traffic from another, and that passes. To settle the difference there is an opt-in check
149
+ that **starts the application**:
150
+
151
+ ```
152
+ npx @patchstack/connect protect --check --runtime
153
+ ```
154
+
155
+ It launches the project's entry with `node`, moves the HTTP listeners **that process** opens to an
156
+ ephemeral loopback port, sends one request per listener carrying a per-run challenge, and reports
157
+ whether the scaffolded guard seam answered it. Exit `0` runtime traversal reached the seam, `1` a
158
+ listener answered and the seam did not, `2` it could not be established — neither a pass nor a
159
+ failure, with the structural checks still standing on their own.
160
+
161
+ Exit `2` is the answer for everything this cannot speak for, and the reason is always printed. The
162
+ common one is an entry that needs the project's own toolchain (a TypeScript entry, a framework
163
+ launcher, a watcher, another runtime, anything reached through a package manager), which this never
164
+ installs, builds or invents. The others are about scope: **the run answers for one process, one
165
+ thread, and one discovery window.** If the app attempts to start another process, the launch is
166
+ refused and the answer is `2`. A child can daemonize after it starts without declaring that in its
167
+ launch options, so allowing it would make the end-of-run process-group cleanup a claim the verifier
168
+ cannot establish. The app sees `EPERM`. A worker thread is also `2`: it inherits the listener
169
+ handling, but it cannot report back, so its listeners can be neither counted nor asked. So is a
170
+ listener that bound an address other than loopback, one that cannot be probed, and anything the app
171
+ opens after the discovery window has closed — the app is asked to stop and its acknowledgement is
172
+ what closes that window, so a run that never gets one is `2` as well. An inherited `NODE_OPTIONS`
173
+ that would run code before the listener handling is in place — a `--require` or `--import` in your
174
+ environment — is `2` too, and is refused before the app is launched rather than after.
175
+
176
+ A worker handed a replacement environment that does not preserve the propagated `NODE_OPTIONS` is
177
+ refused outright, because it would not load the listener handling. The app sees `EPERM`, and the run
178
+ reports `2`.
179
+
180
+ What a pass says is exactly: **runtime traversal reached the scaffolded guard seam.** It does not say
181
+ rules were delivered, that the deployed app is wired, or that ordinary traffic is blocked.
182
+
183
+ Nothing else runs the application. `protect`, `protect --check`, `setup`, `guide`, `scan`, `status`
184
+ and `mark-build` only read and write files.
185
+
143
186
  5. **Commit** `.patchstackrc.json`, the updated `package.json`, the guard/framework source changes, and the layout/HTML file carrying the widget tag (and the production marker, when `scan` wrote one into a JSX root), so every developer and CI run reports to the same site.
144
187
 
145
188
  **Do not commit `.patchstackrc.local.json`.** That file holds the API key issued at provision; the scan writes it and adds it to `.gitignore`, and tells you if it could not. `.patchstackrc.json` holds only the site UUID and settings, and the UUID is public by design — it ships in the widget tag in served HTML.
@@ -149,6 +192,7 @@ It is server-only. Never put it in the widget tag, client bundles, or public env
149
192
  ## Rules
150
193
 
151
194
  - Never invent or guess a UUID — the scan provisions it, the widget silently no-ops on a fake one.
195
+ - Never invent or guess a claim token either. One is only ever handed to you by the person, from their own Patchstack dashboard; pass it with `--claim-token` (or `PATCHSTACK_CLAIM_TOKEN`) and nowhere else — not into `.patchstackrc.json`, not into a committed file, not into your reply.
152
196
  - The CLI never opens the dashboard link and never asks for Patchstack credentials.
153
197
  - Label hosted workspace scans with `PATCHSTACK_ENVIRONMENT=sandbox` in that process only. Leave production builds unset (the default is `production`) and never commit a sandbox label into files shared with production.
154
198
  - If a step fails, stop and report it. Don't proceed with placeholders.
@@ -388,7 +432,8 @@ Two more endpoints the package can call, for completeness:
388
432
  ## Verifying the install
389
433
 
390
434
  - `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`).
391
- - `npx @patchstack/connect protect --check` verifies the runtime guard is connected to the request path.
435
+ - `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.
436
+ - `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.
392
437
  - Load the site in a browser — the "Report a vulnerability" button should appear. Refresh a page that was already open before the tag was added: the button only loads with the page.
393
438
  - On the deployed site, the button appears only after a deploy that includes these source changes.
394
439
 
@@ -401,6 +446,54 @@ These are **two independent states** — never conflate them:
401
446
 
402
447
  Local files alone cannot tell you whether the site was removed from Patchstack. Run `npx @patchstack/connect status` and read the `Site status` line, then answer with both states. For example, when the site was removed but the local files remain, say: *"The site itself was removed from Patchstack — reporting has stopped and the widget no longer renders. The local integration code (widget tag, `.patchstackrc.json`, `.patchstackrc.local.json`, the dependency) is still in the project; want me to remove it?"* — not "Patchstack is still installed."
403
448
 
449
+ ## Attaching the site to an account — `claim`
450
+
451
+ A scan provisions the site without an owner. It is monitored from that moment, but its reports are
452
+ only visible once someone attaches it to a Patchstack account. Opening the dashboard link that `scan`
453
+ and `status` print does that. `claim` does the same thing from the terminal, for when the link is
454
+ output nobody is looking at.
455
+
456
+ ```
457
+ npx @patchstack/connect claim
458
+ ```
459
+
460
+ ```
461
+ Your code: BQDX-7ZKM
462
+ Claim at: https://api.patchstack.com/monitor/pulse/device?code=BQDX-7ZKM
463
+
464
+ Open that link and sign in to Patchstack — or create an account — to attach
465
+ this site to it. Whoever approves becomes the site's owner.
466
+ ```
467
+
468
+ **If you are an assistant running this, the sequence is three steps:**
469
+
470
+ ```
471
+ 1. npx @patchstack/connect claim → prints the link, exits straight away
472
+ 2. Give the user the link. Wait for them to say they have done it.
473
+ 3. npx @patchstack/connect claim → the SAME command again, after they confirm.
474
+ ```
475
+
476
+ - **Step 1 exits immediately** when the output is piped or captured, rather than blocking for ten
477
+ minutes on a link you cannot see yet.
478
+ - **Step 3 is the same command.** While a request is still valid it resumes rather than restarting, so
479
+ running `claim` again never invalidates the link the user is looking at. If they have not finished
480
+ yet it says so, with the time remaining, and exits 0.
481
+ - **An already-claimed site exits 0, not 1.** It is the goal state. Re-running after the user claimed
482
+ in the browser reports that and stops; it is not a setup failure.
483
+
484
+ `claim --wait` is the blocking variant. Prefer the plain re-run — it keeps each command short, which
485
+ is what fits a conversation.
486
+
487
+ ### What it does not do
488
+
489
+ - **It does not rotate the credential.** The project already holds one from provisioning, and CI,
490
+ deploys and other checkouts keep working. (`login` is the command that rotates; use it only to
491
+ recover a lost credential.) A credential is written only when the server issues one for a checkout
492
+ that had none.
493
+ - **It does not open a browser**, and it cannot claim on the user's behalf: the approval is a person
494
+ signing in to Patchstack.
495
+ - **It does not work in CI** — there is no browser and no one to sign in. It refuses and exits 1.
496
+
404
497
  ## Recovering a lost credential — `login`
405
498
 
406
499
  Use this when the project **already has a site** but its credential is gone or rejected: `.patchstackrc.local.json` was deleted, the repo was cloned without it (it is git-ignored, so a fresh clone never has it), a container was recycled, or ingest started failing with 401. The site UUID `login` needs comes from the committed `.patchstackrc.json`.
package/README.md CHANGED
@@ -73,6 +73,13 @@ patchstack-connect protect Install/reconcile the always-
73
73
  guard. Auto-wires supported server stacks;
74
74
  use --check to verify or --demo for local rules.
75
75
  Also run by setup; never run by scan/guide/mark-build.
76
+ --check reads your source and never runs the app.
77
+ --check --runtime STARTS THE APP on a loopback
78
+ port and sends it one request, to establish that
79
+ a request reaches the guard seam. Opt-in, and the
80
+ only mode that runs the app. Exit 0 traversed,
81
+ 1 a listener answered instead of the guard,
82
+ 2 could not be established (see below).
76
83
  patchstack-connect map [--dir p] [--out f] [--upload]
77
84
  Print a JSON map of this project's attack
78
85
  surface: server entry points, the inputs each
@@ -90,12 +97,32 @@ patchstack-connect demo node-serialize Production-backed walkthrough
90
97
  patchstack-connect demo-guide node-serialize Read-only, state-aware instructions for the
91
98
  local demo, including the next exact command,
92
99
  expected proof, and cleanup.
100
+ patchstack-connect claim [--wait] Attach this site to a Patchstack account from
101
+ the terminal. Prints a link for the user to open
102
+ and sign in (or sign up); whoever approves becomes
103
+ the site's owner. Piped or captured, it prints the
104
+ link and exits — run it again once the user
105
+ confirms. Does not rotate the credential. Same
106
+ result as opening the dashboard link scan prints.
107
+ Not usable in CI
108
+ patchstack-connect login [--wait] Recover this site's credential when
109
+ .patchstackrc.local.json has been lost. Prints a
110
+ link for the site's OWNER to approve. Approving
111
+ ROTATES the credential, so CI, deploys and other
112
+ machines using the old one must be updated.
113
+ Not usable in CI
114
+ patchstack-connect uninstall [options] Signal Patchstack that this package is being
115
+ removed. An unclaimed site record is deleted; a
116
+ claimed one is flagged for its owner. Does NOT
117
+ touch local files
93
118
  patchstack-connect help Print help
94
119
  patchstack-connect --version Print the installed version
95
120
 
96
121
  Options (for scan, setup, and status):
97
122
  --site-uuid <uuid> Override the configured site UUID
98
123
  --endpoint <url> Override the API endpoint
124
+ --claim-token <token> (scan, setup) Connect the site to the account that issued
125
+ the token (from the dashboard's "Connect website" prompt)
99
126
  --dry-run (scan only) Print the payload without posting
100
127
 
101
128
  Options (for demo and demo-guide):
@@ -103,11 +130,74 @@ Options (for demo and demo-guide):
103
130
  (default: http://localhost:3000/api/tasks)
104
131
  ```
105
132
 
133
+ ### Verifying the guard at runtime (opt-in)
134
+
135
+ `protect --check` reads the app's source. That establishes the guard is imported and called on a
136
+ request path — not that a request ever reaches it. An app can wire the guard onto one server and serve
137
+ its traffic from another, and the structural check passes.
138
+
139
+ `protect --check --runtime` settles that one question by **starting the application**:
140
+
141
+ ```
142
+ npx @patchstack/connect protect --check --runtime
143
+ ```
144
+
145
+ It runs the project's entry with `node`, moves the HTTP listeners **that process** opens to an ephemeral
146
+ loopback port (so a port already in use is not a failure), sends one request per listener carrying a
147
+ challenge generated for that run, and reports whether the scaffolded guard seam answered it. The child
148
+ is started in its own process group and killed with it — including if you interrupt the command. A
149
+ listener it cannot probe — a Unix socket, a file descriptor, a handed-over handle, an HTTP/2 server — is
150
+ reported and prevented from binding at all, rather than opened on the verifier's behalf.
151
+
152
+ **One process is the scope — one thread of it, and one discovery window.** If the app attempts to start
153
+ another process, the launch is refused and the answer is `2`, whatever that process is. A child can
154
+ daemonize after it starts without declaring that in its launch options, so allowing it would make the
155
+ end-of-run process-group cleanup a claim the verifier cannot establish. The app sees the launch fail
156
+ with `EPERM`. A **worker thread** is also `2`: it inherits the listener handling, but a worker has no
157
+ channel back, so its listeners can be neither counted nor asked. A worker handed a replacement
158
+ environment that does not preserve the propagated `NODE_OPTIONS` is refused, because it would not load
159
+ that handling at all.
160
+
161
+ Everything else the run finds also ends it this way: a listener that bound an address other than
162
+ loopback, a listener it cannot probe, and anything the app opens **after the discovery window closes** —
163
+ a second listener appearing while the first is still being asked cannot join a set that is already being
164
+ answered from. Closing that window is a handshake: the app is asked to stop opening listeners and its
165
+ acknowledgement is what proves nothing is still in flight, so a run that never gets one reports `2`
166
+ rather than passing. An inherited `NODE_OPTIONS` is checked before anything is launched, too: Node reads
167
+ that variable ahead of the command line, so a `--require` or `--import` sitting in your environment would
168
+ run before the listener handling was in place, and the run reports `2` rather than starting the app with
169
+ less containment than it claims. Recognised flags are passed through, with the reporter first.
170
+
171
+ A pass says exactly this: **runtime traversal reached the scaffolded guard seam.** It does not say
172
+ rules were delivered, that the deployed app is wired, or that ordinary traffic is blocked. The
173
+ challenge is generated per run, so a fixed response or a reflected header cannot answer it — but the
174
+ challenge does reach the whole app process, so this establishes traversal in a cooperating app rather
175
+ than against an app written to answer for itself.
176
+
177
+ | Exit | Meaning |
178
+ |---|---|
179
+ | `0` | A request reached the scaffolded guard seam. |
180
+ | `1` | A listener answered and the seam did not — or the structural checks failed, in which case the app is not started at all. |
181
+ | `2` | It could not be established. Neither a pass nor a failure; the structural checks still stand. |
182
+
183
+ Exit `2` is the common answer for entries this deliberately will not start. It runs `node <file>` on a
184
+ file the project already has, and nothing else — no package-manager scripts, no `node_modules/.bin`, no
185
+ build, no install. So a TypeScript entry, a framework launcher (`next start`), a watcher (`nodemon`),
186
+ another runtime (`bun`), or a script wrapped in an environment shim all report unavailable with the
187
+ reason printed. To make such a project verifiable, point a `start` script at a built, directly loadable
188
+ file — the check reports which entry it used and where it came from.
189
+
190
+ Windows reports exit `2` without starting anything: the cleanup this relies on is a POSIX process
191
+ group, and a verification that can leave a server running is worse than an unanswered question.
192
+
193
+ No other command runs your application. `protect`, `protect --check`, `setup`, `guide`, `scan`,
194
+ `status` and `mark-build` only read and write files.
195
+
106
196
  ## Configuration
107
197
 
108
198
  Precedence (highest wins):
109
199
 
110
- 1. CLI flag (`--site-uuid`, `--endpoint`)
200
+ 1. CLI flag (`--site-uuid`, `--endpoint`, `--claim-token`)
111
201
  2. Environment variable
112
202
  3. `.patchstackrc.local.json` in the current directory (the credential)
113
203
  4. `.patchstackrc.json` in the current directory
@@ -118,6 +208,7 @@ Environment variables:
118
208
  - `PATCHSTACK_ENDPOINT` — override the API endpoint (default `https://api.patchstack.com/monitor/pulse/manifest`)
119
209
  - `PATCHSTACK_TIMEOUT_MS` — request timeout in milliseconds (default `30000`)
120
210
  - `PATCHSTACK_ENVIRONMENT` — manifest label: `production` (default) or `sandbox`
211
+ - `PATCHSTACK_CLAIM_TOKEN` — connect the site straight to your account (see *Connecting straight to your account*)
121
212
 
122
213
  Two files, because one value is public and the other is not.
123
214
 
@@ -152,6 +243,17 @@ The credential's file is never committed, so CI needs `PATCHSTACK_API_KEY` in th
152
243
 
153
244
  A `pulseAuth` field is still read if present, and `PATCHSTACK_PULSE_AUTH` still overrides, for deployments that authenticate Pulse ingest with a different credential from block-logs. Neither is written by default, and neither is needed when the two share one.
154
245
 
246
+ ### Connecting straight to your account
247
+
248
+ The dashboard's "Connect website" prompt carries a **claim token**. Pass it to the first `setup` (or `scan`) and the site it provisions is created in your account, so there is no dashboard link to open afterwards:
249
+
250
+ ```bash
251
+ npx @patchstack/connect setup --claim-token <token>
252
+ # or: PATCHSTACK_CLAIM_TOKEN=<token> npx @patchstack/connect setup
253
+ ```
254
+
255
+ The token names your account, not the project: it is never written to `.patchstackrc.json` or the credential file, it is sent to Patchstack as a request header rather than in the manifest body, and it stops working within a day. A token that has expired (or one Patchstack does not recognise) leaves the site exactly as a scan without one would — unconnected, with the dashboard link printed — and `scan` says so. Re-running `setup` with the same token against a site already in your account is a no-op that says the site is already connected; a site that belongs to a different account is left alone.
256
+
155
257
  ### Sandbox and production manifests
156
258
 
157
259
  Every `scan` sends an environment label with its dependency manifest. The default is `production`; sandboxed builders should set `PATCHSTACK_ENVIRONMENT=sandbox` in the sandbox process only. Patchstack stores and deduplicates manifests per environment, so an iterative workspace scan does not replace the last production manifest.
@@ -244,6 +346,8 @@ The name is what the dashboard calls the site. It is taken from `name` in `.patc
244
346
 
245
347
  You can see exactly what would be sent, without sending it, by running `npx @patchstack/connect scan --dry-run`: the preview it prints is the request body itself. Patchstack only applies either field to a site that does not have one yet: it never re-points a site whose address is real, and never replaces a name set in the dashboard.
246
348
 
349
+ One thing travels outside that body: a claim token, when you pass one (`--claim-token` / `PATCHSTACK_CLAIM_TOKEN`), is sent as the `X-Patchstack-Claim-Token` request header so that the site is created in your account. Without one, no such header is sent.
350
+
247
351
  ### `scan --install-paths` (opt-in)
248
352
 
249
353
  Pass it and each entry also carries where that version is installed: