@patchstack/connect 0.3.33 → 0.4.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
@@ -70,6 +70,10 @@ Only `map` reads your source, and only `map --upload` sends anything derived fro
70
70
 
71
71
  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.
72
72
 
73
+ **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.
74
+
75
+ **Then tell them to deploy.** Setup changes source files, and the deployed site keeps serving its previous build until the next deploy — so visitors get no widget, and on a server-rendered root no production marker, until the user deploys (or hits Publish) again. Say it as a reminder; do not deploy anything yourself.
76
+
73
77
  ## Manual setup
74
78
 
75
79
  1. **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 — or, when the root shell is JSX, the production marker instead. No signup, dashboard step, or UUID is needed up front:
@@ -176,8 +180,28 @@ would have stopped while it is still in dry-run. Two separate paths, with differ
176
180
 
177
181
  What a detection report contains, per matched rule — on every phase, whatever fired it: the rule id, the revision of the rule when the
178
182
  bundle carried one, the identifier of the rule bundle in use, which phase matched,
179
- whether it was enforced, the request path **with the query string's values removed**,
180
- that query's parameter names, the method, and a timestamp. Each batch also carries a count of reports dropped when traffic outran the flush,
183
+ the rule's category and the action it declares, whether it was enforced,
184
+ which call it belongs to, the request path **with the query string's values removed**,
185
+ that query's parameter names, the method, and a timestamp.
186
+
187
+ Which call it belongs to is a token your guard mints and repeats on every rule that matched the same
188
+ request or the same outbound call. It exists because two rules matching one call is ordinary rather than
189
+ unusual — a rule that enforces and a rule that only observes are meant to match the same thing — so
190
+ without it, adding these reports up counts one call more than once.
191
+
192
+ Nothing about the request goes into it: not the address, not the path, not a header. It is drawn from
193
+ your runtime's randomness where that exists, and from the clock plus `Math.random` where it does not,
194
+ which is how this package already makes its own instance id. It is never a secret and never a boundary —
195
+ nothing is authorised by holding it — so what it has to do is not collide between two calls. A request and the response to it share one token; an
196
+ outbound call gets its own, because an outbound attempt is a thing in its own right and one made outside
197
+ any request has no request to belong to.
198
+
199
+ The category and the declared action say what KIND of rule matched — "a secret-exposure rule that
200
+ redacts", "an SSRF rule that blocks". Both are read from the rule your guard was served, and both are
201
+ `null` when that rule declares neither: a rule whose class nobody can state is reported as one, not
202
+ filled in. Neither is the same as `enforced`, which is whether the rule acted on this particular
203
+ request: a rule declaring `block` while only observing reports exactly that, and that is what a
204
+ detect-only deployment consists of. Each batch also carries a count of reports dropped when traffic outran the flush,
181
205
  so a partial sample is not read as a complete one.
182
206
 
183
207
  Two fields depend on the phase, because one kind of detection has a client and the other does not. A
@@ -348,7 +372,8 @@ Two more endpoints the package can call, for completeness:
348
372
 
349
373
  - `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`).
350
374
  - `npx @patchstack/connect protect --check` verifies the runtime guard is connected to the request path.
351
- - Load the site in a browser — the "Report a vulnerability" button should appear.
375
+ - 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.
376
+ - On the deployed site, the button appears only after a deploy that includes these source changes.
352
377
 
353
378
  ## Answering "is Patchstack installed?" / "is Patchstack removed?"
354
379
 
