@patchstack/connect 0.3.21 → 0.3.23

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
@@ -73,7 +73,7 @@ This versioned reference ships inside `@patchstack/connect` and documents each s
73
73
  <script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="<SITE_UUID>" defer></script>
74
74
  ```
75
75
 
76
- 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. If the project must not carry the widget, persist `"widget": false` in `.patchstackrc.json`; otherwise the next scan re-adds it.
76
+ 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 `apiKey` (also `PATCHSTACK_API_KEY`, WP format `{secret}-{oauth.id}`) is the opposite: server-only, used to authenticate block-log reporting through the existing connector `POST /api/logs/log` so "Threats blocked" fills in the dashboard. Never put `apiKey` 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. 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.
77
77
 
78
78
  4. **Install and verify runtime protection:**
79
79
 
@@ -98,10 +98,19 @@ This versioned reference ships inside `@patchstack/connect` and documents each s
98
98
 
99
99
  ## Verifying the install
100
100
 
101
- - `npx @patchstack/connect status` re-prints the site UUID and dashboard URL.
101
+ - `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`).
102
102
  - `npx @patchstack/connect protect --check` verifies the runtime guard is connected to the request path.
103
103
  - Load the site in a browser — the "Report a vulnerability" button should appear.
104
104
 
105
+ ## Answering "is Patchstack installed?" / "is Patchstack removed?"
106
+
107
+ These are **two independent states** — never conflate them:
108
+
109
+ 1. **The site record on Patchstack** (remote). Deleting the site in the dashboard or through the widget's uninstall flow removes it. Reporting stops and the widget stops rendering, but nothing in the project changes.
110
+ 2. **The local integration** (this repo): the widget `<script>` tag, `.patchstackrc.json`, the `@patchstack/connect` dependency, the runtime guard files, and the build hooks.
111
+
112
+ 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`, the dependency) is still in the project; want me to remove it?"* — not "Patchstack is still installed."
113
+
105
114
  ## Uninstalling
106
115
 
107
116
  Remove only the pieces that are actually present — check for each first. If none are present, Patchstack isn't installed; report that and stop. If the user asked to remove only one piece (e.g. "just the widget"), remove only that piece.
@@ -111,7 +120,9 @@ Remove only the pieces that are actually present — check for each first. If no
111
120
  3. **Remove runtime protection before uninstalling the package.** Delete the connector-managed guard/rules files and remove only their managed imports, middleware registrations, tunnel code, and `#region patchstack…` blocks from the framework/server files. Preserve unrelated middleware and application code. Run `rg "patchstack|x-ps-target"` (or the available equivalent) afterwards and inspect every remaining source hit.
112
121
  4. **Remove the hooks from `package.json` scripts.** If a hook was chained (e.g. `"postbuild": "existing-command && patchstack-connect mark-build"`), remove only the `patchstack-connect …` part and keep the rest; if removal leaves a script empty, delete the key.
113
122
  5. **Uninstall the package** with the manager matching the lockfile: `npm uninstall` / `pnpm remove` / `yarn remove` / `bun remove` `@patchstack/connect`. Don't hand-edit `node_modules` or the lockfile.
114
- 6. **Delete `.patchstackrc.json`** and remove `PATCHSTACK_SITE_UUID` (and public-prefixed variants like `NEXT_PUBLIC_PATCHSTACK_SITE_UUID`) from env files and CI variables.
123
+ 6. **Delete `.patchstackrc.json`** and remove `PATCHSTACK_SITE_UUID`, `PATCHSTACK_API_KEY` (and public-prefixed variants like `NEXT_PUBLIC_PATCHSTACK_SITE_UUID`) from env files and CI variables.
115
124
  7. **Commit** the changes. Reporting stops immediately. The `window.__PATCHSTACK_PROD__` flag that `mark-build` injected lives only in build output, never in source — the next build simply won't contain it (rebuild if build output is committed).
116
125
 
117
126
  Local removal does not delete the site record on Patchstack's side. An unclaimed site is an anonymous record that stops receiving reports; a claimed site is removed by the user in their dashboard at https://app.patchstack.com. There is no CLI command for account-side deletion — do not invent one, and never attempt to authenticate or remove the site on the user's behalf.
127
+
128
+ The reverse also holds: removing the site on Patchstack's side (dashboard delete or the widget's uninstall flow) does not touch these local files — they must still be removed with the steps above. `npx @patchstack/connect status` shows `Site status: removed from Patchstack` in that state.
package/dist/cli.js CHANGED
@@ -946,6 +946,31 @@ function buildClaimUrl(endpoint, siteUuid) {
946
946
  const origin = new URL(endpoint).origin;
947
947
  return `${origin}/monitor/claim?site=${encodeURIComponent(siteUuid)}`;
948
948
  }
