@patchstack/connect 0.5.20 → 0.5.22

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
@@ -16,11 +16,11 @@ For a standalone site made of HTML, CSS, and browser JavaScript, with no package
16
16
  - **Create a Node project in the folder and let Connect set it up.** No trip to the dashboard first: `setup` creates the site and prints the link that connects it to their account. It adds `package.json`, a lockfile and `node_modules` to their folder. Packages they add later are checked for known security problems. A page with no packages of its own has little to check at first, and there is still no runtime protection without a server. Follow "Creating a Node project for a plain HTML site" below.
17
17
  - **Stop here.** Nothing is changed.
18
18
 
19
- Do not pick for them, and do not create the Node project unless they choose it.
20
- 2. Add one widget tag before `</body>` in the page or shared layout. Preserve an existing correct tag. For a page published directly without a build step, disable the widget's build-mode onboarding with `data-build-mode="false"`:
19
+ Do not pick for them, and do not create the Node project unless they choose it. **A request to install or set up `@patchstack/connect` is that choice**: follow "Creating a Node project for a plain HTML site" without asking.
20
+ 2. Add one widget tag before `</body>` in the page or shared layout. Preserve an existing correct tag. Do not add `data-build-mode`: setting it to `"false"` hides the owner's connect and log-in panels everywhere, including on the person's own machine.
21
21
 
22
22
  ```html
23
- <script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="YOUR_SITE_UUID" data-build-mode="false" defer></script>
23
+ <script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="YOUR_SITE_UUID" defer></script>
24
24
  ```
25
25
 
