@patchstack/connect 0.3.33 → 0.4.0

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/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
  }
@@ -4160,10 +4286,10 @@ import { writeFileSync as writeFileSync8 } from "fs";
4160
4286
  // src/map/ts-loader.ts
4161
4287
  import { createRequire } from "module";
4162
4288
  import { pathToFileURL } from "url";
4163
- import { join as join12 } from "path";
4289
+ import { join as join13 } from "path";
4164
4290
  async function loadTypeScript(cwd) {
4165
4291
  try {
4166
- const req = createRequire(pathToFileURL(join12(cwd, "package.json")));
4292
+ const req = createRequire(pathToFileURL(join13(cwd, "package.json")));
4167
4293
  const resolved = req.resolve("typescript");
4168
4294
  const mod = await import(pathToFileURL(resolved).href);
4169
4295
  return mod.default ?? mod;
@@ -4435,7 +4561,7 @@ function npmPackageOf(spec) {
4435
4561
 
4436
4562
  // src/map/sources.ts
4437
4563
  import { readFileSync as readFileSync8, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
4438
- import { join as join13, relative as relative2, isAbsolute } from "path";
4564
+ import { join as join14, relative as relative2, isAbsolute } from "path";
4439
4565
 
4440
4566
  // src/map/routes.ts
4441
4567
  var ROUTE_REGISTER_NAMES = ["get", "post", "put", "patch", "delete", "options", "all", "head", "use"];
@@ -4519,7 +4645,7 @@ function hasEntrySignal(text) {
4519
4645
  }
4520
4646
  function detectFramework(cwd) {
4521
4647
  try {
4522
- const pkg = JSON.parse(readFileSync8(join13(cwd, "package.json"), "utf8"));
4648
+ const pkg = JSON.parse(readFileSync8(join14(cwd, "package.json"), "utf8"));
4523
4649
  const d = { ...pkg.dependencies, ...pkg.devDependencies };
4524
4650
  if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
4525
4651
  if (d["next"]) return "next";
@@ -4531,11 +4657,11 @@ function detectFramework(cwd) {
4531
4657
  } catch {
4532
4658
  }
4533
4659
  try {
4534
- if (statSync3(join13(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
4660
+ if (statSync3(join14(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
4535
4661
  } catch {
4536
4662
  }
4537
4663
  try {
4538
- if (statSync3(join13(cwd, "functions")).isDirectory()) return "deno-functions";
4664
+ if (statSync3(join14(cwd, "functions")).isDirectory()) return "deno-functions";
4539
4665
  } catch {
4540
4666
  }
4541
4667
  return "unknown";
@@ -4577,7 +4703,7 @@ function probeForSource(dir, budget) {
4577
4703
  for (const e of entries) {
4578
4704
  if (budget.left-- <= 0) return "unknown";
4579
4705
  if (e.isDirectory()) {
4580
- if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join13(dir, e.name));
4706
+ if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join14(dir, e.name));
4581
4707
  } else if (isSourceFile(e.name)) return true;
4582
4708
  else if (e.isSymbolicLink()) return "unknown";
4583
4709
  }
@@ -4614,13 +4740,13 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
4614
4740
  const probeable = !SKIP_IS_DEFINITIONAL.has(e.name);
4615
4741
  if (probeable && !e.isDirectory() && e.isSymbolicLink()) stats.skippedUnknown++;
4616
4742
  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));
4743
+ const held = probeForSource(join14(dir, e.name), probeBudget);
4744
+ if (held === true) stats.skippedWithSource.push(join14(dir, e.name));
4619
4745
  else if (held === "unknown") stats.skippedUnknown++;
4620
4746
  }
4621
4747
  continue;
4622
4748
  }
4623
- const full = join13(dir, e.name);
4749
+ const full = join14(dir, e.name);
4624
4750
  if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
4625
4751
  else if (e.isSymbolicLink()) {
4626
4752
  let st, real;
@@ -4690,7 +4816,7 @@ function detectDeploymentShapes(cwd, opts = {}) {
4690
4816
  const found = [];
4691
4817
  for (const candidate of DEPLOYMENT_SHAPES) {
4692
4818
  for (const file of candidate.files ?? []) {
4693
- const full = join13(cwd, file);
4819
+ const full = join14(cwd, file);
4694
4820
  try {
4695
4821
  if (statSync3(full).isFile() && inProject(full)) {
4696
4822
  found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
@@ -4700,7 +4826,7 @@ function detectDeploymentShapes(cwd, opts = {}) {
4700
4826
  }
4701
4827
  }
4702
4828
  for (const dir of candidate.dirs ?? []) {
4703
- const full = join13(cwd, dir);
4829
+ const full = join14(cwd, dir);
4704
4830
  try {
4705
4831
  if (!statSync3(full).isDirectory() || !inProject(full)) continue;
4706
4832
  } catch {
@@ -4719,7 +4845,7 @@ function holdsFunctionEntry(dir) {
4719
4845
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
4720
4846
  if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
4721
4847
  try {
4722
- for (const nested of readdirSync5(join13(dir, entry.name), { withFileTypes: true })) {
4848
+ for (const nested of readdirSync5(join14(dir, entry.name), { withFileTypes: true })) {
4723
4849
  if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
4724
4850
  }
4725
4851
  } catch {
@@ -4735,7 +4861,7 @@ function holdsSourceFile(dir) {
4735
4861
  if (entry.isFile() && isSourceFile(entry.name)) return true;
4736
4862
  if (entry.isDirectory()) {
4737
4863
  try {
4738
- for (const nested of readdirSync5(join13(dir, entry.name), { withFileTypes: true })) {
4864
+ for (const nested of readdirSync5(join14(dir, entry.name), { withFileTypes: true })) {
4739
4865
  if (nested.isFile() && isSourceFile(nested.name)) return true;
4740
4866
  }
4741
4867
  } catch {
@@ -4749,7 +4875,7 @@ function holdsSourceFile(dir) {
4749
4875
 
4750
4876
  // src/map/surface.ts
4751
4877
  import { readFileSync as readFileSync9 } from "fs";
4752
- import { join as join14 } from "path";
4878
+ import { join as join15 } from "path";
4753
4879
  var STATIC_GENERATORS = [
4754
4880
  { dep: "vite", label: "vite" },
4755
4881
  { dep: "react-scripts", label: "create-react-app" },
@@ -4787,7 +4913,7 @@ var SHAPE_SIGNAL = {
4787
4913
  };
4788
4914
  function readManifest(cwd) {
4789
4915
  try {
4790
- const parsed = JSON.parse(readFileSync9(join14(cwd, "package.json"), "utf8"));
4916
+ const parsed = JSON.parse(readFileSync9(join15(cwd, "package.json"), "utf8"));
4791
4917
  return typeof parsed === "object" && parsed !== null ? parsed : null;
4792
4918
  } catch {
4793
4919
  return null;
@@ -4800,7 +4926,7 @@ function nextExportsStatically(cwd, manifest, ts) {
4800
4926
  for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
4801
4927
  let text;
4802
4928
  try {
4803
- text = readFileSync9(join14(cwd, file), "utf8");
4929
+ text = readFileSync9(join15(cwd, file), "utf8");
4804
4930
  } catch {
4805
4931
  continue;
4806
4932
  }
@@ -5212,7 +5338,7 @@ function dedupeSinks(sinks, owner) {
5212
5338
 
5213
5339
  // src/map/module-graph.ts
5214
5340
  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";
5341
+ import { dirname as dirname7, join as join16, relative as relative3, resolve as resolvePath } from "path";
5216
5342
  var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
5217
5343
  function createModuleGraph(ts, opts) {
5218
5344
  const cache = /* @__PURE__ */ new Map();
@@ -5290,7 +5416,7 @@ function resolveRelativeModule(fromFile, spec) {
5290
5416
  }
5291
5417
  candidates.push(base);
5292
5418
  for (const e of RESOLVE_EXTS) candidates.push(base + e);
5293
- for (const e of RESOLVE_EXTS) candidates.push(join15(base, "index" + e));
5419
+ for (const e of RESOLVE_EXTS) candidates.push(join16(base, "index" + e));
5294
5420
  for (const c of candidates) {
5295
5421
  try {
5296
5422
  if (statSync4(c).isFile()) return c;
@@ -5974,7 +6100,7 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
5974
6100
 
5975
6101
  // src/map/imports.ts
5976
6102
  import { readFileSync as readFileSync11 } from "fs";
5977
- import { join as join16 } from "path";
6103
+ import { join as join17 } from "path";
5978
6104
  var MAX_SITES = 5;
5979
6105
  var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
5980
6106
  function isNpmPackageName(pkg) {
@@ -5985,7 +6111,7 @@ function readPathAliases(cwd) {
5985
6111
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
5986
6112
  let paths;
5987
6113
  try {
5988
- const raw = readFileSync11(join16(cwd, name), "utf8");
6114
+ const raw = readFileSync11(join17(cwd, name), "utf8");
5989
6115
  const parsed = JSON.parse(stripJsonComments(raw));
5990
6116
  paths = parsed.compilerOptions?.paths;
5991
6117
  } catch {
@@ -7213,12 +7339,18 @@ async function runProtectCommand(args) {
7213
7339
  if (args.flags.get("check") === true) {
7214
7340
  const report = runVerify(process.cwd());
7215
7341
  console.log(`patchstack protect --check (${report.stack}):`);
7216
- for (const c of report.checks) {
7342
+ const line = (c) => {
7217
7343
  const mark = c.unverifiable ? "?" : c.ok ? "\u2713" : "\u2717";
7218
7344
  const note = c.unverifiable || !c.ok ? c.hint : void 0;
7219
7345
  console.log(` ${mark} ${c.label}${note ? ` \u2014 ${note}` : ""}`);
7220
- }
7346
+ };
7347
+ for (const c of report.checks.filter((c2) => c2.group !== "reporting")) line(c);
7221
7348
  console.log(report.wired ? "guard is wired \u2713" : "guard is NOT fully wired \u2717");
7349
+ const reporting = report.checks.filter((c) => c.group === "reporting");
7350
+ if (reporting.length > 0) {
7351
+ console.log("reporting to Patchstack (does not affect the verdict above):");
7352
+ for (const c of reporting) line(c);
7353
+ }
7222
7354
  if (report.checks.some((c) => c.unverifiable)) {
7223
7355
  console.log("One or more checks could not be answered from here \u2014 see the `?` lines above.");
7224
7356
  }