@patchstack/connect 0.5.0 → 0.5.1

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) |
@@ -85,6 +85,8 @@ This is a request, not a mechanism: nothing in the install depends on it. Do it
85
85
 
86
86
  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
87
 
88
+ **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.
89
+
88
90
  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
91
 
90
92
  **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.
@@ -149,6 +151,7 @@ It is server-only. Never put it in the widget tag, client bundles, or public env
149
151
  ## Rules
150
152
 
151
153
  - Never invent or guess a UUID — the scan provisions it, the widget silently no-ops on a fake one.
154
+ - 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
155
  - The CLI never opens the dashboard link and never asks for Patchstack credentials.
153
156
  - 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
157
  - If a step fails, stop and report it. Don't proceed with placeholders.
package/README.md CHANGED
@@ -96,6 +96,8 @@ patchstack-connect --version Print the installed version
96
96
  Options (for scan, setup, and status):
97
97
  --site-uuid <uuid> Override the configured site UUID
98
98
  --endpoint <url> Override the API endpoint
99
+ --claim-token <token> (scan, setup) Connect the site to the account that issued
100
+ the token (from the dashboard's "Connect website" prompt)
99
101
  --dry-run (scan only) Print the payload without posting
100
102
 
101
103
  Options (for demo and demo-guide):
@@ -107,7 +109,7 @@ Options (for demo and demo-guide):
107
109
 
108
110
  Precedence (highest wins):
109
111
 
110
- 1. CLI flag (`--site-uuid`, `--endpoint`)
112
+ 1. CLI flag (`--site-uuid`, `--endpoint`, `--claim-token`)
111
113
  2. Environment variable
112
114
  3. `.patchstackrc.local.json` in the current directory (the credential)
113
115
  4. `.patchstackrc.json` in the current directory
@@ -118,6 +120,7 @@ Environment variables:
118
120
  - `PATCHSTACK_ENDPOINT` — override the API endpoint (default `https://api.patchstack.com/monitor/pulse/manifest`)
119
121
  - `PATCHSTACK_TIMEOUT_MS` — request timeout in milliseconds (default `30000`)
120
122
  - `PATCHSTACK_ENVIRONMENT` — manifest label: `production` (default) or `sandbox`
123
+ - `PATCHSTACK_CLAIM_TOKEN` — connect the site straight to your account (see *Connecting straight to your account*)
121
124
 
122
125
  Two files, because one value is public and the other is not.
123
126
 
@@ -152,6 +155,17 @@ The credential's file is never committed, so CI needs `PATCHSTACK_API_KEY` in th
152
155
 
153
156
  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
157
 
158
+ ### Connecting straight to your account
159
+
160
+ 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:
161
+
162
+ ```bash
163
+ npx @patchstack/connect setup --claim-token <token>
164
+ # or: PATCHSTACK_CLAIM_TOKEN=<token> npx @patchstack/connect setup
165
+ ```
166
+
167
+ 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.
168
+
155
169
  ### Sandbox and production manifests
156
170
 
157
171
  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 +258,8 @@ The name is what the dashboard calls the site. It is taken from `name` in `.patc
244
258
 
245
259
  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
260
 
261
+ 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.
262
+
247
263
  ### `scan --install-paths` (opt-in)
248
264
 
249
265
  Pass it and each entry also carries where that version is installed:
package/dist/cli.js CHANGED
@@ -1291,6 +1291,25 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
1291
1291
  // src/client.ts
1292
1292
  var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
1293
1293
  var DEFAULT_TIMEOUT_MS = 3e4;
1294
+ var CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
1295
+ function claimTokenHeader(config) {
1296
+ return typeof config.claimToken === "string" && config.claimToken !== "" ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
1297
+ }
1298
+ function claimOutcomeLines(claim2, config) {
1299
+ if (typeof config.claimToken !== "string" || config.claimToken === "") return [];
1300
+ if (claim2?.state === "claimed" || claim2?.state === "owned-by-you") {
1301
+ const dashboard = typeof claim2.dashboard_url === "string" && claim2.dashboard_url !== "" ? [`Dashboard: ${claim2.dashboard_url}`] : [];
1302
+ return [
1303
+ claim2.state === "claimed" ? "Connected to your Patchstack account." : "This site is already connected to your Patchstack account.",
1304
+ ...dashboard
1305
+ ];
1306
+ }
1307
+ const why = claim2 === void 0 ? "Patchstack did not act on the claim token" : claim2.state === "owned-by-other" ? "this site belongs to a different Patchstack account" : claim2.reason === "expired" ? "the claim token has expired" : "Patchstack did not recognise the claim token";
1308
+ return [
1309
+ `Not connected to your account: ${why}.`,
1310
+ "Open the dashboard link below to connect it, or copy a fresh prompt from the dashboard."
1311
+ ];
1312
+ }
1294
1313
  function buildEndpointUrl(base, siteUuid) {
1295
1314
  const trimmed = base.replace(/\/$/, "");
1296
1315
  return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
@@ -1454,7 +1473,8 @@ async function postManifest(config, payload) {
1454
1473
  headers: {
1455
1474
  "Content-Type": "application/json",
1456
1475
  Accept: "application/json",
1457
- "User-Agent": "@patchstack/connect"
1476
+ "User-Agent": "@patchstack/connect",
1477
+ ...claimTokenHeader(config)
1458
1478
  },
1459
1479
  body: JSON.stringify(buildManifestBody(config, payload)),
1460
1480
  signal: AbortSignal.timeout(timeoutMs)
@@ -1944,6 +1964,7 @@ async function resolveConfig(options) {
1944
1964
  const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
1945
1965
  const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
1946
1966
  const identity = options.detectSiteIdentity === true ? await resolveSiteIdentity(options.cwd, fromEnv, fromFile) : { url: null, name: null };
1967
+ const claimToken = stated(options.cliClaimToken) ?? stated(process.env.PATCHSTACK_CLAIM_TOKEN);
1947
1968
  return {
1948
1969
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
1949
1970
  apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
@@ -1953,7 +1974,8 @@ async function resolveConfig(options) {
1953
1974
  endpoint,
1954
1975
  timeoutMs,
1955
1976
  environment,
1956
- widget: fromFile.widget !== false
1977
+ widget: fromFile.widget !== false,
1978
+ claimToken
1957
1979
  };
1958
1980
  }
1959
1981
  async function writeConfigFile(cwd, config) {
@@ -7214,6 +7236,10 @@ Options (for scan, setup, status, and uninstall):
7214
7236
  same under a workspace that pins its own copy), so an
7215
7237
  advisory can be matched to the copy your code actually
7216
7238
  loads. Off by default; never source file paths
7239
+ --claim-token <token> (scan, setup) Connect the site to the Patchstack account
7240
+ that issued the token \u2014 it comes from the dashboard's
7241
+ "Connect website" prompt. PATCHSTACK_CLAIM_TOKEN works
7242
+ too. Never written to a file, never printed back
7217
7243
 
7218
7244
  Options (for mark-build):
7219
7245
  --dir <path> Build output directory (default: auto-detect
@@ -7247,7 +7273,7 @@ Examples:
7247
7273
  npx @patchstack/connect demo node-serialize
7248
7274
  npx @patchstack/connect demo-guide node-serialize
7249
7275
  `;
7250
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url", "out"]);
7276
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url", "out", "claim-token"]);
7251
7277
  function parseArgs(argv) {
7252
7278
  const args = argv.slice(2);
7253
7279
  const positional = [];
@@ -7381,6 +7407,7 @@ async function runScan(args, options = {}) {
7381
7407
  cwd: process.cwd(),
7382
7408
  cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
7383
7409
  cliEndpoint: getStringFlag(args.flags, "endpoint"),
7410
+ cliClaimToken: getStringFlag(args.flags, "claim-token"),
7384
7411
  // The one command that reports them, so the one command that resolves them.
7385
7412
  detectSiteIdentity: true
7386
7413
  });
@@ -7421,6 +7448,9 @@ async function runScan(args, options = {}) {
7421
7448
  if (typeof body.name === "string") {
7422
7449
  console.log(`Reporting this app's name as "${body.name}".`);
7423
7450
  }
7451
+ if (typeof config.claimToken === "string" && config.claimToken !== "") {
7452
+ console.log("A claim token is set: the site will be connected to the Patchstack account that issued it.");
7453
+ }
7424
7454
  if (dryRun) {
7425
7455
  console.log("");
7426
7456
  if (config.siteUuid === null) {
@@ -7475,14 +7505,21 @@ async function runScan(args, options = {}) {
7475
7505
  } else {
7476
7506
  console.log(`Server response: ${response.message ?? JSON.stringify(response)}`);
7477
7507
  }
7508
+ const claimLines = claimOutcomeLines(response.claim, config);
7509
+ if (claimLines.length > 0) {
7510
+ console.log("");
7511
+ for (const line of claimLines) console.log(line);
7512
+ }
7513
+ const connected = response.claim?.state === "claimed" || response.claim?.state === "owned-by-you";
7478
7514
  const effectiveUuid = config.siteUuid ?? response.uuid ?? null;
7479
7515
  if (config.widget && effectiveUuid !== null && effectiveUuid.length > 0) {
7480
7516
  reportSourceWidget(effectiveUuid);
7481
7517
  }
7482
- if (provisioning && response.uuid !== void 0 && response.uuid.length > 0) {
7518
+ const linkUuid = response.uuid ?? config.siteUuid;
7519
+ if (!connected && (provisioning || claimLines.length > 0) && linkUuid !== null && linkUuid !== void 0 && linkUuid.length > 0) {
7483
7520
  console.log("");
7484
7521
  console.log("Open this dashboard link to view vulnerability reports:");
7485
- console.log(` ${buildClaimUrl(config.endpoint, response.uuid)}`);
7522
+ console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`);
7486
7523
  if (config.endpoint !== DEFAULT_ENDPOINT) {
7487
7524
  console.log(" (this URL inherits the endpoint override above)");
7488
7525
  }