949
+ function buildSettingsUrl(endpoint, siteUuid) {
950
+ const origin = new URL(endpoint).origin;
951
+ return `${origin}/monitor/widget/settings/${encodeURIComponent(siteUuid)}`;
952
+ }
953
+ async function fetchSiteStatus(config) {
954
+ if (config.siteUuid === null) return "unknown";
955
+ const url = new URL(buildSettingsUrl(config.endpoint, config.siteUuid));
956
+ url.searchParams.set("t", Date.now().toString());
957
+ try {
958
+ const response = await fetch(url.toString(), {
959
+ method: "GET",
960
+ headers: {
961
+ Accept: "application/json",
962
+ "Cache-Control": "no-cache",
963
+ "User-Agent": "@patchstack/connect"
964
+ },
965
+ signal: AbortSignal.timeout(config.timeoutMs)
966
+ });
967
+ if (response.status === 404) return "removed";
968
+ if (response.ok) return "active";
969
+ return "unknown";
970
+ } catch {
971
+ return "unknown";
972
+ }
973
+ }
949
974
  async function postManifest(config, payload) {
950
975
  const url = buildEndpointUrl(config.endpoint, config.siteUuid);
951
976
  const timeoutMs = config.timeoutMs;
@@ -1247,8 +1272,10 @@ async function resolveConfig(options) {
1247
1272
  "CONFIG_MISSING"
1248
1273
  );
1249
1274
  }
1275
+ const apiKeyRaw = fromEnv.apiKey ?? fromFile.apiKey ?? null;
1250
1276
  return {
1251
1277
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
1278
+ apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
1252
1279
  endpoint,
1253
1280
  timeoutMs,
1254
1281
  environment,
@@ -1265,6 +1292,10 @@ async function persistSiteUuid(cwd, siteUuid) {
1265
1292
  const existing = await readConfigFile(cwd);
1266
1293
  return writeConfigFile(cwd, { ...existing, siteUuid });
1267
1294
  }
1295
+ async function persistApiKey(cwd, apiKey) {
1296
+ const existing = await readConfigFile(cwd);
1297
+ return writeConfigFile(cwd, { ...existing, apiKey });
1298
+ }
1268
1299
  async function readConfigFile(cwd) {
1269
1300
  const target = path6.join(cwd, CONFIG_FILENAME);
1270
1301
  let raw;
@@ -1306,6 +1337,7 @@ function readEnv() {
1306
1337
  const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
1307
1338
  return {
1308
1339
  siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
1340
+ apiKey: process.env.PATCHSTACK_API_KEY ?? void 0,
1309
1341
  endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
1310
1342
  timeoutMs,
1311
1343
  environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
@@ -2858,7 +2890,9 @@ Usage:
2858
2890
  Never runs the project build
2859
2891
  patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
2860
2892
  with an existing site UUID
2861
- patchstack-connect status [options] Show current configuration
2893
+ patchstack-connect status [options] Show current configuration and whether the
2894
+ site still exists on Patchstack (active /
2895
+ removed)
2862
2896
  patchstack-connect mark-build [options] Stamp built HTML with a production flag +
2863
2897
  build fingerprint, and ensure the widget
2864
2898
  tag in built pages (run as a postbuild step)
@@ -2899,6 +2933,9 @@ Options (for demo and demo-guide):
2899
2933
 
2900
2934
  Environment:
2901
2935
  PATCHSTACK_SITE_UUID Site UUID
2936
+ PATCHSTACK_API_KEY WP-format site API key for block-log reporting (never put in the widget)
2937
+ PATCHSTACK_TELEMETRY Set to off to disable block-log reporting
2938
+ PATCHSTACK_API_BASE API origin for /oauth/token and /api/logs/log (default: https://api.patchstack.com)
2902
2939
  PATCHSTACK_ENDPOINT API endpoint (default: https://api.patchstack.com/monitor/pulse/manifest)
2903
2940
  PATCHSTACK_TIMEOUT_MS Request timeout in ms (default: 30000)
2904
2941
  PATCHSTACK_ENVIRONMENT Manifest environment: production | sandbox (default: production)
@@ -3022,6 +3059,10 @@ async function runScan(args, options = {}) {
3022
3059
  const target = await persistSiteUuid(process.cwd(), response.uuid);
3023
3060
  console.log(`Provisioned site ${response.uuid}. Saved UUID to ${target}.`);
3024
3061
  }
3062
+ if (typeof response.api_key === "string" && response.api_key.length > 0) {
3063
+ const target = await persistApiKey(process.cwd(), response.api_key);
3064
+ console.log(`Saved API key to ${target} (for block-log reporting via /api/logs/log; keep out of the public widget).`);
3065
+ }
3025
3066
  if (response.stored) {
3026
3067
  console.log(`Stored manifest #${response.manifest_id} (checksum ${response.checksum}).`);
3027
3068
  } else if (response.reason === "duplicate") {
@@ -3290,6 +3331,26 @@ async function runStatus(args) {
3290
3331
  console.log(`Environment: ${config.environment}`);
3291
3332
  if (config.siteUuid !== null) {
3292
3333
  console.log(`Dashboard URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
3334
+ switch (await fetchSiteStatus(config)) {
3335
+ case "active":
3336
+ console.log("Site status: active on Patchstack");
3337
+ break;
3338
+ case "removed":
3339
+ console.log("Site status: removed from Patchstack");
3340
+ console.log(
3341
+ " The site record no longer exists (deleted from the dashboard or via the"
3342
+ );
3343
+ console.log(
3344
+ " widget uninstall flow). The local integration files are still in this"
3345
+ );
3346
+ console.log(
3347
+ ' project \u2014 see "Uninstalling" in AGENT-INSTALL.md to remove them.'
3348
+ );
3349
+ break;
3350
+ case "unknown":
3351
+ console.log("Site status: could not be verified (Patchstack unreachable)");
3352
+ break;
3353
+ }
3293
3354
  }
3294
3355
  return 0;
3295
3356
  }