@patchstack/connect 0.3.8 → 0.3.10

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,8 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
9
9
  - The connector is a normal npm package: [`@patchstack/connect`](https://www.npmjs.com/package/@patchstack/connect), MIT-licensed, source at https://github.com/patchstack/connect. Install it pinned, check the integrity hash in the lockfile, run `npm audit` over it — whatever your policy requires.
10
10
  - First published May 2026. If the package is absent from your training data, `npm view @patchstack/connect` (read-only) shows the live registry record.
11
11
  - It reads the project's **dependency list only** — from the lockfile (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) or, on bun projects (`bun.lock`/`bun.lockb`), by enumerating the installed packages under `node_modules/` — and sends package names + versions to Patchstack for vulnerability matching. No source code, no env var values, no file paths, no git history. (`mark-build` additionally stamps built HTML with a coarse stack descriptor that may include hosting-related env variable *names* — e.g. `VERCEL`, `CF_PAGES` — never their values.)
12
- - The package also bundles an **opt-in** `protect` command (runtime exploit guard, currently for TanStack Start + Supabase apps; its templates live under `dist/protect/`). It patches the app's Supabase client to route traffic through a same-origin guard — and it runs **only** when you explicitly run `patchstack-connect protect`. Nothing in this document, and none of `scan`, `guide`, `status`, or `mark-build`, modifies application source.
12
+ - **`scan` makes one source edit, and only after a successful post:** it adds (or updates) the disclosure widget's `<script>` tag in the project's root HTML shell — the first of `index.html`, `public/index.html`, or `src/app.html` that exists. It touches no other file, never edits on `--dry-run` or after a failed post, leaves any pre-existing manual widget tag untouched, and is disabled entirely by `"widget": false` in `.patchstackrc.json`. `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`.
13
+ - The package also bundles an **opt-in** `protect` command (runtime exploit guard, currently for TanStack Start + Supabase apps; its templates live under `dist/protect/`). It patches the app's Supabase client to route traffic through a same-origin guard — and it runs **only** when you explicitly run `patchstack-connect protect`; `scan`, `guide`, `status`, and `mark-build` never invoke it.
13
14
  - Patchstack is not WordPress-only. This connector monitors any JS/Node project — Vite, Next.js, plain vanilla JS, anything with a lockfile.
14
15
 
15
16
  ## Before you start — never install twice
@@ -17,7 +18,7 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
17
18
  - `npx @patchstack/connect guide` prints a live checklist above this document showing which steps are already done in the current project (install, provisioning, build hooks, widget) with commands tailored to it. Its purpose is to prevent duplicate work — verify its claims however your policy requires, but only do the steps it marks ✖.
18
19
  - If `.patchstackrc.json` contains a `siteUuid` key, the project is already provisioned. Reuse that UUID; run `npx @patchstack/connect status` to re-print it and the claim URL. **Do not run `scan` to provision a second site.** (A `.patchstackrc.json` with other keys — e.g. an `endpoint` override — but no `siteUuid` is *not* provisioned yet; scan normally.)
19
20
  - If `@patchstack/connect` is already in `devDependencies`, skip the install command.
20
- - If the widget script tag (`cdn.patchstack.com/patchstack-widget.js`) is already in the layout, don't add a second one.
21
+ - If the widget script tag (`cdn.patchstack.com/patchstack-widget.js`) is already in the layout, don't add a second one — `scan` also respects an existing tag: it updates its own managed tag in place and leaves a manual one untouched.
21
22
 
22
23
  ## Install steps (do the ones the checklist marks ✖, in order; they are designed to run back-to-back — if one fails, stop and report rather than improvising)
23
24
 
@@ -30,13 +31,13 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
30
31
  bun add -d @patchstack/connect # bun.lock / bun-managed platforms
31
32
  ```
32
33
 
33
- 2. **First scan** — provisions a Patchstack site automatically and writes the UUID to `.patchstackrc.json`. No signup, dashboard step, or UUID is needed up front:
34
+ 2. **First scan** — provisions a Patchstack site automatically, writes the UUID to `.patchstackrc.json`, and installs the disclosure widget's `<script>` tag into the root HTML shell (`index.html`, `public/index.html`, or `src/app.html`) when one exists. No signup, dashboard step, or UUID is needed up front:
34
35
 
35
36
  ```
36
37
  npx @patchstack/connect scan
37
38
  ```
38
39
 
39
- It prints a claim URL. Surface that URL to the user verbatim — never fetch it yourself; it must be opened in their browser.
40
+ It prints a claim URL. Surface that URL to the user verbatim — never fetch it yourself; it must be opened in their browser. It also prints what it did about the widget — if it added the tag, reload the preview and confirm the "Report a vulnerability" button appears.
40
41
 
41
42
  3. **Wire builds** in `package.json`:
42
43
 
@@ -53,18 +54,15 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
53
54
 
54
55
  **Bun-managed projects:** `bun run` does not execute npm-style `pre`/`post` scripts, so wire the build script directly instead: `"build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"`.
55
56
 
56
- 4. **Install the disclosure widget** — a floating "Report a vulnerability" button. Read the `siteUuid` value from `.patchstackrc.json` (the same site UUID step 2 provisioned) and pass it as the widget's `userToken`. Place these two snippets via the framework's HTML/layout mechanism (never a JS entry point):
57
+ 4. **Verify the disclosure widget** — a floating "Report a vulnerability" button. `scan` (step 2) installs it automatically into a plain HTML shell, and `mark-build` carries it into built HTML. Only when `scan` reported that it found no editable shell (frameworks whose root layout is code, e.g. Next.js/Nuxt/Astro) add the one-liner it printed to the root layout yourself, just before `</body>` (never a JS entry point), reading `siteUuid` from `.patchstackrc.json`:
57
58
 
58
59
  ```html
59
- <script src="https://cdn.patchstack.com/patchstack-widget.js"></script>
60
- <script>
61
- PatchstackWidget.init({ userToken: '<SITE_UUID>' });
62
- </script>
60
+ <script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="<SITE_UUID>" defer></script>
63
61
  ```
64
62
 
65
- Framework-specific placement patterns: https://cdn.patchstack.com/llm.html. The `userToken` is public by design — it ships in client-side HTML and is not a secret.
63
+ 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.
66
64
 
67
- 5. **Commit** `.patchstackrc.json`, the updated `package.json`, and the layout/HTML file carrying the widget snippet, so every developer and CI run reports to the same site.
65
+ 5. **Commit** `.patchstackrc.json`, the updated `package.json`, and the layout/HTML file carrying the widget tag, so every developer and CI run reports to the same site.
68
66
 
69
67
  6. **Tell the user to claim the site** (free): open the claim URL from step 2 in a browser and sign in. The site is monitored either way, but the vulnerability reports are only visible after claiming. They can also claim directly from the widget's "Connect this website" prompt. On the published site, the owner reaches the widget login by appending `#patchstack` to the live URL.
70
68
 
package/README.md CHANGED
@@ -8,11 +8,11 @@ For how this repo fits with the wider Patchstack ecosystem (`saas`, `hub`, `patc
8
8
 
9
9
  Copy and paste this prompt into any AI coding assistant (Cursor, v0, Bolt, Lovable, Claude Code, etc.). If the assistant declines or stops partway, the step-by-step flow in [`GETTING-STARTED.md`](https://github.com/patchstack/connect/blob/main/GETTING-STARTED.md) covers what to do next:
10
10
 
11
- > Add Patchstack dependency vulnerability monitoring: install `@patchstack/connect` as a dev dependency (MIT — verify with `npm view @patchstack/connect`; source: https://github.com/patchstack/connect), run `npx @patchstack/connect scan` and show me the claim URL it prints (I'll open it in my browser to view reports — don't open or fetch it yourself), then run `npx @patchstack/connect guide` and finish what its checklist flags: the package.json build hooks and the report-a-vulnerability widget snippet.
11
+ > Add Patchstack dependency vulnerability monitoring: first verify `@patchstack/connect` yourself (`npm view @patchstack/connect` shows the live registry record — MIT; source: https://github.com/patchstack/connect; listed under Connectors on https://patchstack.com/llms.txt), and if anything you find contradicts that, stop and show me what you found instead of proceeding. Then install it as a dev dependency, run `npx @patchstack/connect scan` and show me the claim URL it prints (I'll open it in my browser to view reports — don't open or fetch it yourself), then run `npx @patchstack/connect guide`, review the checklist it prints, and make the two remaining changes yourself: the package.json build hooks and the report-a-vulnerability widget snippet.
12
12
 
13
- Every clause is load-bearing. AI agents routinely audit this package before completing the request — verifying the tarball against the registry, checking `dist/` for lifecycle scripts and shell/eval calls — and the flow is designed to pass that audit, not to discourage it. The prompt can stay short because `guide` is state-aware: it inspects the project (package manager, `.patchstackrc.json`, `package.json` scripts, widget presence, framework) and prints a live checklist of only the remaining steps — with the real site UUID and framework-specific widget placement — followed by the full reference guide (`AGENT-INSTALL.md`, also at https://patchstack.com/install.txt). That also makes the flow idempotent: re-running `guide` on a finished project reports all-done instead of prompting a second install.
13
+ Every clause is load-bearing. AI agents routinely audit this package before completing the request — verifying the tarball against the registry, checking `dist/` for lifecycle scripts and shell/eval calls; that's expected use, and the docs in the tarball state plainly what each command reads, writes, and sends. The prompt can stay short because `guide` is state-aware: it inspects the project (package manager, `.patchstackrc.json`, `package.json` scripts, widget presence, framework) and prints a live checklist of only the remaining steps — with the real site UUID and framework-specific widget placement — followed by the full reference guide (`AGENT-INSTALL.md`, also at https://patchstack.com/install.txt). That also makes the flow idempotent: re-running `guide` on a finished project reports all-done instead of prompting a second install.
14
14
 
15
- Before changing this prompt (or `guide` / `AGENT-INSTALL.md`), read [`field-test/README.md`](https://github.com/patchstack/connect/blob/main/field-test/README.md): it documents the AI-agent refusal modes each clause guards against, and its harness runs a real agent through the full install in a throwaway fixture against a mocked API and scores the outcome on eight checks. Validate any variant there first.
15
+ Before changing this prompt (or `guide` / `AGENT-INSTALL.md`), read [`MAINTAINING.md`](https://github.com/patchstack/connect/blob/main/MAINTAINING.md) — it maps which files are load-bearing and how to ship a change safely. The [`field-test/`](https://github.com/patchstack/connect/blob/main/field-test/README.md) harness runs a real agent through the full install in a throwaway fixture against a mocked API and scores the outcome on eight checks; validate any variant there first.
16
16
 
17
17
  ## Quick start (zero configuration)
18
18
 
@@ -29,7 +29,8 @@ That's it. The first `scan`:
29
29
  2. POSTs the package list to Patchstack with **no** UUID.
30
30
  3. Patchstack provisions a fresh site and returns its UUID.
31
31
  4. The connector writes the UUID to `.patchstackrc.json` so the next `scan` targets the same site.
32
- 5. The connector prints a claim URL — open it in a browser to attach the new site to your Patchstack account. You can re-display it any time with `npx @patchstack/connect status`.
32
+ 5. The connector installs the disclosure widget's `<script>` tag into your root HTML shell (see *The disclosure widget* below) so the "Report a vulnerability" button shows up on the next preview reload.
33
+ 6. The connector prints a claim URL — open it in a browser to attach the new site to your Patchstack account. You can re-display it any time with `npx @patchstack/connect status`.
33
34
 
34
35
  Then wire it into builds:
35
36
 
@@ -57,12 +58,16 @@ npx @patchstack/connect scan
57
58
  ```
58
59
  patchstack-connect scan [options] Scan the lockfile and POST to Patchstack.
59
60
  If no UUID is configured the server provisions
60
- one and the connector persists it.
61
+ one and the connector persists it. After a
62
+ successful post, adds/updates the disclosure
63
+ widget tag in the root HTML shell (opt out
64
+ with "widget": false in .patchstackrc.json)
61
65
  patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json with
62
66
  an existing site UUID
63
67
  patchstack-connect status [options] Show current configuration
64
68
  patchstack-connect mark-build [options] Stamp built HTML with a production flag +
65
- build fingerprint (run as a postbuild step)
69
+ build fingerprint and ensure the widget tag
70
+ in built pages (run as a postbuild step)
66
71
  patchstack-connect guide Show this project's setup status (what's done,
67
72
  what's missing, with tailored commands), then
68
73
  print the full setup guide
@@ -97,11 +102,30 @@ Environment variables:
97
102
 
98
103
  ```json
99
104
  {
100
- "siteUuid": "550e8400-e29b-41d4-a716-446655440000"
105
+ "siteUuid": "550e8400-e29b-41d4-a716-446655440000",
106
+ "widget": true
101
107
  }
102
108
  ```
103
109
 
104
- The site UUID identifies the site; it is not a secret — the disclosure widget ships the same UUID in client-side HTML as its `userToken`, and committing `.patchstackrc.json` is the intended workflow so every developer and CI run reports to the same site. Possession of the UUID lets someone submit dependency manifests for that site (noise, not data access). In CI setups where the file isn't committed, set `PATCHSTACK_SITE_UUID` instead.
110
+ `"widget"` is optional and defaults to `true`; set it to `false` to stop the connector from managing the disclosure-widget tag (see *The disclosure widget*).
111
+
112
+ The site UUID identifies the site; it is not a secret — the disclosure widget ships the same UUID in client-side HTML, and committing `.patchstackrc.json` is the intended workflow so every developer and CI run reports to the same site. Possession of the UUID lets someone submit dependency manifests for that site (noise, not data access). In CI setups where the file isn't committed, set `PATCHSTACK_SITE_UUID` instead.
113
+
114
+ ## The disclosure widget
115
+
116
+ The widget is a floating "Report a vulnerability" button — a disclosure channel for anyone who spots a bug on the site. The connector manages its install so the UUID never has to be copied by hand:
117
+
118
+ - **`scan`** (after a successful post) adds this managed tag to the first root HTML shell it finds — `index.html`, `public/index.html`, or `src/app.html` — immediately before `</body>`:
119
+
120
+ ```html
121
+ <script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="<SITE_UUID>" defer data-patchstack-connect-widget="true"></script>
122
+ ```
123
+
124
+ Re-runs update the tag in place (the `data-patchstack-connect-widget` attribute marks it as connector-managed); a pre-existing manual widget tag is left untouched. `--dry-run` and failed posts never edit anything. Projects whose root layout is code rather than HTML (Next.js, Nuxt, Astro, …) get the exact snippet and target file printed instead — `guide` shows framework-specific placement.
125
+
126
+ - **`mark-build`** ensures the same tag in built HTML output, covering builds whose source shell the connector couldn't edit, and stamps `window.__PATCHSTACK_PROD__` so the widget hides the claim/login UI on the published site (owners reach it by appending `#patchstack` to the live URL).
127
+
128
+ - **Opting out:** persist `"widget": false` in `.patchstackrc.json` to disable both passes (dependency scanning only). Without it, the next successful scan re-adds the managed tag.
105
129
 
106
130
  ## Programmatic API
107
131
 
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
5
5
 
6
6
  // src/parsers/index.ts
7
7
  import { access } from "fs/promises";
@@ -1039,7 +1039,8 @@ async function resolveConfig(options) {
1039
1039
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
1040
1040
  endpoint,
1041
1041
  timeoutMs,
1042
- environment
1042
+ environment,
1043
+ widget: fromFile.widget !== false
1043
1044
  };
1044
1045
  }
1045
1046
  async function writeConfigFile(cwd, config) {
@@ -1251,10 +1252,71 @@ function injectMarker(html, snippet) {
1251
1252
  }
1252
1253
 
1253
1254
  // src/guide.ts
1254
- import { existsSync as existsSync2, readFileSync, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1255
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1256
+ import path9 from "path";
1257
+
1258
+ // src/widget.ts
1259
+ import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
1255
1260
  import path8 from "path";
1256
1261
  var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
1262
+ var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
1257
1263
  var WIDGET_NEEDLE = "patchstack-widget";
1264
+ var SOURCE_SHELL_CANDIDATES = ["index.html", "public/index.html", "src/app.html"];
1265
+ function buildWidgetTag(siteUuid) {
1266
+ return `<script src="${WIDGET_SCRIPT_URL}" data-site-uuid="${siteUuid}" defer ${WIDGET_MARKER_ATTR}="true"></script>`;
1267
+ }
1268
+ var MANAGED_TAG_RE = new RegExp(
1269
+ `<script[^>]*${WIDGET_MARKER_ATTR}[^>]*>\\s*</script>`,
1270
+ "i"
1271
+ );
1272
+ function ensureWidgetInHtml(html, siteUuid) {
1273
+ const tag = buildWidgetTag(siteUuid);
1274
+ const managed = html.match(MANAGED_TAG_RE);
1275
+ if (managed !== null) {
1276
+ if (managed[0].includes(`data-site-uuid="${siteUuid}"`)) {
1277
+ return { html, action: "unchanged" };
1278
+ }
1279
+ return { html: html.replace(MANAGED_TAG_RE, tag), action: "updated" };
1280
+ }
1281
+ if (html.includes(WIDGET_NEEDLE)) {
1282
+ return { html, action: "manual" };
1283
+ }
1284
+ const bodyClose = html.match(/([ \t]*)<\/body>/i);
1285
+ if (bodyClose === null || bodyClose.index === void 0) {
1286
+ return { html, action: "no-body" };
1287
+ }
1288
+ const indent = bodyClose[1] ?? "";
1289
+ const insertion = `${indent} ${tag}
1290
+ ${indent}</body>`;
1291
+ return {
1292
+ html: html.slice(0, bodyClose.index) + insertion + html.slice(bodyClose.index + bodyClose[0].length),
1293
+ action: "added"
1294
+ };
1295
+ }
1296
+ function findSourceShell(cwd) {
1297
+ for (const candidate of SOURCE_SHELL_CANDIDATES) {
1298
+ if (existsSync2(path8.join(cwd, candidate))) {
1299
+ return candidate;
1300
+ }
1301
+ }
1302
+ return null;
1303
+ }
1304
+ function ensureSourceWidget(cwd, siteUuid) {
1305
+ const shell = findSourceShell(cwd);
1306
+ if (shell === null) {
1307
+ return { shell: null, action: "no-shell" };
1308
+ }
1309
+ const file = path8.join(cwd, shell);
1310
+ const before = readFileSync(file, "utf8");
1311
+ const { html, action } = ensureWidgetInHtml(before, siteUuid);
1312
+ if (html !== before) {
1313
+ writeFileSync(file, html);
1314
+ }
1315
+ return { shell, action };
1316
+ }
1317
+
1318
+ // src/guide.ts
1319
+ var WIDGET_NEEDLE2 = "patchstack-widget";
1258
1320
  var INSTALL_COMMANDS = {
1259
1321
  npm: "npm install --save-dev @patchstack/connect",
1260
1322
  pnpm: "pnpm add -D @patchstack/connect",
@@ -1322,7 +1384,7 @@ var WIDGET_SCAN_MAX_DEPTH = 6;
1322
1384
  var WIDGET_SCAN_MAX_BYTES = 512 * 1024;
1323
1385
  function detectPackageManager(cwd) {
1324
1386
  for (const { filename, pm } of PM_BY_LOCKFILE) {
1325
- if (existsSync2(path8.join(cwd, filename))) {
1387
+ if (existsSync3(path9.join(cwd, filename))) {
1326
1388
  return pm;
1327
1389
  }
1328
1390
  }
@@ -1333,7 +1395,7 @@ function installCommand(pm) {
1333
1395
  }
1334
1396
  function readPackageJson(cwd) {
1335
1397
  try {
1336
- return JSON.parse(readFileSync(path8.join(cwd, "package.json"), "utf8"));
1398
+ return JSON.parse(readFileSync2(path9.join(cwd, "package.json"), "utf8"));
1337
1399
  } catch {
1338
1400
  return null;
1339
1401
  }
@@ -1341,7 +1403,7 @@ function readPackageJson(cwd) {
1341
1403
  function installedVersion(cwd, declaredRange) {
1342
1404
  try {
1343
1405
  const pkg = JSON.parse(
1344
- readFileSync(path8.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
1406
+ readFileSync2(path9.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
1345
1407
  );
1346
1408
  if (typeof pkg.version === "string" && pkg.version.length > 0) {
1347
1409
  return pkg.version;
@@ -1369,7 +1431,7 @@ function findWidgetMarker(cwd, siteUuid) {
1369
1431
  if (budget <= 0 || settled()) {
1370
1432
  return;
1371
1433
  }
1372
- const full = path8.join(dir, entry.name);
1434
+ const full = path9.join(dir, entry.name);
1373
1435
  if (entry.isDirectory()) {
1374
1436
  if (SKIPPED_DIRS.has(entry.name) || entry.name.startsWith(".")) {
1375
1437
  continue;
@@ -1377,7 +1439,7 @@ function findWidgetMarker(cwd, siteUuid) {
1377
1439
  walk2(full, depth + 1);
1378
1440
  continue;
1379
1441
  }
1380
- if (!entry.isFile() || !WIDGET_EXTENSIONS.has(path8.extname(entry.name).toLowerCase())) {
1442
+ if (!entry.isFile() || !WIDGET_EXTENSIONS.has(path9.extname(entry.name).toLowerCase())) {
1381
1443
  continue;
1382
1444
  }
1383
1445
  budget -= 1;
@@ -1385,8 +1447,8 @@ function findWidgetMarker(cwd, siteUuid) {
1385
1447
  if (statSync2(full).size > WIDGET_SCAN_MAX_BYTES) {
1386
1448
  continue;
1387
1449
  }
1388
- const content = readFileSync(full, "utf8");
1389
- if (!content.includes(WIDGET_NEEDLE)) {
1450
+ const content = readFileSync2(full, "utf8");
1451
+ if (!content.includes(WIDGET_NEEDLE2)) {
1390
1452
  continue;
1391
1453
  }
1392
1454
  sawWidget = true;
@@ -1409,7 +1471,7 @@ function resolveWidgetFileHint(cwd, framework) {
1409
1471
  ...GENERIC_WIDGET_FILES
1410
1472
  ];
1411
1473
  for (const candidate of candidates) {
1412
- if (existsSync2(path8.join(cwd, candidate))) {
1474
+ if (existsSync3(path9.join(cwd, candidate))) {
1413
1475
  return candidate;
1414
1476
  }
1415
1477
  }
@@ -1433,6 +1495,7 @@ async function collectGuideState(cwd) {
1433
1495
  let siteUuid = null;
1434
1496
  let claimUrl = null;
1435
1497
  let endpointOverride = null;
1498
+ let widgetOptOut = false;
1436
1499
  try {
1437
1500
  const config = await resolveConfig({ cwd });
1438
1501
  siteUuid = config.siteUuid;
@@ -1442,6 +1505,7 @@ async function collectGuideState(cwd) {
1442
1505
  if (config.endpoint !== DEFAULT_ENDPOINT) {
1443
1506
  endpointOverride = config.endpoint;
1444
1507
  }
1508
+ widgetOptOut = !config.widget;
1445
1509
  } catch {
1446
1510
  }
1447
1511
  const declaredNames = Object.keys({ ...pkg?.dependencies, ...pkg?.devDependencies });
@@ -1464,6 +1528,7 @@ async function collectGuideState(cwd) {
1464
1528
  postbuildWired: (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build") || (pkg?.scripts?.build ?? "").includes("patchstack-connect mark-build"),
1465
1529
  widgetInstalled: widget.found,
1466
1530
  widgetTokenMatches: widget.uuidMatches,
1531
+ widgetOptOut,
1467
1532
  framework: stack.framework,
1468
1533
  widgetFileHint: resolveWidgetFileHint(cwd, stack.framework)
1469
1534
  };
@@ -1481,7 +1546,7 @@ function countRemainingSteps(state) {
1481
1546
  state.installed !== null,
1482
1547
  state.siteUuid !== null,
1483
1548
  state.prebuildWired && state.postbuildWired,
1484
- state.widgetInstalled && state.widgetTokenMatches !== false
1549
+ state.widgetOptOut || state.widgetInstalled && state.widgetTokenMatches !== false
1485
1550
  ].filter((step) => !step).length;
1486
1551
  }
1487
1552
  function renderGuideChecklist(state, useColor) {
@@ -1493,7 +1558,7 @@ function renderGuideChecklist(state, useColor) {
1493
1558
  const headerParts = [state.framework, state.packageManager].filter(
1494
1559
  (part) => part !== null
1495
1560
  );
1496
- const name = state.projectName ?? path8.basename(process.cwd());
1561
+ const name = state.projectName ?? path9.basename(process.cwd());
1497
1562
  lines.push(paint(ANSI.bold, `Patchstack setup status \u2014 ${name} (${headerParts.join(" \xB7 ")})`));
1498
1563
  if (state.endpointOverride !== null) {
1499
1564
  lines.push(
@@ -1540,19 +1605,24 @@ function renderGuideChecklist(state, useColor) {
1540
1605
  }
1541
1606
  }
1542
1607
  const widgetOk = state.widgetInstalled && state.widgetTokenMatches !== false;
1543
- if (widgetOk) {
1608
+ if (state.widgetOptOut && !widgetOk) {
1609
+ lines.push(done('Disclosure widget disabled by config ("widget": false in .patchstackrc.json)'));
1610
+ } else if (widgetOk) {
1544
1611
  lines.push(done("Disclosure widget installed"));
1545
1612
  } else if (state.widgetInstalled) {
1546
- lines.push(todo("Disclosure widget found, but its userToken doesn't match this project's site UUID"));
1547
- lines.push(detail(`\u2192 a wrong userToken makes the widget silently no-op; set it to '${state.siteUuid}'`));
1613
+ lines.push(todo("Disclosure widget found, but its site UUID doesn't match this project's"));
1614
+ lines.push(detail(`\u2192 a wrong site UUID makes the widget silently no-op; set data-site-uuid (or userToken) to '${state.siteUuid}'`));
1615
+ } else if (state.siteUuid === null) {
1616
+ lines.push(todo('Add the "Report a vulnerability" widget \u2014 the first scan does this for you'));
1617
+ lines.push(detail("\u2192 `npx @patchstack/connect scan` provisions the site and adds the widget tag"));
1618
+ lines.push(detail(" to the root HTML shell (index.html / public/index.html / src/app.html)."));
1548
1619
  } else {
1549
1620
  lines.push(todo('Add the "Report a vulnerability" widget'));
1550
- const placement = state.widgetFileHint !== null ? `\u2192 add to ${state.widgetFileHint}, just before </body> (via the framework's HTML/layout mechanism):` : "\u2192 add just before </body> via the framework's HTML/layout mechanism (never a JS entry point):";
1621
+ lines.push(detail("\u2192 a normal `scan` adds this tag to a plain HTML shell automatically;"));
1622
+ const placement = state.widgetFileHint !== null ? ` for this project put it in ${state.widgetFileHint}, just before </body>:` : " put it just before </body> via the framework's HTML/layout mechanism (never a JS entry point):";
1551
1623
  lines.push(detail(placement));
1552
- lines.push(detail(` <script src="${WIDGET_SCRIPT_URL}"></script>`));
1553
- const token = state.siteUuid ?? "<SITE_UUID from .patchstackrc.json \u2014 run scan first>";
1554
- lines.push(detail(` <script>PatchstackWidget.init({ userToken: '${token}' });</script>`));
1555
- lines.push(detail("The userToken is public by design \u2014 it ships in client-side HTML."));
1624
+ lines.push(detail(` ${buildWidgetTag(state.siteUuid)}`));
1625
+ lines.push(detail("The site UUID is public by design \u2014 it ships in client-side HTML."));
1556
1626
  }
1557
1627
  lines.push("");
1558
1628
  if (state.claimUrl !== null) {
@@ -1588,13 +1658,13 @@ function renderGuideChecklist(state, useColor) {
1588
1658
  }
1589
1659
 
1590
1660
  // src/protect/install.ts
1591
- import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, mkdirSync, copyFileSync } from "fs";
1661
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync, copyFileSync } from "fs";
1592
1662
  import { join, dirname } from "path";
1593
1663
  import { fileURLToPath } from "url";
1594
1664
  var TEMPLATES = join(dirname(fileURLToPath(import.meta.url)), "protect", "templates");
1595
1665
  var APP = process.cwd();
1596
1666
  var PS_DIR = join(APP, "src/integrations/patchstack");
1597
- var read = (p) => readFileSync2(p, "utf8");
1667
+ var read = (p) => readFileSync3(p, "utf8");
1598
1668
  var log = (msg) => console.log(`patchstack protect: ${msg}`);
1599
1669
  var CLIENT_TUNNEL = [
1600
1670
  "",
@@ -1639,10 +1709,10 @@ var FUNCTION_MIDDLEWARE_DEF = [
1639
1709
  ].join("\n");
1640
1710
  function detectSupportedStack(cwd) {
1641
1711
  const pkgPath = join(cwd, "package.json");
1642
- if (!existsSync3(pkgPath)) return false;
1712
+ if (!existsSync4(pkgPath)) return false;
1643
1713
  const pkg = JSON.parse(read(pkgPath));
1644
1714
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1645
- return Boolean(deps["@tanstack/react-start"]) && existsSync3(join(cwd, "src/start.ts")) && existsSync3(join(cwd, "src/integrations/supabase/client.ts"));
1715
+ return Boolean(deps["@tanstack/react-start"]) && existsSync4(join(cwd, "src/start.ts")) && existsSync4(join(cwd, "src/integrations/supabase/client.ts"));
1646
1716
  }
1647
1717
  function scaffold(cwd) {
1648
1718
  const dst = join(cwd, "src/integrations/patchstack");
@@ -1657,7 +1727,7 @@ function patchClient(cwd) {
1657
1727
  if (s.includes("x-ps-target")) return log("client.ts already wired");
1658
1728
  const anchor = "headers.set('apikey', supabaseKey);";
1659
1729
  if (!s.includes(anchor)) return log("client.ts anchor not found \u2014 skipping (template changed?)");
1660
- writeFileSync(p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL));
1730
+ writeFileSync2(p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL));
1661
1731
  log("patched client.ts (tunnel Supabase through the guard)");
1662
1732
  }
1663
1733
  function patchStart(cwd) {
@@ -1696,7 +1766,7 @@ function patchStart(cwd) {
1696
1766
  }
1697
1767
  }
1698
1768
  if (s === original) return log("start.ts already wired");
1699
- writeFileSync(p, s);
1769
+ writeFileSync2(p, s);
1700
1770
  log("patched start.ts (guard registered as request + function middleware)");
1701
1771
  }
1702
1772
  function runProtect(cwd) {
@@ -1716,12 +1786,18 @@ var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to
1716
1786
  Usage:
1717
1787
  patchstack-connect scan [options] Scan lockfile and POST to Patchstack.
1718
1788
  If no UUID is configured, the server
1719
- provisions one and we persist it.
1789
+ provisions one and we persist it. After a
1790
+ successful post it also adds/updates the
1791
+ disclosure-widget <script> tag in the root
1792
+ HTML shell (index.html, public/index.html,
1793
+ or src/app.html) \u2014 opt out with
1794
+ "widget": false in .patchstackrc.json
1720
1795
  patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
1721
1796
  with an existing site UUID
1722
1797
  patchstack-connect status [options] Show current configuration
1723
1798
  patchstack-connect mark-build [options] Stamp built HTML with a production flag +
1724
- build fingerprint (run as a postbuild step)
1799
+ build fingerprint, and ensure the widget
1800
+ tag in built pages (run as a postbuild step)
1725
1801
  patchstack-connect protect Install always-on runtime protection (the
1726
1802
  guard) into a TanStack Start + Supabase app.
1727
1803
  Covers the browser + server-function paths.
@@ -1868,6 +1944,10 @@ async function runScan(args) {
1868
1944
  } else {
1869
1945
  console.log(`Server response: ${response.message ?? JSON.stringify(response)}`);
1870
1946
  }
1947
+ const effectiveUuid = config.siteUuid ?? response.uuid ?? null;
1948
+ if (config.widget && effectiveUuid !== null && effectiveUuid.length > 0) {
1949
+ reportSourceWidget(effectiveUuid);
1950
+ }
1871
1951
  if (provisioning && response.uuid !== void 0 && response.uuid.length > 0) {
1872
1952
  console.log("");
1873
1953
  console.log("Claim this site to view vulnerability reports in your Patchstack dashboard:");
@@ -1878,6 +1958,39 @@ async function runScan(args) {
1878
1958
  }
1879
1959
  return 0;
1880
1960
  }
1961
+ function reportSourceWidget(siteUuid) {
1962
+ try {
1963
+ const result = ensureSourceWidget(process.cwd(), siteUuid);
1964
+ switch (result.action) {
1965
+ case "added":
1966
+ console.log(`Widget: added the "Report a vulnerability" tag to ${result.shell}. Reload your preview to see it.`);
1967
+ break;
1968
+ case "updated":
1969
+ console.log(`Widget: updated the managed tag in ${result.shell} to site ${siteUuid}.`);
1970
+ break;
1971
+ case "unchanged":
1972
+ console.log(`Widget: already installed in ${result.shell}.`);
1973
+ break;
1974
+ case "manual":
1975
+ console.log(`Widget: found an existing (manual) install in ${result.shell} \u2014 left untouched.`);
1976
+ break;
1977
+ case "no-body":
1978
+ console.log(`Widget: ${result.shell} has no </body> tag to anchor on. Add this tag to your root layout manually:`);
1979
+ console.log(` ${buildWidgetTag(siteUuid)}`);
1980
+ break;
1981
+ case "no-shell":
1982
+ console.log("Widget: no plain HTML shell found (index.html / public/index.html / src/app.html).");
1983
+ console.log("Add this tag to your root layout before </body> (run `guide` for framework-specific placement):");
1984
+ console.log(` ${buildWidgetTag(siteUuid)}`);
1985
+ break;
1986
+ }
1987
+ if (result.action === "added" || result.action === "updated") {
1988
+ console.log(' (opt out any time with "widget": false in .patchstackrc.json)');
1989
+ }
1990
+ } catch (err) {
1991
+ console.warn(`Widget: skipped (${err.message}).`);
1992
+ }
1993
+ }
1881
1994
  async function runProtectCommand(_args) {
1882
1995
  try {
1883
1996
  runProtect(process.cwd());
@@ -1906,7 +2019,7 @@ async function runGuide(args) {
1906
2019
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
1907
2020
  console.log("");
1908
2021
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
1909
- console.log(readFileSync3(guidePath, "utf8"));
2022
+ console.log(readFileSync4(guidePath, "utf8"));
1910
2023
  return 0;
1911
2024
  }
1912
2025
  async function runStatus(args) {
@@ -1952,6 +2065,20 @@ async function runMarkBuild(args) {
1952
2065
  `mark-build: could not compute the build fingerprint (${err.message}). Stamping the production flag only.`
1953
2066
  );
1954
2067
  }
2068
+ let widgetUuid = null;
2069
+ try {
2070
+ const config = await resolveConfig({
2071
+ cwd,
2072
+ cliSiteUuid: getStringFlag(args.flags, "site-uuid")
2073
+ });
2074
+ if (config.widget) {
2075
+ widgetUuid = config.siteUuid;
2076
+ }
2077
+ } catch (err) {
2078
+ console.warn(
2079
+ `mark-build: could not resolve the site UUID (${err.message}). Skipping the widget pass.`
2080
+ );
2081
+ }
1955
2082
  const dir = resolveBuildDir(cwd, getStringFlag(args.flags, "dir"));
1956
2083
  if (dir === null) {
1957
2084
  console.warn(
@@ -1966,17 +2093,25 @@ async function runMarkBuild(args) {
1966
2093
  }
1967
2094
  const snippet = buildInjectionSnippet(checksum, stack);
1968
2095
  let marked = 0;
2096
+ let widgetTouched = 0;
1969
2097
  for (const file of files) {
1970
- const before = readFileSync3(file, "utf8");
1971
- const after = injectMarker(before, snippet);
2098
+ const before = readFileSync4(file, "utf8");
2099
+ let after = injectMarker(before, snippet);
2100
+ if (widgetUuid !== null) {
2101
+ const ensured = ensureWidgetInHtml(after, widgetUuid);
2102
+ if (ensured.action === "added" || ensured.action === "updated") {
2103
+ widgetTouched += 1;
2104
+ }
2105
+ after = ensured.html;
2106
+ }
1972
2107
  if (after !== before) {
1973
- writeFileSync2(file, after);
2108
+ writeFileSync3(file, after);
1974
2109
  marked += 1;
1975
2110
  }
1976
2111
  }
1977
2112
  const stackSummary = stack !== null ? describeStack(stack) : null;
1978
2113
  console.log(
1979
- `mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
2114
+ `mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
1980
2115
  );
1981
2116
  return 0;
1982
2117
  }