26
26
  Replace `YOUR_SITE_UUID` with the real public site UUID before saving. Keep credentials out of the page. The [public widget reference](https://cdn.patchstack.com/llm.html) documents this embed and its options.
@@ -32,20 +32,25 @@ Report this as **Patchstack Connector installed**, with any remaining preview or
32
32
 
33
33
  Only when the person chose this option in step 1 above. Work in the folder that holds the site's pages.
34
34
 
35
- 1. Create the project and install Connect:
35
+ The pages get a build step, even though nothing is compiled. The build is what runs `scan` before and `mark-build` after, and `mark-build` is what tells a visitor's browser that a page is the live site. Without it the live site is never recognised as deployed. The build copies the pages to `dist/` and the stamp goes on the copy, so the pages the person edits stay as they are and their own machine keeps showing the owner panels.
36
+
37
+ 1. Move the pages (every `.html` file, plus the CSS, scripts and images they use) into `public/` if they are not there already. Then create the project with its build script **before** running `setup`, so `setup` wires the hooks around it:
36
38
 
37
39
  ```
38
40
  npm init -y
41
+ npm pkg set scripts.build="node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.cpSync('public','dist',{recursive:true})\""
39
42
  npm install --save @patchstack/connect
40
43
  npx @patchstack/connect setup
41
44
  ```
42
45
 
43
- In a hosted builder, scope `PATCHSTACK_ENVIRONMENT=sandbox` to the `setup` command, as in "Automated setup". `setup` creates the site, writes its `siteUuid` to `.patchstackrc.json`, adds `"postinstall": "patchstack-connect scan"` to `package.json`, and prints the dashboard link. With no `build` script it adds no build steps.
44
- 2. Put the widget on the pages. `setup` adds the tag only to `index.html`, `public/index.html` or `src/app.html`. For any other page it lists the widget under `Missing` and prints the tag to add. Add one tag before `</body>` on each page, or in the shared layout. Because the pages are published without a build step, the tag needs `data-build-mode="false"`, as in step 2 above. If `setup` already added the tag to `index.html`, add that attribute to it. Later scans keep the attribute: they rewrite the tag only when the site ID changes.
45
- 3. Two `✘` lines are expected and need no fix:
46
+ `setup` creates the site, writes its `siteUuid` to `.patchstackrc.json`, adds the Patchstack widget to `public/index.html`, adds `"postinstall": "patchstack-connect scan"`, and wires `"prebuild": "patchstack-connect scan"` and `"postbuild": "patchstack-connect mark-build"` around the build. In a hosted builder, scope `PATCHSTACK_ENVIRONMENT=sandbox` to the `setup` command, as in "Automated setup".
47
+ 2. Add `dist` to `.gitignore` next to the entries `setup` wrote.
48
+ 3. Put the widget on the other pages. `setup` adds the tag only to `index.html`, `public/index.html` or `src/app.html`. For any other page it lists the widget under `Missing` and prints the tag to add. Add one tag before `</body>` on each page, or in the shared layout, exactly as printed — no `data-build-mode`.
49
+ 4. When the person names where the site is published, add that host's build settings so it publishes `dist/` and runs the build. See "Deploying" below; for Netlify that is a `netlify.toml` with `command = "npm run build"` and `publish = "dist"`.
50
+ 5. Two `✘` lines are expected and need no fix:
46
51
  - `Runtime protection: no server file found`. A plain HTML site has no server to guard. Do not add one. `setup` leaves a generic guard in `patchstack/`, which nothing loads until a server does.
47
52
  - `Deploy project to protect live app`. Publishing the pages is the person's step.
48
- 4. End as in "The message you end on", with the dashboard link from the `Next:` line. Say that the widget, and a check of the packages the site installs, are active, and that runtime protection is not. Remind them to publish the pages when ready, and that `node_modules` is not part of the site. Do not publish anything yourself.
53
+ 6. End as in "The message you end on", with the dashboard link from the `Next:` line. Say that the widget, and a check of the packages the site installs, are active, and that runtime protection is not. To preview it on their own machine, serve `public/` (for example `python3 -m http.server -d public`). Do not publish anything yourself.
49
54
 
50
55
  ### JS/Node applications — the usual path
51
56
 
@@ -129,10 +134,37 @@ When setup is finished, the person reads one short message, not a transcript of
129
134
  1. **One sentence on the result.** "Patchstack is now watching your site." If something did not finish, say what, in one line.
130
135
  2. **The four steps**, copied from the end of the `setup` output with their `✔` and `✘`.
131
136
  3. **The next step**, from the `Next:` line, in plain words, with its link or command.
132
- 4. **Refresh and deploy.** Refresh the preview to see the Patchstack Connector, and deploy when ready.
137
+ 4. **Refresh and deploy.** Refresh the preview to see the Patchstack Connector, and deploy when ready. Say how, in one line: "When you want it live, ask me to deploy it — I'll run the build so Patchstack can recognise the live site."
138
+
139
+ **When the person later asks you to deploy, re-read "Deploying" below before running anything.** A deploy that skips the build, or uploads the project folder, publishes the API key and leaves the live site unrecognised.
133
140
 
134
141
  Leave out the files you changed, the commands you ran, settings, and anything that worked as expected. If the person asks for the detail, give it then. `--verbose` prints it.
135
142
 
143
+ ## Deploying
144
+
145
+ Only when the person asks you to publish the site. Two things decide whether Patchstack recognises the deploy: the build has to run, and it has to know it is the production build.
146
+
147
+ - **Deploy through the build, and publish only its output.** Never pass `--no-build`, and never upload the project folder itself: it holds `node_modules` and `.patchstackrc.local.json`, which contains the site's API key. Publish `dist/` for a plain HTML site, or whatever the framework builds.
148
+ - **A deploy that builds on this machine** (`netlify deploy --prod --build`, a static folder uploaded by hand) has nothing that says "production" to Patchstack. Prefix that command, and only the production one, with `PATCHSTACK_ENVIRONMENT=production`:
149
+
150
+ ```
151
+ PATCHSTACK_ENVIRONMENT=production npx netlify deploy --prod --build
152
+ ```
153
+
154
+ The key is in `.patchstackrc.local.json` on this machine, so nothing else is needed. A preview deploy runs without the prefix.
155
+ - **A deploy that builds on the host** (`vercel --prod`, or any git-connected Netlify or Vercel site) labels production by itself, but the host never receives `.patchstackrc.local.json`. Before the first production deploy, put the key in the host's production settings, read straight from the file so it is never printed:
156
+
157
+ ```
158
+ node -p "require('./.patchstackrc.local.json').apiKey" | npx vercel env add PATCHSTACK_API_KEY production --sensitive
159
+ npx netlify env:set PATCHSTACK_API_KEY "$(node -p "require('./.patchstackrc.local.json').apiKey")" --context production --secret
160
+ ```
161
+
162
+ An app with a server needs this for runtime protection: the guard fetches its rules with the key. Without it the deploy is still recognised as long as the packages have not changed since the last scan, but nothing is protected. Never put the key in a committed file, in a public variable (`NEXT_PUBLIC_*`, `VITE_*`), or in your reply.
163
+ - **Check the live site after deploying**, and tell the person what you found:
164
+ - `curl -s <live url> | grep -ac __PATCHSTACK_PROD__` prints `1` or more. `0` means the build did not know it was production, and the widget will treat the live site as a preview.
165
+ - `curl -s -o /dev/null -w "%{http_code}" <live url>/.patchstackrc.local.json` is not `200`. A `200` means the API key was published: delete the deploy and tell the person.
166
+ - The owner reaches their dashboard on the live site by adding `#patchstack` to the address, for example `https://example.com/#patchstack`. Visitors never see the owner panels there.
167
+
136
168
  ## Automated setup
137
169
 
138
170
  1. **Install** (skip if already present), matching the project's package manager:
@@ -277,6 +309,8 @@ Handle it in this order:
277
309
 
278
310
  - `apiKey` (in `.patchstackrc.local.json`, which is git-ignored; 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.
279
311
 
312
+ A production build of a registered site that has **no** credential — a hosted builder's publish, which builds from the committed project and so never sees the git-ignored key file — cannot send its manifest. `scan` and `mark-build` then POST `monitor/pulse/build/<your site uuid>` instead, with no credential and exactly three fields: the manifest checksum, `environment: "production"`, and (`mark-build` only) the marker result. **No package names, no address, no name, no environment variable values.** Patchstack accepts it only when the checksum is the build it last scanned for this site, and shows the build as reported rather than deployed until the live page is seen running it. A publish that changed your packages is refused; set `PATCHSTACK_API_KEY` for that build.
313
+
280
314
  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; the git-ignored `.patchstackrc.local.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.
281
315
 
282
316
  A `pulseAuth` field is still honoured if a project has one, and `PATCHSTACK_PULSE_AUTH` still overrides it, for deployments that authenticate Pulse ingest with a different credential from block-logs. Do not add either yourself: they are unnecessary when the two share one credential, which is the default.
package/README.md CHANGED
@@ -262,7 +262,7 @@ group, and a verification that can leave a server running is worse than an unans
262
262
 
263
263
  No other command runs your application. `protect`, `protect --check`, `setup`, `guide`, `scan`,
264
264
  `status` and `mark-build` only read and write files, and — for `scan` and `mark-build` — report the
265
- dependency manifest they read.
265
+ dependency manifest they read, or only its checksum from a production build that has no API key.
266
266
 
267
267
  ## Configuration
268
268
 
@@ -310,7 +310,7 @@ The site UUID identifies the site and is **not** a secret — the Patchstack Con
310
310
 
311
311
  If it is ever lost, `npx @patchstack/connect login` recovers it — approval happens in the dashboard and rotates the credential.
312
312
 
313
- The credential's file is never committed, so CI needs `PATCHSTACK_API_KEY` in the environment (and `PATCHSTACK_SITE_UUID` too where `.patchstackrc.json` is also absent). Precedence is CLI flag → env var → `.patchstackrc.local.json` → `.patchstackrc.json`.
313
+ The credential's file is never committed, so CI needs `PATCHSTACK_API_KEY` in the environment (and `PATCHSTACK_SITE_UUID` too where `.patchstackrc.json` is also absent). A production build that has the site UUID but no key — a hosted builder's publish, typically — reports only that the build ran: it POSTs the manifest checksum to `monitor/pulse/build/<uuid>` without a credential and without the package list, and Patchstack accepts it only for the build it last scanned. Set the key there if the publish can change your packages. Precedence is CLI flag → env var → `.patchstackrc.local.json` → `.patchstackrc.json`.
314
314
 
315
315
  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.
316
316
 
package/dist/cli.js CHANGED
@@ -1876,11 +1876,79 @@ async function postManifestWithEnvironmentFallback(config, payload, marker = nul
1876
1876
  environmentUsed: config.environment
1877
1877
  };
1878
1878
  } catch (err) {
1879
+ if (err instanceof PatchstackError && err.code === "UNAUTHORIZED" && reportsBuildWithoutCredential(config)) {
1880
+ return {
1881
+ response: await postBuildReport(config, computeManifestChecksum(payload.packages), marker),
1882
+ environmentUsed: "production"
1883
+ };
1884
+ }
1879
1885
  if (config.environment !== "local" || !environmentRejected(err)) throw err;
1880
1886
  const fallback = { ...config, environment: "sandbox" };
1881
1887
  return { response: await postManifest(fallback, payload, marker), environmentUsed: "sandbox" };
1882
1888
  }
1883
1889
  }
1890
+ function reportsBuildWithoutCredential(config) {
1891
+ const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
1892
+ return !hasCredential && config.environment === "production" && config.siteUuid !== null && config.siteUuid !== "";
1893
+ }
1894
+ function buildReportUrl(manifestEndpoint, siteUuid) {
1895
+ const url = new URL(manifestEndpoint);
1896
+ const path17 = url.pathname.replace(/\/$/, "");
1897
+ const id = encodeURIComponent(siteUuid);
1898
+ url.pathname = path17.endsWith("/manifest") ? `${path17.slice(0, -"/manifest".length)}/build/${id}` : `/monitor/pulse/build/${id}`;
1899
+ url.search = "";
1900
+ url.hash = "";
1901
+ return url.toString();
1902
+ }
1903
+ async function postBuildReport(config, checksum, marker = null) {
1904
+ const siteUuid = config.siteUuid ?? "";
1905
+ const url = buildReportUrl(config.endpoint, siteUuid);
1906
+ assertConnectableEndpoint(config, url);
1907
+ const needsKey = authFailureMessage(401, config) ?? "Set PATCHSTACK_API_KEY where this build runs.";
1908
+ let response;
1909
+ try {
1910
+ response = await fetch(url, {
1911
+ method: "POST",
1912
+ headers: {
1913
+ "Content-Type": "application/json",
1914
+ Accept: "application/json",
1915
+ "User-Agent": "@patchstack/connect"
1916
+ },
1917
+ body: JSON.stringify({ checksum, environment: "production", ...marker !== null ? { marker } : {} }),
1918
+ signal: AbortSignal.timeout(config.timeoutMs)
1919
+ });
1920
+ } catch (cause) {
1921
+ throw new PatchstackError(
1922
+ isTimeoutError(cause) ? `Patchstack request to ${url} timed out after ${config.timeoutMs}ms. Override with PATCHSTACK_TIMEOUT_MS.` : `Could not reach Patchstack at ${url}. Check your network connection.`,
1923
+ isTimeoutError(cause) ? "NETWORK_TIMEOUT" : "NETWORK_ERROR",
1924
+ cause
1925
+ );
1926
+ }
1927
+ let parsed = null;
1928
+ try {
1929
+ const text = await readBoundedText(response);
1930
+ parsed = text.length > 0 ? JSON.parse(text) : null;
1931
+ } catch {
1932
+ parsed = null;
1933
+ }
1934
+ if (response.status === 422) {
1935
+ throw new PatchstackError(
1936
+ safeDisplayString(parsed?.error) ?? `This build is not the one Patchstack last scanned. ${needsKey}`,
1937
+ "UNAUTHORIZED"
1938
+ );
1939
+ }
1940
+ if (response.status < 200 || response.status >= 300) {
1941
+ throw new PatchstackError(needsKey, "UNAUTHORIZED");
1942
+ }
1943
+ const result = parsed?.result;
1944
+ return {
1945
+ uuid: siteUuid,
1946
+ stored: false,
1947
+ checksum,
1948
+ reason: "build-reported",
1949
+ message: result === "already-reported" ? "Patchstack already knew this build was published" : "Told Patchstack this build was published. There is no API key here, so the package list was not sent again"
1950
+ };
1951
+ }
1884
1952
  async function postManifest(config, payload, marker = null) {
1885
1953
  const url = buildEndpointUrl(config.endpoint, config.siteUuid);
1886
1954
  const timeoutMs = config.timeoutMs;
@@ -10463,6 +10531,8 @@ async function runScan(args, options = {}) {
10463
10531
  say(`Stored manifest #${response.manifest_id} (checksum ${response.checksum}).`);
10464
10532
  } else if (response.reason === "duplicate") {
10465
10533
  report.done.unshift(checked, "No changes since the last check");
10534
+ } else if (response.reason === "build-reported") {
10535
+ report.done.unshift(checked, response.message ?? "Reported this production build");
10466
10536
  } else {
10467
10537
  report.missing.push({
10468
10538
  text: "Patchstack did not save this check",
@@ -10508,7 +10578,7 @@ async function runScan(args, options = {}) {
10508
10578
  if (config.widget && effectiveUuid !== null && effectiveUuid.length > 0) {
10509
10579
  reportSourceWidget(effectiveUuid, shellFramework, report);
10510
10580
  }
10511
- const synced = effectiveUuid !== null && effectiveUuid.length > 0 && (response.stored || response.reason === "duplicate");
10581
+ const synced = effectiveUuid !== null && effectiveUuid.length > 0 && (response.stored || response.reason === "duplicate" || response.reason === "build-reported");
10512
10582
  const outcome = {
10513
10583
  connected,
10514
10584
  synced,