package/README.md CHANGED
@@ -6,7 +6,7 @@ Connect a JavaScript / Node.js application to [Patchstack](https://patchstack.co
6
6
 
7
7
  Copy this request into a coding assistant, or run the same command yourself:
8
8
 
9
- > I have vetted the official [`@patchstack/connect`](https://www.npmjs.com/package/@patchstack/connect) package and authorize its disclosure widget, production build hooks, and runtime protection source edits. Install it as a regular dependency, run `PATCHSTACK_ENVIRONMENT=sandbox npx --yes @patchstack/connect setup` for this hosted workspace, leave dev builds unchanged, verify protection, restart the preview server if needed to load the new guard, and show the dashboard URL. Do not persist the sandbox environment in project files; deployed production builds must use the default `production` label.
9
+ > I have vetted the official [`@patchstack/connect`](https://www.npmjs.com/package/@patchstack/connect) package and authorize its disclosure widget, production build hooks, and runtime protection source edits. Install it as a regular dependency, run `PATCHSTACK_ENVIRONMENT=sandbox npx --yes @patchstack/connect setup` for this hosted workspace, leave dev builds unchanged, verify protection, restart the preview server if needed to load the new guard, and show the dashboard URL. Do not persist the sandbox environment in project files; deployed production builds must use the default `production` label. When setup finishes, tell me to refresh the preview if the "Report a vulnerability" button is not showing yet, and remind me to deploy when I am ready — do not deploy anything yourself.
10
10
 
11
11
  `setup` is state-aware and idempotent: it scans dependencies, provisions or reuses the site, manages the disclosure widget, installs and verifies the runtime guard, adds a dependency-install scan, wires the existing build command without replacing it, and prints the remaining setup status. It never runs the project build. `guide` provides the same project-specific status without changing files.
12
12
 
@@ -32,6 +32,10 @@ That's it. `setup`:
32
32
  8. Wires `scan` before builds and `mark-build` after builds, preserving existing commands and using direct build chaining for Bun.
33
33
  9. Prints a dashboard link — 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`.
34
34
 
35
+ Then **refresh your 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. Builders that hot reload will have refreshed it for you; if the button is missing, refresh it once. `setup` prints the same reminder, and the CLI has no way to reload a browser itself.
36
+
37
+ Then **deploy**. These are source changes, so your live site keeps serving its previous build — visitors get the widget, and a server-rendered root gets the production marker, only after the next deploy.
38
+
35
39
  ## Quick start (existing site)
36
40
 
37
41
  If you already created an "Application" site in the Patchstack dashboard, pre-seed the UUID:
package/dist/cli.js CHANGED
@@ -2176,7 +2176,7 @@ function ensureSourceMarker(cwd, shell, framework) {
2176
2176
  }
2177
2177
 
2178
2178
  // src/guide.ts
2179
- import { existsSync as existsSync11, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
2179
+ import { existsSync as existsSync12, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
2180
2180
  import path9 from "path";
2181
2181
 
2182
2182
  // src/protect/install/util.ts
@@ -2193,8 +2193,8 @@ function hasDependency(cwd, name) {
2193
2193
  return false;
2194
2194
  }
2195
2195
  }
2196
- function hasResolvableCredential(cwd) {
2197
- if ((process.env.PATCHSTACK_API_KEY ?? "") !== "" || (process.env.PATCHSTACK_PULSE_AUTH ?? "") !== "") {
2196
+ function hasResolvableCredential(cwd, env = process.env) {
2197
+ if ((env.PATCHSTACK_API_KEY ?? "") !== "" || (env.PATCHSTACK_PULSE_AUTH ?? "") !== "") {
2198
2198
  return true;
2199
2199
  }
2200
2200
  for (const file of [".patchstackrc.local.json", ".patchstackrc.json"]) {
@@ -2209,6 +2209,9 @@ function hasResolvableCredential(cwd) {
2209
2209
  }
2210
2210
  var SITE_UUID_PLACEHOLDER = "__PATCHSTACK_SITE_UUID__";
2211
2211
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2212
+ function isSiteUuid(value) {
2213
+ return typeof value === "string" && UUID_RE.test(value);
2214
+ }
2212
2215
  function bakeSiteUuid(cwd, guardRelPath) {
2213
2216
  const rc = join2(cwd, ".patchstackrc.json");
2214
2217
  if (!existsSync2(rc)) {
@@ -3487,6 +3490,121 @@ var expressAdapter = {
3487
3490
  verify: (cwd) => verifyRegister(cwd, SPEC6)
3488
3491
  };
3489
3492
 
3493
+ // src/protect/install/reporting.ts
3494
+ import { existsSync as existsSync10 } from "fs";
3495
+ import { join as join12 } from "path";
3496
+
3497
+ // src/protect/reporting-state.js
3498
+ var REPORTING_STATES = Object.freeze([
3499
+ /** Enrolled, managed rules, credential present, not opted out. Events are sent. */
3500
+ "on",
3501
+ /** `PATCHSTACK_REPORT_DETECTIONS=0`. An explicit, per-deployment opt-out. */
3502
+ "disabled-by-config",
3503
+ /** `PATCHSTACK_TELEMETRY=0`. The broader switch, which covers this along with everything else. */
3504
+ "disabled-by-telemetry-opt-out",
3505
+ /** No site identity: a local or unenrolled install. Nothing to report against. */
3506
+ "not-enrolled",
3507
+ /**
3508
+ * A site identity, but the rules running are not the platform's — the caller's own bundle, or none.
3509
+ * There is no managed rule document to attribute a detection to, so a report would name a rule id the
3510
+ * platform never issued.
3511
+ */
3512
+ "no-managed-rules",
3513
+ /** Enrolled with managed rules, but no credential resolved, so a report would be refused. */
3514
+ "unavailable-no-credential"
3515
+ ]);
3516
+ function optedOut(value) {
3517
+ if (value === void 0 || value === null || value === "") return false;
3518
+ return /^(0|false|off|no)$/i.test(String(value));
3519
+ }
3520
+ function reportingState(input) {
3521
+ const env = input.env ?? (typeof process !== "undefined" ? process.env : void 0) ?? {};
3522
+ if (input.configOptOut === true) return { state: "disabled-by-config", reports: false };
3523
+ if (optedOut(env.PATCHSTACK_REPORT_DETECTIONS)) return { state: "disabled-by-config", reports: false };
3524
+ if (optedOut(env.PATCHSTACK_TELEMETRY)) return { state: "disabled-by-telemetry-opt-out", reports: false };
3525
+ const siteUuid = input.siteUuid;
3526
+ if (typeof siteUuid !== "string" || siteUuid === "") return { state: "not-enrolled", reports: false };
3527
+ if (input.hasCredential !== true) return { state: "unavailable-no-credential", reports: false };
3528
+ const managed = input.ruleOrigin === "api" || input.ruleOrigin === "cache";
3529
+ if (!managed) return { state: "no-managed-rules", reports: false };
3530
+ return { state: "on", reports: true };
3531
+ }
3532
+ function explainReportingState(state) {
3533
+ switch (state) {
3534
+ case "on":
3535
+ return "Security events are reported to Patchstack for this site.";
3536
+ case "disabled-by-config":
3537
+ return "Reporting is off because PATCHSTACK_REPORT_DETECTIONS is set to a false value.";
3538
+ case "disabled-by-telemetry-opt-out":
3539
+ return "Reporting is off because PATCHSTACK_TELEMETRY is set to a false value, which covers all telemetry.";
3540
+ case "not-enrolled":
3541
+ return "Reporting is off because this install has no site identity \u2014 it is not enrolled in Patchstack-managed mitigation.";
3542
+ case "no-managed-rules":
3543
+ return "Reporting is off because the rules in force did not come from Patchstack, so a detection could not be attributed to a managed rule.";
3544
+ case "unavailable-no-credential":
3545
+ return "Reporting is unavailable because no API credential resolved, so a report would be refused.";
3546
+ default:
3547
+ return `Unrecognised reporting state: ${String(state)}.`;
3548
+ }
3549
+ }
3550
+
3551
+ // src/protect/install/reporting.ts
3552
+ function siteIdentity(cwd, env) {
3553
+ const rc = join12(cwd, ".patchstackrc.json");
3554
+ let recorded;
3555
+ if (existsSync10(rc)) {
3556
+ try {
3557
+ recorded = JSON.parse(read(rc)).siteUuid;
3558
+ } catch {
3559
+ recorded = void 0;
3560
+ }
3561
+ }
3562
+ if (isSiteUuid(recorded)) return { uuid: recorded, found: true };
3563
+ const fromEnv = env.PATCHSTACK_SITE_UUID;
3564
+ if (isSiteUuid(fromEnv)) return { uuid: fromEnv, found: true };
3565
+ const anything = typeof recorded === "string" && recorded !== "" || (fromEnv ?? "") !== "";
3566
+ return { uuid: null, found: anything };
3567
+ }
3568
+ function reportingChecks(cwd, env = process.env) {
3569
+ const { uuid: siteUuid, found: identityFound } = siteIdentity(cwd, env);
3570
+ const hasCredential = hasResolvableCredential(cwd, env);
3571
+ const { state } = reportingState({ siteUuid, hasCredential, ruleOrigin: "api", env });
3572
+ const checks = [
3573
+ {
3574
+ group: "reporting",
3575
+ label: "this app is enrolled (a site identity is recorded)",
3576
+ ok: siteUuid !== null,
3577
+ hint: identityFound ? "a site identity is recorded but is not a usable UUID \u2014 check `siteUuid` in .patchstackrc.json, or PATCHSTACK_SITE_UUID. The guard protects the app and reports nothing, because there is nothing to attribute a detection to." : "no site identity \u2014 run `npx @patchstack/connect scan` to provision one, or set PATCHSTACK_SITE_UUID in the deployment. Without it the guard protects the app but reports nothing."
3578
+ }
3579
+ ];
3580
+ if (state === "on") {
3581
+ checks.push({
3582
+ group: "reporting",
3583
+ label: "detection reporting is switched on for this environment",
3584
+ ok: true,
3585
+ unverifiable: true,
3586
+ hint: "nothing here switches it off, but whether it actually reports depends on the rules the guard resolves at runtime: reporting is on for Patchstack-delivered rules and off for a bundle of your own (`no-managed-rules`). Read `protection.detectionReporting` in the running app for the settled answer. This also only judges the environment it ran in \u2014 a deployment may set an opt-out this machine cannot see."
3587
+ });
3588
+ } else {
3589
+ checks.push({
3590
+ group: "reporting",
3591
+ label: `detection reporting is off for this environment (${state})`,
3592
+ ok: state === "disabled-by-config" || state === "disabled-by-telemetry-opt-out",
3593
+ hint: `${explainReportingState(state)} Judged from this environment only; a deployment's is not this one.`
3594
+ });
3595
+ }
3596
+ checks.push({
3597
+ group: "reporting",
3598
+ // A wired guard says events CAN be sent. It says nothing about whether any were, or whether the
3599
+ // platform took them, and this is where an operator would otherwise assume both.
3600
+ label: "delivery health (events attempted, acknowledged, refused, dropped)",
3601
+ ok: true,
3602
+ unverifiable: true,
3603
+ hint: "not observable from here \u2014 the counters live in the running guard. Call `protection.detectionHealth()` in the deployment: it is undefined when there is no reporter, and otherwise reports what was attempted, acknowledged, refused and dropped for queue pressure. A wired guard is not evidence that anything arrived."
3604
+ });
3605
+ return checks;
3606
+ }
3607
+
3490
3608
  // src/protect/install/index.ts
