@patchstack/connect 0.5.2 → 0.5.3

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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
4
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
5
5
  import { createRequire as createRequire2 } from "module";
6
6
 
7
7
  // src/parsers/index.ts
@@ -1288,6 +1288,27 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
1288
1288
  return first.response;
1289
1289
  }
1290
1290
 
1291
+ // src/build-id.ts
1292
+ var BUILD_ID = /^[0-9a-f]{64}$/i;
1293
+ function canonicalBuildId(value) {
1294
+ if (typeof value !== "string") return null;
1295
+ const trimmed = value.trim();
1296
+ return BUILD_ID.test(trimmed) ? trimmed.toLowerCase() : null;
1297
+ }
1298
+ var BUILD_STAMP_KEY = "_patchstack";
1299
+ function readBuildStamp(bundle) {
1300
+ if (bundle === null || typeof bundle !== "object") return null;
1301
+ const namespace = bundle[BUILD_STAMP_KEY];
1302
+ if (namespace === null || typeof namespace !== "object") return null;
1303
+ return canonicalBuildId(namespace.build_id);
1304
+ }
1305
+ function hasRawBuildStamp(bundle) {
1306
+ if (bundle === null || typeof bundle !== "object") return false;
1307
+ const namespace = bundle[BUILD_STAMP_KEY];
1308
+ if (namespace === null || typeof namespace !== "object") return false;
1309
+ return "build_id" in namespace;
1310
+ }
1311
+
1291
1312
  // src/client.ts
1292
1313
  var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
1293
1314
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -1359,12 +1380,13 @@ function buildInputMapUrl(manifestEndpoint, siteUuid) {
1359
1380
  url.hash = "";
1360
1381
  return url.toString();
1361
1382
  }
1362
- async function postInputMap(config, map) {
1383
+ async function postInputMap(config, map, buildId) {
1363
1384
  if (config.siteUuid === null) {
1364
1385
  return { result: "skipped", message: "No site UUID configured \u2014 run `patchstack-connect scan` first." };
1365
1386
  }
1366
1387
  const url = buildInputMapUrl(config.endpoint, config.siteUuid);
1367
1388
  try {
1389
+ const canonical = canonicalBuildId(buildId);
1368
1390
  const response = await pulseFetch(config, url, {
1369
1391
  method: "POST",
1370
1392
  headers: {
@@ -1372,7 +1394,12 @@ async function postInputMap(config, map) {
1372
1394
  "Content-Type": "application/json",
1373
1395
  "User-Agent": "@patchstack/connect"
1374
1396
  },
1375
- body: JSON.stringify(map),
1397
+ // `build_id` is additive to the v3 document: a reader that ignores it behaves exactly as before.
1398
+ // It is the digest of the policy map carrying these coordinates, so the server can serve a rule pinned
1399
+ // to one of them as enforceable only to a guard carrying that same map. Omitted entirely when this
1400
+ // run could not bind the map into a pre-bundle guard — an absent field is a question the server
1401
+ // answers by withholding enforceability, which is the safe direction.
1402
+ body: JSON.stringify(canonical === null ? map : { ...map, build_id: canonical }),
1376
1403
  signal: AbortSignal.timeout(config.timeoutMs)
1377
1404
  });
1378
1405
  if (response.status === 404) {
@@ -4292,9 +4319,9 @@ async function collectGuideState(cwd) {
4292
4319
  endpointOverride,
4293
4320
  hasBuildScript: Boolean(pkg?.scripts?.build?.trim()),
4294
4321
  installScanWired: (pkg?.scripts?.postinstall ?? "").includes("patchstack-connect scan"),
4295
- // bun run doesn't execute npm-style pre/post scripts, so chaining inside
4296
- // the build script itself also counts as wired (and is what we suggest on bun).
4297
- prebuildWired: (pkg?.scripts?.prebuild ?? "").includes("patchstack-connect scan") || (pkg?.scripts?.build ?? "").includes("patchstack-connect scan"),
4322
+ // The scan has to run first: a later prebuild command may upload and stamp the map that the bundle
4323
+ // must retain. Bun skips npm-style pre/post scripts, so its build chain follows the same order.
4324
+ prebuildWired: /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.prebuild ?? "") || /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.build ?? ""),
4298
4325
  postbuildWired: (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build") || (pkg?.scripts?.build ?? "").includes("patchstack-connect mark-build"),
4299
4326
  widgetInstalled: widget.found,
4300
4327
  widgetTokenMatches: widget.uuidMatches,
@@ -5196,7 +5223,7 @@ function formatRuntimeCheck(report) {
5196
5223
  }
5197
5224
 
5198
5225
  // src/map-command.ts
5199
- import { writeFileSync as writeFileSync8 } from "fs";
5226
+ import { writeFileSync as writeFileSync9 } from "fs";
5200
5227
 
5201
5228
  // src/map/ts-loader.ts
5202
5229
  import { createRequire } from "module";
@@ -6068,14 +6095,14 @@ function directSinks(node, ts, bindings, ctx) {
6068
6095
  const root = base ? rootIdentifier(base, ts) : void 0;
6069
6096
  if (!root) return {};
6070
6097
  const spec = bindings.resolve(root);
6071
- const relative6 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
6098
+ const relative7 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
6072
6099
  const pkg = npmPackageOf(spec);
6073
6100
  if (pkg) return { pkg, root, spec };
6074
- if (relative6 && ctx && spec) {
6101
+ if (relative7 && ctx && spec) {
6075
6102
  const viaModule = ctx.graph.importedBinding(ctx.file, spec, bindings.exportNameOf(root) ?? root);
6076
6103
  if (viaModule) return { pkg: viaModule.package, root, spec };
6077
6104
  }
6078
- return { local: bindings.locals.has(root), root, spec, relative: relative6 };
6105
+ return { local: bindings.locals.has(root), root, spec, relative: relative7 };
6079
6106
  };
6080
6107
  const attributionOf = (b, pkg) => b.pkg ? "import" : pkg ? "inferred" : void 0;
6081
6108
  const dbApi = (pkg, attribution) => {
@@ -7608,6 +7635,243 @@ function getStringFlag(flags, name) {
7608
7635
  return typeof value === "string" ? value : void 0;
7609
7636
  }
7610
7637
 
7638
+ // src/build-stamp.ts
7639
+ import { chmodSync, lstatSync as lstatSync3, readFileSync as readFileSync14, readdirSync as readdirSync6, realpathSync as realpathSync5, renameSync, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
7640
+ import { join as join19, relative as relative6, sep as sep2 } from "path";
7641
+ var RULES_FILE_NAMES = /* @__PURE__ */ new Set(["rules.json", "patchstack.rules.json"]);
7642
+ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "tmp", "vendor"]);
7643
+ var MAX_DEPTH2 = 6;
7644
+ var GUARD_SOURCE = /\.(?:[cm]?[jt]sx?)$/;
7645
+ var MAX_GUARD_SOURCE_BYTES = 1024 * 1024;
7646
+ var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7647
+ function guardImportsRulesFile(dir, rulesName) {
7648
+ const quotedRules = `["']\\./${escapeRegExp(rulesName)}["']`;
7649
+ const importsRules = new RegExp(`(?:\\bfrom\\s*|\\brequire\\s*\\(\\s*|\\bnew\\s+URL\\s*\\(\\s*)${quotedRules}`);
7650
+ let siblings;
7651
+ try {
7652
+ siblings = readdirSync6(dir);
7653
+ } catch {
7654
+ return false;
7655
+ }
7656
+ for (const name of siblings) {
7657
+ if (!GUARD_SOURCE.test(name)) continue;
7658
+ const path14 = join19(dir, name);
7659
+ try {
7660
+ const entry = lstatSync3(path14);
7661
+ if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_GUARD_SOURCE_BYTES) continue;
7662
+ const source = stripComments(readFileSync14(path14, "utf8"));
7663
+ if (source.includes("@patchstack/connect/protect") && importsRules.test(source)) return true;
7664
+ } catch {
7665
+ }
7666
+ }
7667
+ return false;
7668
+ }
7669
+ function isRuleBundle(value) {
7670
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
7671
+ const record = value;
7672
+ return Array.isArray(record.firewall) && Array.isArray(record.whitelists) && record.whitelist_keys !== null && typeof record.whitelist_keys === "object";
7673
+ }
7674
+ function findRulesFile(cwd) {
7675
+ const found = [];
7676
+ const unreadable = [];
7677
+ let root;
7678
+ try {
7679
+ root = realpathSync5(cwd);
7680
+ } catch {
7681
+ return { kind: "none" };
7682
+ }
7683
+ const contained = (path14) => {
7684
+ try {
7685
+ const real = realpathSync5(path14);
7686
+ return real === root || real.startsWith(root + sep2);
7687
+ } catch {
7688
+ return false;
7689
+ }
7690
+ };
7691
+ const walk2 = (dir, depth) => {
7692
+ if (depth > MAX_DEPTH2 || found.length > 8) return;
7693
+ let entries;
7694
+ try {
7695
+ entries = readdirSync6(dir);
7696
+ } catch {
7697
+ return;
7698
+ }
7699
+ for (const name of entries) {
7700
+ if (name.startsWith(".")) continue;
7701
+ const path14 = join19(dir, name);
7702
+ let entry;
7703
+ try {
7704
+ entry = lstatSync3(path14);
7705
+ } catch {
7706
+ continue;
7707
+ }
7708
+ if (entry.isSymbolicLink()) continue;
7709
+ if (entry.isDirectory()) {
7710
+ if (!SKIP_DIRECTORIES.has(name)) walk2(path14, depth + 1);
7711
+ continue;
7712
+ }
7713
+ if (!entry.isFile() || !RULES_FILE_NAMES.has(name)) continue;
7714
+ if (!contained(path14)) continue;
7715
+ if (!guardImportsRulesFile(dir, name)) continue;
7716
+ try {
7717
+ if (isRuleBundle(JSON.parse(readFileSync14(path14, "utf8")))) found.push(path14);
7718
+ } catch {
7719
+ unreadable.push(path14);
7720
+ }
7721
+ }
7722
+ };
7723
+ walk2(cwd, 0);
7724
+ if (found.length > 1) return { kind: "ambiguous", paths: found.map((p) => relative6(cwd, p)) };
7725
+ if (found.length === 1) return { kind: "one", path: found[0] };
7726
+ if (unreadable.length > 0) return { kind: "unreadable", paths: unreadable.map((p) => relative6(cwd, p)) };
7727
+ return { kind: "none" };
7728
+ }
7729
+ function detectIndent(text) {
7730
+ const match = /\n([ \t]+)"/.exec(text);
7731
+ if (match === null) return 0;
7732
+ const whitespace = match[1];
7733
+ return whitespace.includes(" ") ? " " : whitespace.length;
7734
+ }
7735
+ function applyBuildStamp(cwd, id) {
7736
+ const location = findRulesFile(cwd);
7737
+ if (location.kind === "none") return { kind: "skipped", reason: "no guard rules file in this project" };
7738
+ if (location.kind === "unreadable") {
7739
+ return { kind: "skipped", reason: `${location.paths.join(", ")} is not valid JSON \u2014 left untouched` };
7740
+ }
7741
+ if (location.kind === "ambiguous") {
7742
+ return {
7743
+ kind: "skipped",
7744
+ reason: `more than one candidate guard rules file (${location.paths.join(", ")}) \u2014 none was stamped`
7745
+ };
7746
+ }
7747
+ const file = relative6(cwd, location.path);
7748
+ let text;
7749
+ try {
7750
+ text = readFileSync14(location.path, "utf8");
7751
+ } catch (err) {
7752
+ return { kind: "skipped", reason: `${file} could not be read (${err.message})` };
7753
+ }
7754
+ let parsed;
7755
+ try {
7756
+ parsed = JSON.parse(text);
7757
+ } catch {
7758
+ return { kind: "skipped", reason: `${file} is not valid JSON \u2014 left untouched` };
7759
+ }
7760
+ if (!isRuleBundle(parsed)) return { kind: "skipped", reason: `${file} is not a guard rules bundle` };
7761
+ const bundle = parsed;
7762
+ const current = readBuildStamp(bundle);
7763
+ if (id === null ? !hasRawBuildStamp(bundle) : current === id) return { kind: "unchanged", file };
7764
+ const namespace = { ...bundle[BUILD_STAMP_KEY] ?? {} };
7765
+ if (id === null) delete namespace.build_id;
7766
+ else namespace.build_id = id;
7767
+ if (Object.keys(namespace).length === 0) delete bundle[BUILD_STAMP_KEY];
7768
+ else bundle[BUILD_STAMP_KEY] = namespace;
7769
+ const serialised = JSON.stringify(bundle, null, detectIndent(text)) + (text.endsWith("\n") ? "\n" : "");
7770
+ const temporary = `${location.path}.patchstack-${process.pid}.tmp`;
7771
+ try {
7772
+ const mode = lstatSync3(location.path).mode & 4095;
7773
+ writeFileSync8(temporary, serialised, { encoding: "utf8", mode });
7774
+ chmodSync(temporary, mode);
7775
+ renameSync(temporary, location.path);
7776
+ } catch (err) {
7777
+ try {
7778
+ unlinkSync(temporary);
7779
+ } catch {
7780
+ }
7781
+ return { kind: "skipped", reason: `${file} could not be written (${err.message})` };
7782
+ }
7783
+ return id === null ? { kind: "cleared", file } : { kind: "stamped", file, id };
7784
+ }
7785
+
7786
+ // src/build-hook.ts
7787
+ var INSTALL_AND_BUILD_EVENTS = /* @__PURE__ */ new Set([
7788
+ "preinstall",
7789
+ "install",
7790
+ "postinstall",
7791
+ "prepare",
7792
+ "prebuild",
7793
+ "build",
7794
+ "postbuild"
7795
+ ]);
7796
+ function isInstallOrBuildHook(env = process.env) {
7797
+ const event = env.npm_lifecycle_event;
7798
+ return typeof event === "string" && INSTALL_AND_BUILD_EVENTS.has(event);
7799
+ }
7800
+ function isPreBundleBuildHook(env = process.env) {
7801
+ const event = env.npm_lifecycle_event;
7802
+ if (event === "prebuild") return true;
7803
+ if (event !== "build") return false;
7804
+ return /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(env.npm_lifecycle_script ?? "");
7805
+ }
7806
+ function undeliveredReportLines(err, config, cwd) {
7807
+ const lines = [`patchstack: manifest not reported \u2014 ${err.message}`];
7808
+ if (err.code === "UNAUTHORIZED") {
7809
+ const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
7810
+ if (hasCredential) {
7811
+ lines.push(
7812
+ "patchstack: the credential this environment holds was rejected. If `login` rotated it, set the new value as PATCHSTACK_API_KEY here."
7813
+ );
7814
+ } else {
7815
+ lines.push(
7816
+ `patchstack: PATCHSTACK_API_KEY is not set, and neither ${SECRET_CONFIG_FILENAME} nor .patchstackrc.json in ${cwd} holds a credential.`,
7817
+ `patchstack: ${SECRET_CONFIG_FILENAME} is git-ignored, so a clean checkout never has it. Set PATCHSTACK_API_KEY in the platform's environment so builds can report.`
7818
+ );
7819
+ }
7820
+ }
7821
+ lines.push(
7822
+ "patchstack: continuing the build. Patchstack keeps the last manifest it accepted for this site until a scan that can report."
7823
+ );
7824
+ return lines;
7825
+ }
7826
+
7827
+ // src/input-map-id.ts
7828
+ import { createHash as createHash6 } from "crypto";
7829
+ var NonCanonicalInputMap = class extends Error {
7830
+ };
7831
+ function canonicalString(value) {
7832
+ let out = '"';
7833
+ for (const character of value) {
7834
+ const code = character.codePointAt(0);
7835
+ if (character === '"') out += '\\"';
7836
+ else if (character === "\\") out += "\\\\";
7837
+ else if (code < 32) out += `\\u${code.toString(16).padStart(4, "0")}`;
7838
+ else out += character;
7839
+ }
7840
+ return `${out}"`;
7841
+ }
7842
+ function canonicalText(value) {
7843
+ if (value === null) return "null";
7844
+ if (value === true) return "true";
7845
+ if (value === false) return "false";
7846
+ if (typeof value === "string") return canonicalString(value);
7847
+ if (typeof value === "number") {
7848
+ if (!Number.isSafeInteger(value)) {
7849
+ throw new NonCanonicalInputMap("the input map contains a number outside the safe-integer form");
7850
+ }
7851
+ return String(value === 0 ? 0 : value);
7852
+ }
7853
+ if (Array.isArray(value)) return `[${value.map(canonicalText).join(",")}]`;
7854
+ if (value !== null && typeof value === "object") {
7855
+ const record = value;
7856
+ const keys = Object.keys(record).sort();
7857
+ return `{${keys.map((key) => `${canonicalString(key)}:${canonicalText(record[key])}`).join(",")}}`;
7858
+ }
7859
+ throw new NonCanonicalInputMap(`the input map contains a value with no JSON form (${typeof value})`);
7860
+ }
7861
+ function inputMapBuildId(map) {
7862
+ let material = map;
7863
+ if (map !== null && typeof map === "object" && !Array.isArray(map)) {
7864
+ const record = map;
7865
+ const { build_id: _buildId, ...mapDocument } = record;
7866
+ const coverage = record.coverage;
7867
+ if (coverage !== null && typeof coverage === "object" && !Array.isArray(coverage)) {
7868
+ const { analysisMs: _analysisMs, rssBytes: _rssBytes, peakRssBytes: _peakRssBytes, ...stableCoverage } = coverage;
7869
+ material = { ...mapDocument, coverage: stableCoverage };
7870
+ } else material = mapDocument;
7871
+ }
7872
+ return createHash6("sha256").update("patchstack-input-map-v1\0", "utf8").update(canonicalText(material), "utf8").digest("hex");
7873
+ }
7874
+
7611
7875
  // src/map-command.ts
7612
7876
  async function runMap(flags) {
7613
7877
  const cwd = getStringFlag(flags, "dir") ?? process.cwd();
@@ -7652,7 +7916,7 @@ async function runMap(flags) {
7652
7916
  const json = JSON.stringify(map, null, 2);
7653
7917
  const out = getStringFlag(flags, "out");
7654
7918
  if (out) {
7655
- writeFileSync8(out, json);
7919
+ writeFileSync9(out, json);
7656
7920
  console.error(`patchstack: wrote ${out}`);
7657
7921
  } else if (flags.get("upload") !== true) {
7658
7922
  console.log(json);
@@ -7668,7 +7932,25 @@ async function runMap(flags) {
7668
7932
  cliSiteUuid: getStringFlag(flags, "site-uuid"),
7669
7933
  cliEndpoint: getStringFlag(flags, "endpoint")
7670
7934
  });
7671
- const outcome = await postInputMap(config, map);
7935
+ let buildId = null;
7936
+ if (isPreBundleBuildHook()) {
7937
+ const candidate = inputMapBuildId(map);
7938
+ const stamp = applyBuildStamp(cwd, candidate);
7939
+ if (stamp.kind === "stamped" || stamp.kind === "unchanged") {
7940
+ buildId = candidate;
7941
+ console.error(`patchstack: bound this map to ${stamp.file} (${candidate.slice(0, 12)}).`);
7942
+ } else {
7943
+ const reason = stamp.kind === "skipped" ? stamp.reason : "the rules file did not retain the map identity";
7944
+ console.error(
7945
+ `patchstack: could not bind this map to the runtime guard \u2014 ${reason}. Rules generated from these coordinates will detect only, not block.`
7946
+ );
7947
+ }
7948
+ } else {
7949
+ console.error(
7950
+ "patchstack: no runtime binding recorded \u2014 run `map --upload` in a prebuild hook before the bundler, so rules generated from these coordinates can be tied to the runtime guard. Until then they detect only, not block."
7951
+ );
7952
+ }
7953
+ const outcome = await postInputMap(config, map, buildId);
7672
7954
  if (outcome.result === "stored") {
7673
7955
  console.error(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
7674
7956
  } else if (outcome.result === "unchanged") {
@@ -7683,7 +7965,7 @@ async function runMap(flags) {
7683
7965
  }
7684
7966
 
7685
7967
  // src/setup.ts
7686
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
7968
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
7687
7969
  import path13 from "path";
7688
7970
  var SCAN_COMMAND = "patchstack-connect scan";
7689
7971
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
@@ -7701,9 +7983,16 @@ function appendHook(existing, command) {
7701
7983
  }
7702
7984
  return `${existing} && ${command}`;
7703
7985
  }
7986
+ function prependHook(existing, command) {
7987
+ if (existing === void 0 || existing.trim().length === 0) return command;
7988
+ const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7989
+ if (new RegExp(`^\\s*${escaped}\\s*(?:;|\\|\\|)`).test(existing)) return existing;
7990
+ const remaining = existing.split(/\s*&&\s*/).filter((part) => part.trim() !== command && part.trim().length > 0);
7991
+ return [command, ...remaining].join(" && ");
7992
+ }
7704
7993
  function wireBuildScripts(cwd, packageManager) {
7705
7994
  const target = path13.join(cwd, "package.json");
7706
- const raw = readFileSync14(target, "utf8");
7995
+ const raw = readFileSync15(target, "utf8");
7707
7996
  const pkg = JSON.parse(raw);
7708
7997
  const scripts = pkg.scripts ?? {};
7709
7998
  const build = scripts.build;
@@ -7718,10 +8007,7 @@ function wireBuildScripts(cwd, packageManager) {
7718
8007
  }
7719
8008
  scripts.postinstall = postinstall;
7720
8009
  } else if (packageManager === "bun") {
7721
- let nextBuild = build;
7722
- if (!nextBuild.includes(SCAN_COMMAND)) {
7723
- nextBuild = `${SCAN_COMMAND} && ${nextBuild}`;
7724
- }
8010
+ let nextBuild = prependHook(build, SCAN_COMMAND);
7725
8011
  if (!nextBuild.includes(MARK_BUILD_COMMAND)) {
7726
8012
  nextBuild = `${nextBuild} && ${MARK_BUILD_COMMAND}`;
7727
8013
  }
@@ -7735,7 +8021,7 @@ function wireBuildScripts(cwd, packageManager) {
7735
8021
  scripts.postinstall = postinstall;
7736
8022
  scripts.build = nextBuild;
7737
8023
  } else {
7738
- const prebuild = appendHook(scripts.prebuild, SCAN_COMMAND);
8024
+ const prebuild = prependHook(scripts.prebuild, SCAN_COMMAND);
7739
8025
  const postbuild = appendHook(scripts.postbuild, MARK_BUILD_COMMAND);
7740
8026
  if (prebuild === scripts.prebuild && postbuild === scripts.postbuild && postinstall === scripts.postinstall) {
7741
8027
  return {
@@ -7752,7 +8038,7 @@ function wireBuildScripts(cwd, packageManager) {
7752
8038
  const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
7753
8039
  const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
7754
8040
  const trailingNewline = raw.endsWith("\n") ? "\n" : "";
7755
- writeFileSync9(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
8041
+ writeFileSync10(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
7756
8042
  if (build === void 0 || build.trim().length === 0) {
7757
8043
  return {
7758
8044
  changed: true,
@@ -7771,41 +8057,6 @@ function wireBuildScripts(cwd, packageManager) {
7771
8057
  };
7772
8058
  }
7773
8059
 
7774
- // src/build-hook.ts
7775
- var INSTALL_AND_BUILD_EVENTS = /* @__PURE__ */ new Set([
7776
- "preinstall",
7777
- "install",
7778
- "postinstall",
7779
- "prepare",
7780
- "prebuild",
7781
- "build",
7782
- "postbuild"
7783
- ]);
7784
- function isInstallOrBuildHook(env = process.env) {
7785
- const event = env.npm_lifecycle_event;
7786
- return typeof event === "string" && INSTALL_AND_BUILD_EVENTS.has(event);
7787
- }
7788
- function undeliveredReportLines(err, config, cwd) {
7789
- const lines = [`patchstack: manifest not reported \u2014 ${err.message}`];
7790
- if (err.code === "UNAUTHORIZED") {
7791
- const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
7792
- if (hasCredential) {
7793
- lines.push(
7794
- "patchstack: the credential this environment holds was rejected. If `login` rotated it, set the new value as PATCHSTACK_API_KEY here."
7795
- );
7796
- } else {
7797
- lines.push(
7798
- `patchstack: PATCHSTACK_API_KEY is not set, and neither ${SECRET_CONFIG_FILENAME} nor .patchstackrc.json in ${cwd} holds a credential.`,
7799
- `patchstack: ${SECRET_CONFIG_FILENAME} is git-ignored, so a clean checkout never has it. Set PATCHSTACK_API_KEY in the platform's environment so builds can report.`
7800
- );
7801
- }
7802
- }
7803
- lines.push(
7804
- "patchstack: continuing the build. Patchstack keeps the last manifest it accepted for this site until a scan that can report."
7805
- );
7806
- return lines;
7807
- }
7808
-
7809
8060
  // src/cli.ts
7810
8061
  var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
7811
8062
 
@@ -7833,7 +8084,8 @@ Usage:
7833
8084
  with how the link was established). Best-effort static
7834
8085
  analysis \u2014 reports the DETECTED surface, with
7835
8086
  coverage counters. Prints JSON (--out writes a
7836
- file; --follow-symlinks leaves the project dir).
8087
+ file; a pre-bundle --upload also stamps the existing
8088
+ guard rules file; --follow-symlinks leaves the project dir).
7837
8089
  Uses the app's own TypeScript
7838
8090
  patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
7839
8091
  with an existing site UUID
@@ -8255,6 +8507,10 @@ async function runScan(args, options = {}) {
8255
8507
  }
8256
8508
  return 0;
8257
8509
  }
8510
+ if (isPreBundleBuildHook()) {
8511
+ const cleared = applyBuildStamp(process.cwd(), null);
8512
+ if (cleared.kind === "cleared") console.log(`Removed the previous map binding from ${cleared.file}.`);
8513
+ }
8258
8514
  if (config.widget) {
8259
8515
  reportSourceMarker(detectStack(payload.packages).framework);
8260
8516
  }
@@ -8553,7 +8809,7 @@ async function runGuide(args) {
8553
8809
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
8554
8810
  console.log("");
8555
8811
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
8556
- console.log(readFileSync15(guidePath, "utf8"));
8812
+ console.log(readFileSync16(guidePath, "utf8"));
8557
8813
  return 0;
8558
8814
  }
8559
8815
  async function runSetup(args) {
@@ -8747,7 +9003,7 @@ async function runMarkBuild(args) {
8747
9003
  let marked = 0;
8748
9004
  let widgetTouched = 0;
8749
9005
  for (const file of files) {
8750
- const before = readFileSync15(file, "utf8");
9006
+ const before = readFileSync16(file, "utf8");
8751
9007
  let after = injectMarker(before, snippet);
8752
9008
  if (widgetUuid !== null) {
8753
9009
  const ensured = ensureWidgetInHtml(after, widgetUuid);
@@ -8757,7 +9013,7 @@ async function runMarkBuild(args) {
8757
9013
  after = ensured.html;
8758
9014
  }
8759
9015
  if (after !== before) {
8760
- writeFileSync10(file, after);
9016
+ writeFileSync11(file, after);
8761
9017
  marked += 1;
8762
9018
  }
8763
9019
  }