3491
3609
  var ADAPTERS = [
3492
3610
  tanstackSupabaseAdapter,
@@ -3537,17 +3655,25 @@ function runVerify(cwd) {
3537
3655
  const adapter = ADAPTERS.find((a) => a.detect(cwd));
3538
3656
  if (adapter) {
3539
3657
  const result = adapter.verify(cwd);
3540
- return { stack: adapter.label, ...result, checks: [...result.checks, ...credentialNote(cwd, adapter.name)] };
3658
+ return {
3659
+ stack: adapter.label,
3660
+ ...result,
3661
+ checks: [...result.checks, ...credentialNote(cwd, adapter.name), ...reportingChecks(cwd)]
3662
+ };
3541
3663
  }
3542
3664
  const generic = genericVerify(cwd);
3543
- return { stack: "generic", ...generic, checks: [...generic.checks, ...credentialNote(cwd, "generic")] };
3665
+ return {
3666
+ stack: "generic",
3667
+ ...generic,
3668
+ checks: [...generic.checks, ...credentialNote(cwd, "generic"), ...reportingChecks(cwd)]
3669
+ };
3544
3670
  } catch (err) {
3545
3671
  return { stack: "unknown", wired: false, checks: [{ label: "verification failed", ok: false, hint: String(err?.message ?? err) }] };
3546
3672
  }
3547
3673
  }
3548
3674
 
3549
3675
  // src/widget.ts
3550
- import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
3676
+ import { existsSync as existsSync11, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
3551
3677
  import path8 from "path";
3552
3678
  var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
3553
3679
  var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
@@ -3586,7 +3712,7 @@ ${indent}</body>`;
3586
3712
  }
3587
3713
  function findSourceShell(cwd) {
3588
3714
  for (const candidate of SOURCE_SHELL_CANDIDATES) {
3589
- if (existsSync10(path8.join(cwd, candidate))) {
3715
+ if (existsSync11(path8.join(cwd, candidate))) {
3590
3716
  return candidate;
3591
3717
  }
3592
3718
  }
@@ -3680,7 +3806,7 @@ function detectPackageManager(cwd) {
3680
3806
  return declared;
3681
3807
  }
3682
3808
  for (const { filename, pm } of PM_BY_LOCKFILE) {
3683
- if (existsSync11(path9.join(cwd, filename))) {
3809
+ if (existsSync12(path9.join(cwd, filename))) {
3684
3810
  return pm;
3685
3811
  }
3686
3812
  }
@@ -3777,7 +3903,7 @@ function resolveWidgetFileHint(cwd, framework) {
3777
3903
  ...GENERIC_WIDGET_FILES
3778
3904
  ];
3779
3905
  for (const candidate of candidates) {
3780
- if (existsSync11(path9.join(cwd, candidate))) {
3906
+ if (existsSync12(path9.join(cwd, candidate))) {
3781
3907
  return candidate;
3782
3908
  }
3783
3909
  }
@@ -3861,6 +3987,9 @@ function needsSourceProductionMarker(state) {
3861
3987
  }
3862
3988
  return !state.widgetFileHint.toLowerCase().endsWith(".html");
3863
3989
  }
3990
+ function widgetTagInPlace(state) {
3991
+ return state.siteUuid !== null && !state.widgetOptOut && state.widgetInstalled && state.widgetTokenMatches !== false;
3992
+ }
3864
3993
  function countRemainingSteps(state) {
3865
3994
  return [
3866
3995
  state.installed?.section === "dependencies",
@@ -4007,6 +4136,19 @@ function renderGuideChecklist(state, useColor) {
4007
4136
  } else {
4008
4137
  lines.push(detail("The dashboard link appears after the first scan (re-print any time with `status`)."));
4009
4138
  }
4139
+ if (widgetTagInPlace(state)) {
4140
+ lines.push("");
4141
+ lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Refresh the preview to see the widget:")}`);
4142
+ lines.push(' The "Report a vulnerability" button loads with the page, so a preview that was');
4143
+ lines.push(" already open still shows the HTML from before this change. Builders that hot");
4144
+ lines.push(" reload refresh it themselves; if the button is missing, refresh the preview once.");
4145
+ }
4146
+ if (state.siteUuid !== null) {
4147
+ lines.push("");
4148
+ lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Deploy to put this on your live site:")}`);
4149
+ lines.push(" These are source changes. Your deployed site keeps serving its previous build,");
4150
+ lines.push(" so visitors only get the widget after you deploy (or hit Publish) again.");
4151
+ }
4010
4152
  const remaining = countRemainingSteps(state);
4011
4153
  lines.push("");
4012
4154
  if (remaining === 0) {
@@ -4160,10 +4302,10 @@ import { writeFileSync as writeFileSync8 } from "fs";
4160
4302
  // src/map/ts-loader.ts
4161
4303
  import { createRequire } from "module";
4162
4304
  import { pathToFileURL } from "url";
4163
- import { join as join12 } from "path";
4305
+ import { join as join13 } from "path";
4164
4306
  async function loadTypeScript(cwd) {
4165
4307
  try {
4166
- const req = createRequire(pathToFileURL(join12(cwd, "package.json")));
4308
+ const req = createRequire(pathToFileURL(join13(cwd, "package.json")));
4167
4309
  const resolved = req.resolve("typescript");
4168
4310
  const mod = await import(pathToFileURL(resolved).href);
4169
4311
  return mod.default ?? mod;
@@ -4435,7 +4577,7 @@ function npmPackageOf(spec) {
4435
4577
 
4436
4578
  // src/map/sources.ts
4437
4579
  import { readFileSync as readFileSync8, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
4438
- import { join as join13, relative as relative2, isAbsolute } from "path";
4580
+ import { join as join14, relative as relative2, isAbsolute } from "path";
4439
4581
 
4440
4582
  // src/map/routes.ts
4441
4583
  var ROUTE_REGISTER_NAMES = ["get", "post", "put", "patch", "delete", "options", "all", "head", "use"];
@@ -4519,7 +4661,7 @@ function hasEntrySignal(text) {
4519
4661
  }
4520
4662
  function detectFramework(cwd) {
4521
4663
  try {
4522
- const pkg = JSON.parse(readFileSync8(join13(cwd, "package.json"), "utf8"));
4664
+ const pkg = JSON.parse(readFileSync8(join14(cwd, "package.json"), "utf8"));
4523
4665
  const d = { ...pkg.dependencies, ...pkg.devDependencies };
4524
4666
  if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
4525
4667
  if (d["next"]) return "next";
@@ -4531,11 +4673,11 @@ function detectFramework(cwd) {
4531
4673
  } catch {
4532
4674
  }
4533
4675
  try {
4534
- if (statSync3(join13(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
4676
+ if (statSync3(join14(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
4535
4677
  } catch {
4536
4678
  }
4537
4679
  try {
4538
- if (statSync3(join13(cwd, "functions")).isDirectory()) return "deno-functions";
4680
+ if (statSync3(join14(cwd, "functions")).isDirectory()) return "deno-functions";
4539
4681
  } catch {
4540
4682
  }
4541
4683
  return "unknown";
@@ -4577,7 +4719,7 @@ function probeForSource(dir, budget) {
4577
4719
  for (const e of entries) {
4578
4720
  if (budget.left-- <= 0) return "unknown";
4579
4721
  if (e.isDirectory()) {
4580
- if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join13(dir, e.name));
4722
+ if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join14(dir, e.name));
4581
4723
  } else if (isSourceFile(e.name)) return true;
4582
4724
  else if (e.isSymbolicLink()) return "unknown";
4583
4725
  }
@@ -4614,13 +4756,13 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
4614
4756
  const probeable = !SKIP_IS_DEFINITIONAL.has(e.name);
4615
4757
  if (probeable && !e.isDirectory() && e.isSymbolicLink()) stats.skippedUnknown++;
4616
4758
  else if (probeable && e.isDirectory()) {
4617
- const held = probeForSource(join13(dir, e.name), probeBudget);
4618
- if (held === true) stats.skippedWithSource.push(join13(dir, e.name));
4759
+ const held = probeForSource(join14(dir, e.name), probeBudget);
4760
+ if (held === true) stats.skippedWithSource.push(join14(dir, e.name));
4619
4761
  else if (held === "unknown") stats.skippedUnknown++;
4620
4762
  }
4621
4763
  continue;
4622
4764
  }
4623
- const full = join13(dir, e.name);
4765
+ const full = join14(dir, e.name);
4624
4766
  if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
4625
4767
  else if (e.isSymbolicLink()) {
4626
4768
  let st, real;
@@ -4690,7 +4832,7 @@ function detectDeploymentShapes(cwd, opts = {}) {
4690
4832
  const found = [];
4691
4833
  for (const candidate of DEPLOYMENT_SHAPES) {
4692
4834
  for (const file of candidate.files ?? []) {
4693
- const full = join13(cwd, file);
4835
+ const full = join14(cwd, file);
4694
4836
  try {
4695
4837
  if (statSync3(full).isFile() && inProject(full)) {
4696
4838
  found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
@@ -4700,7 +4842,7 @@ function detectDeploymentShapes(cwd, opts = {}) {
4700
4842
  }
4701
4843
  }
4702
4844
  for (const dir of candidate.dirs ?? []) {
4703
- const full = join13(cwd, dir);
4845
+ const full = join14(cwd, dir);
4704
4846
  try {
4705
4847
  if (!statSync3(full).isDirectory() || !inProject(full)) continue;
4706
4848
  } catch {
@@ -4719,7 +4861,7 @@ function holdsFunctionEntry(dir) {
4719
4861
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
4720
4862
  if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
4721
4863
  try {
4722
- for (const nested of readdirSync5(join13(dir, entry.name), { withFileTypes: true })) {
4864
+ for (const nested of readdirSync5(join14(dir, entry.name), { withFileTypes: true })) {
4723
4865
  if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
4724
4866
  }
4725
4867
  } catch {
@@ -4735,7 +4877,7 @@ function holdsSourceFile(dir) {
4735
4877
  if (entry.isFile() && isSourceFile(entry.name)) return true;
4736
4878
  if (entry.isDirectory()) {
4737
4879
  try {
4738
- for (const nested of readdirSync5(join13(dir, entry.name), { withFileTypes: true })) {
4880
+ for (const nested of readdirSync5(join14(dir, entry.name), { withFileTypes: true })) {
4739
4881
  if (nested.isFile() && isSourceFile(nested.name)) return true;
4740
4882
  }
4741
4883
  } catch {
@@ -4749,7 +4891,7 @@ function holdsSourceFile(dir) {
4749
4891
 
4750
4892
  // src/map/surface.ts
4751
4893
  import { readFileSync as readFileSync9 } from "fs";
4752
- import { join as join14 } from "path";
4894
+ import { join as join15 } from "path";
4753
4895
  var STATIC_GENERATORS = [
4754
4896
  { dep: "vite", label: "vite" },
4755
4897
  { dep: "react-scripts", label: "create-react-app" },
@@ -4787,7 +4929,7 @@ var SHAPE_SIGNAL = {
4787
4929
  };
4788
4930
  function readManifest(cwd) {
4789
4931
  try {
4790
- const parsed = JSON.parse(readFileSync9(join14(cwd, "package.json"), "utf8"));
4932
+ const parsed = JSON.parse(readFileSync9(join15(cwd, "package.json"), "utf8"));
4791
4933
  return typeof parsed === "object" && parsed !== null ? parsed : null;
4792
4934
  } catch {
4793
4935
  return null;
@@ -4800,7 +4942,7 @@ function nextExportsStatically(cwd, manifest, ts) {
4800
4942
  for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
4801
4943
  let text;
4802
4944
  try {
4803
- text = readFileSync9(join14(cwd, file), "utf8");
4945
+ text = readFileSync9(join15(cwd, file), "utf8");
4804
4946
  } catch {
4805
4947
  continue;
4806
4948
  }
@@ -5212,7 +5354,7 @@ function dedupeSinks(sinks, owner) {
5212
5354
 
5213
5355
  // src/map/module-graph.ts
5214
5356
  import { readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
5215
- import { dirname as dirname7, join as join15, relative as relative3, resolve as resolvePath } from "path";
5357
+ import { dirname as dirname7, join as join16, relative as relative3, resolve as resolvePath } from "path";
5216
5358
  var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
5217
5359
  function createModuleGraph(ts, opts) {
5218
5360
  const cache = /* @__PURE__ */ new Map();
@@ -5290,7 +5432,7 @@ function resolveRelativeModule(fromFile, spec) {
5290
5432
  }
5291
5433
  candidates.push(base);
5292
5434
  for (const e of RESOLVE_EXTS) candidates.push(base + e);
5293
- for (const e of RESOLVE_EXTS) candidates.push(join15(base, "index" + e));
5435
+ for (const e of RESOLVE_EXTS) candidates.push(join16(base, "index" + e));
5294
5436
  for (const c of candidates) {
5295
5437
  try {
5296
5438
  if (statSync4(c).isFile()) return c;
@@ -5974,7 +6116,7 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
5974
6116
 
5975
6117
  // src/map/imports.ts
5976
6118
  import { readFileSync as readFileSync11 } from "fs";
5977
- import { join as join16 } from "path";
6119
+ import { join as join17 } from "path";
5978
6120
  var MAX_SITES = 5;
5979
6121
  var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
5980
6122
  function isNpmPackageName(pkg) {
@@ -5985,7 +6127,7 @@ function readPathAliases(cwd) {
5985
6127
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
5986
6128
  let paths;
5987
6129
  try {
5988
- const raw = readFileSync11(join16(cwd, name), "utf8");
6130
+ const raw = readFileSync11(join17(cwd, name), "utf8");
5989
6131
  const parsed = JSON.parse(stripJsonComments(raw));
5990
6132
  paths = parsed.compilerOptions?.paths;
5991
6133
  } catch {
@@ -7213,12 +7355,18 @@ async function runProtectCommand(args) {
7213
7355
  if (args.flags.get("check") === true) {
7214
7356
  const report = runVerify(process.cwd());
7215
7357
  console.log(`patchstack protect --check (${report.stack}):`);
7216
- for (const c of report.checks) {
7358
+ const line = (c) => {
7217
7359
  const mark = c.unverifiable ? "?" : c.ok ? "\u2713" : "\u2717";
7218
7360
  const note = c.unverifiable || !c.ok ? c.hint : void 0;
7219
7361
  console.log(` ${mark} ${c.label}${note ? ` \u2014 ${note}` : ""}`);
7220
- }
7362
+ };
7363
+ for (const c of report.checks.filter((c2) => c2.group !== "reporting")) line(c);
7221
7364
  console.log(report.wired ? "guard is wired \u2713" : "guard is NOT fully wired \u2717");
7365
+ const reporting = report.checks.filter((c) => c.group === "reporting");
7366
+ if (reporting.length > 0) {
7367
+ console.log("reporting to Patchstack (does not affect the verdict above):");
7368
+ for (const c of reporting) line(c);
7369
+ }
7222
7370
  if (report.checks.some((c) => c.unverifiable)) {
7223
7371
  console.log("One or more checks could not be answered from here \u2014 see the `?` lines above.");
7224
7372
  }
@@ -7397,6 +7545,12 @@ Run: ${installCommand(before.packageManager)}`
7397
7545
  console.log("");
7398
7546
  console.log(`Setup applied its bounded changes; ${remaining} manual step(s) remain above.`);
7399
7547
  }
7548
+ console.log("");
7549
+ console.log("Tell the user:");
7550
+ if (widgetTagInPlace(after)) {
7551
+ console.log(' - refresh the preview if the "Report a vulnerability" button is not showing yet;');
7552
+ }
7553
+ console.log(" - deploy (or hit Publish) when ready, so the live site serves these changes.");
7400
7554
  return 0;
7401
7555
  }
7402
7556
  async function runStatus(args) {