@patchstack/connect 0.5.5 → 0.5.7

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,10 +1,131 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
5
- import path16 from "path";
4
+ import { readFileSync as readFileSync19 } from "fs";
5
+ import path17 from "path";
6
6
  import { createRequire as createRequire2 } from "module";
7
7
 
8
+ // src/safe-file.ts
9
+ import {
10
+ chmodSync,
11
+ closeSync,
12
+ fsyncSync,
13
+ lstatSync,
14
+ mkdirSync,
15
+ openSync,
16
+ readFileSync,
17
+ realpathSync,
18
+ renameSync,
19
+ statSync,
20
+ unlinkSync,
21
+ writeFileSync
22
+ } from "fs";
23
+ import { randomUUID } from "crypto";
24
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
25
+ function pathError(message) {
26
+ return Object.assign(new Error(message), { code: "EINVAL" });
27
+ }
28
+ function existingEntry(path18) {
29
+ try {
30
+ return lstatSync(path18);
31
+ } catch (error) {
32
+ if (error.code === "ENOENT") return null;
33
+ throw error;
34
+ }
35
+ }
36
+ function assertWritableTarget(target) {
37
+ const entry = existingEntry(target);
38
+ if (entry?.isSymbolicLink()) throw pathError(`refusing to replace symbolic link: ${target}`);
39
+ if (entry && !entry.isFile()) throw pathError(`refusing to replace non-file path: ${target}`);
40
+ return entry;
41
+ }
42
+ function atomicWriteFileSync(target, content, options = {}) {
43
+ const current = assertWritableTarget(target);
44
+ const preserveMode = options.preserveMode !== false;
45
+ const exactMode = options.mode ?? (preserveMode && current ? current.mode & 4095 : options.createMode);
46
+ const temporary = join(dirname(target), `.${randomUUID()}.patchstack-tmp`);
47
+ let descriptor = null;
48
+ try {
49
+ descriptor = openSync(temporary, "wx", exactMode ?? 438);
50
+ writeFileSync(descriptor, content, options.encoding);
51
+ fsyncSync(descriptor);
52
+ closeSync(descriptor);
53
+ descriptor = null;
54
+ if (exactMode !== void 0) chmodSync(temporary, exactMode);
55
+ try {
56
+ renameSync(temporary, target);
57
+ } catch (error) {
58
+ const code = error.code;
59
+ if (process.platform !== "win32" || code !== "EEXIST" && code !== "EPERM") throw error;
60
+ try {
61
+ unlinkSync(target);
62
+ } catch (unlinkError) {
63
+ if (unlinkError.code !== "ENOENT") throw unlinkError;
64
+ }
65
+ renameSync(temporary, target);
66
+ }
67
+ } catch (error) {
68
+ if (descriptor !== null) {
69
+ try {
70
+ closeSync(descriptor);
71
+ } catch {
72
+ }
73
+ }
74
+ try {
75
+ unlinkSync(temporary);
76
+ } catch {
77
+ }
78
+ throw error;
79
+ }
80
+ }
81
+ function projectTarget(cwd, target, allowRoot = false) {
82
+ const logicalRoot = resolve(cwd);
83
+ const logicalTarget = isAbsolute(target) ? resolve(target) : resolve(logicalRoot, target);
84
+ const rel = relative(logicalRoot, logicalTarget);
85
+ if (!allowRoot && rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
86
+ throw pathError(`path is outside the project: ${target}`);
87
+ }
88
+ const root = realpathSync(logicalRoot);
89
+ return { root, path: rel === "" ? root : join(root, rel), parts: rel === "" ? [] : rel.split(sep) };
90
+ }
91
+ function assertProjectParents(root, parts) {
92
+ let current = root;
93
+ for (const part of parts.slice(0, -1)) {
94
+ current = join(current, part);
95
+ const entry = existingEntry(current);
96
+ if (entry === null) throw Object.assign(new Error(`project directory does not exist: ${current}`), { code: "ENOENT" });
97
+ if (entry.isSymbolicLink() || !entry.isDirectory()) {
98
+ throw pathError(`project path contains a non-directory or symbolic link: ${current}`);
99
+ }
100
+ }
101
+ }
102
+ function ensureProjectDirectorySync(cwd, target) {
103
+ const resolved = projectTarget(cwd, target, true);
104
+ let current = resolved.root;
105
+ for (const part of resolved.parts) {
106
+ current = join(current, part);
107
+ let entry = existingEntry(current);
108
+ if (entry === null) {
109
+ mkdirSync(current);
110
+ entry = lstatSync(current);
111
+ }
112
+ if (entry.isSymbolicLink() || !entry.isDirectory()) {
113
+ throw pathError(`project path contains a non-directory or symbolic link: ${current}`);
114
+ }
115
+ }
116
+ return resolved.path;
117
+ }
118
+ function writeProjectFileSync(cwd, target, content, options = {}) {
119
+ const resolved = projectTarget(cwd, target);
120
+ assertProjectParents(resolved.root, resolved.parts);
121
+ atomicWriteFileSync(resolved.path, content, options);
122
+ }
123
+ function copyProjectFileSync(cwd, source, target) {
124
+ const content = readFileSync(source);
125
+ const sourceMode = statSync(source).mode & 4095;
126
+ writeProjectFileSync(cwd, target, content, { createMode: sourceMode });
127
+ }
128
+
8
129
  // src/parsers/index.ts
9
130
  import { access } from "fs/promises";
10
131
  import path5 from "path";
@@ -1241,12 +1362,134 @@ function detectHostingPlatform(env = process.env) {
1241
1362
  return { platform: null, evidence: [] };
1242
1363
  }
1243
1364
 
1365
+ // src/protect/safe-origin.js
1366
+ function isSafeOrigin(value) {
1367
+ try {
1368
+ const u = new URL(value);
1369
+ if (u.protocol === "https:") return true;
1370
+ return u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]" || u.hostname === "::1");
1371
+ } catch {
1372
+ return false;
1373
+ }
1374
+ }
1375
+
1376
+ // src/endpoint-policy.ts
1377
+ function assertConnectableEndpoint(config, target = config.endpoint) {
1378
+ if (config.endpointTrusted === false) {
1379
+ throw new PatchstackError(
1380
+ "The custom endpoint in .patchstackrc.json must be confirmed with --endpoint or PATCHSTACK_ENDPOINT before it can be used.",
1381
+ "CONFIG_INVALID"
1382
+ );
1383
+ }
1384
+ if (safeRemoteUrl(config.endpoint) === null) {
1385
+ throw new PatchstackError(
1386
+ "Patchstack endpoints must use HTTPS, except for localhost development endpoints.",
1387
+ "CONFIG_INVALID"
1388
+ );
1389
+ }
1390
+ let endpointOrigin;
1391
+ let targetOrigin;
1392
+ try {
1393
+ endpointOrigin = new URL(config.endpoint).origin;
1394
+ targetOrigin = new URL(target).origin;
1395
+ } catch (cause) {
1396
+ throw new PatchstackError("The configured Patchstack endpoint is not a valid URL.", "CONFIG_INVALID", cause);
1397
+ }
1398
+ if (safeRemoteUrl(target) === null || targetOrigin !== endpointOrigin) {
1399
+ throw new PatchstackError(
1400
+ "A Patchstack request resolved outside the configured endpoint origin.",
1401
+ "CONFIG_INVALID"
1402
+ );
1403
+ }
1404
+ }
1405
+ function safeRemoteUrl(value) {
1406
+ if (typeof value !== "string" || value.length === 0 || value.length > 2048) return null;
1407
+ try {
1408
+ const parsed = new URL(value);
1409
+ if (!isSafeOrigin(parsed.toString()) || parsed.username !== "" || parsed.password !== "") return null;
1410
+ return parsed.toString();
1411
+ } catch {
1412
+ return null;
1413
+ }
1414
+ }
1415
+ function safeDisplayString(value, maxLength = 512) {
1416
+ if (typeof value !== "string") return null;
1417
+ const clean = value.replace(/\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))/g, "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim();
1418
+ if (clean.length === 0) return null;
1419
+ return clean.slice(0, maxLength);
1420
+ }
1421
+ function isCanonicalUuid(value) {
1422
+ return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
1423
+ }
1424
+ function isCredential(value) {
1425
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096) return false;
1426
+ for (let i = 0; i < value.length; i += 1) {
1427
+ const code = value.charCodeAt(i);
1428
+ if (code < 33 || code > 126) return false;
1429
+ }
1430
+ const separator = value.lastIndexOf("-");
1431
+ return separator > 0 && separator < value.length - 1 && /^\d+$/.test(value.slice(separator + 1));
1432
+ }
1433
+ function isHeaderValue(value, maxLength = 8192) {
1434
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength) return false;
1435
+ for (let i = 0; i < value.length; i += 1) {
1436
+ const code = value.charCodeAt(i);
1437
+ if (code < 33 || code > 126) return false;
1438
+ }
1439
+ return true;
1440
+ }
1441
+
1442
+ // src/bounded-response.ts
1443
+ var MAX_CONTROL_RESPONSE_BYTES = 256 * 1024;
1444
+ async function readBoundedText(response, maxBytes = MAX_CONTROL_RESPONSE_BYTES) {
1445
+ const stated2 = Number(response.headers?.get?.("content-length"));
1446
+ if (Number.isFinite(stated2) && stated2 > maxBytes) {
1447
+ void response.body?.cancel().catch(() => {
1448
+ });
1449
+ throw new Error(`response exceeds ${maxBytes} bytes`);
1450
+ }
1451
+ const stream = response.body;
1452
+ if (stream === null || typeof stream.getReader !== "function") {
1453
+ if (stated2 === 0) return "";
1454
+ throw new Error("response body cannot be read with a size bound");
1455
+ }
1456
+ const reader = stream.getReader();
1457
+ const chunks = [];
1458
+ let total = 0;
1459
+ try {
1460
+ for (; ; ) {
1461
+ const { done, value } = await reader.read();
1462
+ if (done) break;
1463
+ total += value.byteLength;
1464
+ if (total > maxBytes) {
1465
+ void reader.cancel().catch(() => {
1466
+ });
1467
+ throw new Error(`response exceeds ${maxBytes} bytes`);
1468
+ }
1469
+ chunks.push(value);
1470
+ }
1471
+ } finally {
1472
+ reader.releaseLock();
1473
+ }
1474
+ const bytes = new Uint8Array(total);
1475
+ let offset = 0;
1476
+ for (const chunk of chunks) {
1477
+ bytes.set(chunk, offset);
1478
+ offset += chunk.byteLength;
1479
+ }
1480
+ return new TextDecoder().decode(bytes);
1481
+ }
1482
+ async function readBoundedJson(response, maxBytes = MAX_CONTROL_RESPONSE_BYTES) {
1483
+ const text = await readBoundedText(response, maxBytes);
1484
+ return JSON.parse(text);
1485
+ }
1486
+
1244
1487
  // src/pulse-token.ts
1245
1488
  var TOKEN_SKEW_MS = 6e4;
1246
1489
  function buildTokenUrl(manifestEndpoint) {
1247
1490
  const url = new URL(manifestEndpoint);
1248
- const path17 = url.pathname.replace(/\/$/, "");
1249
- url.pathname = path17.endsWith("/manifest") ? `${path17.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
1491
+ const path18 = url.pathname.replace(/\/$/, "");
1492
+ url.pathname = path18.endsWith("/manifest") ? `${path18.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
1250
1493
  url.search = "";
1251
1494
  url.hash = "";
1252
1495
  return url.toString();
@@ -1258,22 +1501,48 @@ function parsePulseAuth(credential) {
1258
1501
  if (!/^\d+$/.test(clientId)) return null;
1259
1502
  return { clientId, clientSecret: credential.slice(0, index) };
1260
1503
  }
1261
- var cached = null;
1262
- var inflight = null;
1263
- function clearPulseToken() {
1264
- cached = null;
1504
+ var cached = /* @__PURE__ */ new Map();
1505
+ var inflight = /* @__PURE__ */ new Map();
1506
+ function cacheKey(tokenUrl, credential) {
1507
+ return `${tokenUrl}
1508
+ ${credential}`;
1509
+ }
1510
+ function clearPulseToken(config) {
1511
+ if (!config || typeof config.pulseAuth !== "string") {
1512
+ cached.clear();
1513
+ inflight.clear();
1514
+ return;
1515
+ }
1516
+ try {
1517
+ const key = cacheKey(buildTokenUrl(config.endpoint), config.pulseAuth);
1518
+ cached.delete(key);
1519
+ inflight.delete(key);
1520
+ } catch {
1521
+ }
1265
1522
  }
1266
1523
  async function getPulseToken(config, fetchImpl = fetch) {
1267
1524
  if (typeof config.pulseAuth !== "string" || config.pulseAuth.length === 0) return null;
1268
- if (cached !== null && Date.now() < cached.expiresAt - TOKEN_SKEW_MS) {
1269
- return cached.token;
1525
+ if (config.endpointTrusted === false) return null;
1526
+ let tokenUrl;
1527
+ try {
1528
+ tokenUrl = buildTokenUrl(config.endpoint);
1529
+ } catch {
1530
+ return null;
1531
+ }
1532
+ if (!isSafeOrigin(tokenUrl)) return null;
1533
+ const key = cacheKey(tokenUrl, config.pulseAuth);
1534
+ const existing = cached.get(key);
1535
+ if (existing !== void 0 && Date.now() < existing.expiresAt - TOKEN_SKEW_MS) {
1536
+ return existing.token;
1270
1537
  }
1271
- if (inflight !== null) return inflight;
1538
+ const pending = inflight.get(key);
1539
+ if (pending !== void 0) return pending;
1272
1540
  const credentials = parsePulseAuth(config.pulseAuth);
1273
1541
  if (credentials === null) return null;
1274
- inflight = (async () => {
1542
+ let exchange;
1543
+ exchange = (async () => {
1275
1544
  try {
1276
- const response = await fetchImpl(buildTokenUrl(config.endpoint), {
1545
+ const response = await fetchImpl(tokenUrl, {
1277
1546
  method: "POST",
1278
1547
  headers: {
1279
1548
  "Content-Type": "application/json",
@@ -1288,25 +1557,27 @@ async function getPulseToken(config, fetchImpl = fetch) {
1288
1557
  signal: AbortSignal.timeout(config.timeoutMs)
1289
1558
  });
1290
1559
  if (!response.ok) return null;
1291
- const body = await response.json();
1292
- if (typeof body.access_token !== "string" || body.access_token.length === 0) return null;
1560
+ const body = await readBoundedJson(response);
1561
+ if (!isHeaderValue(body.access_token)) return null;
1293
1562
  const expiresIn = Number(body.expires_in);
1294
1563
  const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
1295
- cached = { token: body.access_token, expiresAt: Date.now() + ttlMs };
1564
+ cached.set(key, { token: body.access_token, expiresAt: Date.now() + ttlMs });
1296
1565
  return body.access_token;
1297
1566
  } catch {
1298
1567
  return null;
1299
1568
  } finally {
1300
- inflight = null;
1569
+ if (inflight.get(key) === exchange) inflight.delete(key);
1301
1570
  }
1302
1571
  })();
1303
- return inflight;
1572
+ inflight.set(key, exchange);
1573
+ return exchange;
1304
1574
  }
1305
1575
  async function pulseAuthHeader(config, fetchImpl = fetch) {
1306
1576
  const token = await getPulseToken(config, fetchImpl);
1307
1577
  return token === null ? {} : { Authorization: `Bearer ${token}` };
1308
1578
  }
1309
1579
  async function pulseFetch(config, url, init, fetchImpl = fetch) {
1580
+ assertConnectableEndpoint(config, url);
1310
1581
  const send = async () => {
1311
1582
  const auth = await pulseAuthHeader(config, fetchImpl);
1312
1583
  const response = await fetchImpl(url, {
@@ -1317,7 +1588,7 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
1317
1588
  };
1318
1589
  const first = await send();
1319
1590
  if (first.response.status === 401 && first.authenticated) {
1320
- clearPulseToken();
1591
+ clearPulseToken(config);
1321
1592
  return (await send()).response;
1322
1593
  }
1323
1594
  return first.response;
@@ -1349,12 +1620,13 @@ var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
1349
1620
  var DEFAULT_TIMEOUT_MS = 3e4;
1350
1621
  var CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
1351
1622
  function claimTokenHeader(config) {
1352
- return typeof config.claimToken === "string" && config.claimToken !== "" ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
1623
+ return config.endpointTrusted !== false && isHeaderValue(config.claimToken) ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
1353
1624
  }
1354
1625
  function claimOutcomeLines(claim3, config) {
1355
1626
  if (typeof config.claimToken !== "string" || config.claimToken === "") return [];
1356
1627
  if (claim3?.state === "claimed" || claim3?.state === "owned-by-you") {
1357
- const dashboard = typeof claim3.dashboard_url === "string" && claim3.dashboard_url !== "" ? [`Dashboard: ${claim3.dashboard_url}`] : [];
1628
+ const dashboardUrl = safeRemoteUrl(claim3.dashboard_url);
1629
+ const dashboard = dashboardUrl === null ? [] : [`Dashboard: ${dashboardUrl}`];
1358
1630
  return [
1359
1631
  claim3.state === "claimed" ? "Connected to your Patchstack account." : "This site is already connected to your Patchstack account.",
1360
1632
  ...dashboard
@@ -1385,8 +1657,8 @@ function authFailureMessage(status, config) {
1385
1657
  }
1386
1658
  function buildRulesUrl(manifestEndpoint, siteUuid) {
1387
1659
  const url = new URL(manifestEndpoint);
1388
- const path17 = url.pathname.replace(/\/$/, "");
1389
- url.pathname = path17.endsWith("/manifest") ? `${path17.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
1660
+ const path18 = url.pathname.replace(/\/$/, "");
1661
+ url.pathname = path18.endsWith("/manifest") ? `${path18.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
1390
1662
  url.search = "";
1391
1663
  url.hash = "";
1392
1664
  return url.toString();
@@ -1401,16 +1673,16 @@ function buildSettingsUrl(endpoint, siteUuid) {
1401
1673
  }
1402
1674
  function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
1403
1675
  const url = new URL(manifestEndpoint);
1404
- const path17 = url.pathname.replace(/\/$/, "");
1405
- url.pathname = path17.endsWith("/manifest") ? `${path17.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1676
+ const path18 = url.pathname.replace(/\/$/, "");
1677
+ url.pathname = path18.endsWith("/manifest") ? `${path18.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1406
1678
  url.search = "";
1407
1679
  url.hash = "";
1408
1680
  return url.toString();
1409
1681
  }
1410
1682
  function buildInputMapUrl(manifestEndpoint, siteUuid) {
1411
1683
  const url = new URL(manifestEndpoint);
1412
- const path17 = url.pathname.replace(/\/$/, "");
1413
- url.pathname = path17.endsWith("/manifest") ? `${path17.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1684
+ const path18 = url.pathname.replace(/\/$/, "");
1685
+ url.pathname = path18.endsWith("/manifest") ? `${path18.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1414
1686
  url.search = "";
1415
1687
  url.hash = "";
1416
1688
  return url.toString();
@@ -1450,7 +1722,7 @@ async function postInputMap(config, map, buildId) {
1450
1722
  if (!response.ok) {
1451
1723
  return { result: "failed", message: `Patchstack returned ${response.status}.` };
1452
1724
  }
1453
- const body = await response.json();
1725
+ const body = await readBoundedJson(response);
1454
1726
  const revision = Number(body.revision);
1455
1727
  if ((body.result === "stored" || body.result === "unchanged") && Number.isInteger(revision) && revision > 0) {
1456
1728
  return { result: body.result, revision };
@@ -1487,9 +1759,9 @@ async function postPackageRemoved(config) {
1487
1759
  if (!response.ok) {
1488
1760
  return { result: "failed", message: `Patchstack returned ${response.status}.` };
1489
1761
  }
1490
- const body = await response.json();
1762
+ const body = await readBoundedJson(response);
1491
1763
  if (body.status === "deleted" || body.status === "flagged") {
1492
- return { result: body.status, message: body.message ?? null };
1764
+ return { result: body.status, message: safeDisplayString(body.message) };
1493
1765
  }
1494
1766
  return { result: "failed", message: "Patchstack returned an unexpected response." };
1495
1767
  } catch {
@@ -1501,6 +1773,7 @@ async function fetchSiteStatus(config) {
1501
1773
  const url = new URL(buildSettingsUrl(config.endpoint, config.siteUuid));
1502
1774
  url.searchParams.set("t", Date.now().toString());
1503
1775
  try {
1776
+ assertConnectableEndpoint(config, url.toString());
1504
1777
  const response = await fetch(url.toString(), {
1505
1778
  method: "GET",
1506
1779
  headers: {
@@ -1533,6 +1806,42 @@ function validationFields(body) {
1533
1806
  if (typeof errors !== "object" || errors === null || Array.isArray(errors)) return [];
1534
1807
  return Object.keys(errors);
1535
1808
  }
1809
+ function manifestResponse(body, provisioning) {
1810
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return null;
1811
+ const value = body;
1812
+ if (typeof value.stored !== "boolean") return null;
1813
+ const uuid = value.uuid === void 0 ? void 0 : isCanonicalUuid(value.uuid) ? value.uuid : null;
1814
+ if (uuid === null || provisioning && uuid === void 0) return null;
1815
+ if (value.api_key !== void 0 && !isCredential(value.api_key)) return null;
1816
+ if (value.manifest_id !== void 0 && (!Number.isInteger(value.manifest_id) || Number(value.manifest_id) <= 0)) return null;
1817
+ const claimValue = value.claim;
1818
+ let claim3;
1819
+ if (claimValue !== void 0) {
1820
+ if (typeof claimValue !== "object" || claimValue === null || Array.isArray(claimValue)) return null;
1821
+ const raw = claimValue;
1822
+ if (!["claimed", "owned-by-you", "owned-by-other", "rejected"].includes(String(raw.state))) return null;
1823
+ const dashboardUrl = raw.dashboard_url === void 0 || raw.dashboard_url === null ? null : safeRemoteUrl(raw.dashboard_url);
1824
+ if (raw.dashboard_url !== void 0 && raw.dashboard_url !== null && dashboardUrl === null) return null;
1825
+ claim3 = {
1826
+ state: raw.state,
1827
+ ...Number.isInteger(raw.site_id) ? { site_id: Number(raw.site_id) } : {},
1828
+ ...dashboardUrl !== null ? { dashboard_url: dashboardUrl } : {},
1829
+ ...raw.reason === "expired" || raw.reason === "invalid" ? { reason: raw.reason } : {}
1830
+ };
1831
+ }
1832
+ const result = {
1833
+ stored: value.stored,
1834
+ ...uuid !== void 0 ? { uuid } : {},
1835
+ ...typeof value.api_key === "string" ? { api_key: value.api_key } : {},
1836
+ ...Number.isInteger(value.manifest_id) ? { manifest_id: Number(value.manifest_id) } : {},
1837
+ ...safeDisplayString(value.checksum, 256) !== null ? { checksum: safeDisplayString(value.checksum, 256) } : {},
1838
+ ...safeDisplayString(value.reason) !== null ? { reason: safeDisplayString(value.reason) } : {},
1839
+ ...safeDisplayString(value.message) !== null ? { message: safeDisplayString(value.message) } : {},
1840
+ ...safeDisplayString(value.error) !== null ? { error: safeDisplayString(value.error) } : {},
1841
+ ...claim3 !== void 0 ? { claim: claim3 } : {}
1842
+ };
1843
+ return result;
1844
+ }
1536
1845
  function environmentRejected(err) {
1537
1846
  return err instanceof PatchstackError && err.code === "VALIDATION_ERROR" && err.fields.includes("environment");
1538
1847
  }
@@ -1551,6 +1860,7 @@ async function postManifestWithEnvironmentFallback(config, payload) {
1551
1860
  async function postManifest(config, payload) {
1552
1861
  const url = buildEndpointUrl(config.endpoint, config.siteUuid);
1553
1862
  const timeoutMs = config.timeoutMs;
1863
+ assertConnectableEndpoint(config, url);
1554
1864
  let response;
1555
1865
  try {
1556
1866
  response = await pulseFetch(config, url, {
@@ -1578,25 +1888,27 @@ async function postManifest(config, payload) {
1578
1888
  cause
1579
1889
  );
1580
1890
  }
1581
- const text = await response.text();
1582
- let body = null;
1891
+ let text = "";
1892
+ let parsed = null;
1583
1893
  try {
1584
- body = text.length > 0 ? JSON.parse(text) : null;
1894
+ text = await readBoundedText(response);
1895
+ parsed = text.length > 0 ? JSON.parse(text) : null;
1585
1896
  } catch {
1586
- body = null;
1897
+ parsed = null;
1587
1898
  }
1899
+ const body = manifestResponse(parsed, config.siteUuid === null);
1588
1900
  if (response.status === 404) {
1589
1901
  throw new PatchstackError(
1590
- body?.error ?? "Site not found. Check that your site UUID is correct and that the app is registered as a Pulse app in your Patchstack dashboard.",
1902
+ safeDisplayString(parsed?.error) ?? "Site not found. Check that your site UUID is correct and that the app is registered as a Pulse app in your Patchstack dashboard.",
1591
1903
  "SITE_NOT_FOUND"
1592
1904
  );
1593
1905
  }
1594
1906
  if (response.status === 422) {
1595
1907
  const refused2 = new PatchstackError(
1596
- body?.message ?? "Patchstack rejected the manifest payload (validation failed).",
1908
+ safeDisplayString(parsed?.message) ?? "Patchstack rejected the manifest payload (validation failed).",
1597
1909
  "VALIDATION_ERROR"
1598
1910
  );
1599
- refused2.fields = validationFields(body);
1911
+ refused2.fields = validationFields(parsed);
1600
1912
  throw refused2;
1601
1913
  }
1602
1914
  const refused = authFailureMessage(response.status, config);
@@ -1605,12 +1917,12 @@ async function postManifest(config, payload) {
1605
1917
  }
1606
1918
  if (response.status < 200 || response.status >= 300) {
1607
1919
  throw new PatchstackError(
1608
- `Patchstack returned ${response.status}: ${text.slice(0, 200)}`,
1920
+ `Patchstack returned ${response.status}: ${safeDisplayString(text, 200) ?? "no response details"}`,
1609
1921
  "SERVER_ERROR"
1610
1922
  );
1611
1923
  }
1612
1924
  if (body === null) {
1613
- throw new PatchstackError("Patchstack returned an empty response.", "SERVER_ERROR");
1925
+ throw new PatchstackError("Patchstack returned an unexpected response.", "SERVER_ERROR");
1614
1926
  }
1615
1927
  return body;
1616
1928
  }
@@ -1623,7 +1935,7 @@ function isTimeoutError(cause) {
1623
1935
 
1624
1936
  // src/demo.ts
1625
1937
  import { readFile as readFile7 } from "fs/promises";
1626
- import { join } from "path";
1938
+ import { join as join2 } from "path";
1627
1939
  var NODE_SERIALIZE_DEMO = {
1628
1940
  name: "node-serialize",
1629
1941
  packageName: "node-serialize",
@@ -1692,7 +2004,7 @@ Add this deliberately vulnerable package first: npm install --save-exact ${scena
1692
2004
  }
1693
2005
  async function readPersistedSiteUuid(cwd) {
1694
2006
  try {
1695
- const value = JSON.parse(await readFile7(join(cwd, ".patchstackrc.json"), "utf8")).siteUuid;
2007
+ const value = JSON.parse(await readFile7(join2(cwd, ".patchstackrc.json"), "utf8")).siteUuid;
1696
2008
  return typeof value === "string" && value.length > 0 ? value : null;
1697
2009
  } catch {
1698
2010
  return null;
@@ -1777,7 +2089,7 @@ async function waitForDemoRule(manifestEndpoint, siteUuid, scenario, options = {
1777
2089
  const pollIntervalMs = options.pollIntervalMs ?? 1e3;
1778
2090
  const requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
1779
2091
  const fetchFn = options.fetchFn ?? fetch;
1780
- const sleep = options.sleep ?? ((ms) => new Promise((resolve4) => setTimeout(resolve4, ms)));
2092
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
1781
2093
  const now = options.now ?? Date.now;
1782
2094
  const started = now();
1783
2095
  const rulesUrl = buildRulesUrl(manifestEndpoint, siteUuid);
@@ -1826,11 +2138,12 @@ function renderDemoTestCommands(url, scenario) {
1826
2138
  }
1827
2139
 
1828
2140
  // src/config.ts
1829
- import { readFile as readFile9, writeFile, chmod } from "fs/promises";
2141
+ import { readFile as readFile9, chmod, lstat as lstat2, open, rename, unlink } from "fs/promises";
1830
2142
  import path8 from "path";
2143
+ import { randomUUID as randomUUID2 } from "crypto";
1831
2144
 
1832
2145
  // src/environment.ts
1833
- import { readFileSync } from "fs";
2146
+ import { readFileSync as readFileSync2 } from "fs";
1834
2147
  import path6 from "path";
1835
2148
  var set = (value) => value !== void 0 && value !== "";
1836
2149
  var DISCRIMINATORS = [
@@ -1872,7 +2185,7 @@ var HOSTED_BUILDER_PACKAGES = [
1872
2185
  function detectHostedBuilder(cwd) {
1873
2186
  let declared;
1874
2187
  try {
1875
- const pkg = JSON.parse(readFileSync(path6.join(cwd, "package.json"), "utf8"));
2188
+ const pkg = JSON.parse(readFileSync2(path6.join(cwd, "package.json"), "utf8"));
1876
2189
  declared = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1877
2190
  } catch {
1878
2191
  return null;
@@ -2098,7 +2411,15 @@ async function resolveConfig(options) {
2098
2411
  const fromSecretFile = await readSecretFile(options.cwd);
2099
2412
  const fromEnv = readEnv();
2100
2413
  const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
2101
- const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
2414
+ const endpointRaw = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
2415
+ const endpointTrusted = endpointRaw === DEFAULT_ENDPOINT || options.cliEndpoint !== void 0 || fromEnv.endpoint !== void 0 || fromFile.endpoint === void 0;
2416
+ const endpoint = safeRemoteUrl(endpointRaw);
2417
+ if (endpoint === null) {
2418
+ throw new PatchstackError(
2419
+ "Patchstack endpoints must be valid HTTPS URLs, except for localhost development endpoints.",
2420
+ "CONFIG_INVALID"
2421
+ );
2422
+ }
2102
2423
  const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2103
2424
  const environmentRaw = fromEnv.environment ?? fromFile.environment;
2104
2425
  if (environmentRaw !== void 0 && !isEnvironment(environmentRaw)) {
@@ -2110,7 +2431,7 @@ async function resolveConfig(options) {
2110
2431
  const inferred = environmentRaw === void 0 ? inferEnvironment(process.env, detectHostedBuilder(options.cwd)) : null;
2111
2432
  const environment = environmentRaw ?? inferred.environment;
2112
2433
  const environmentEvidence = inferred?.evidence ?? [];
2113
- if (siteUuid !== null && siteUuid.length > 0 && !isUuid(siteUuid)) {
2434
+ if (siteUuid !== null && siteUuid.length > 0 && !isCanonicalUuid(siteUuid)) {
2114
2435
  throw new PatchstackError(
2115
2436
  `Site UUID "${siteUuid}" does not look like a valid UUID.`,
2116
2437
  "CONFIG_INVALID"
@@ -2133,6 +2454,7 @@ async function resolveConfig(options) {
2133
2454
  siteUrl: identity.url,
2134
2455
  siteName: identity.name,
2135
2456
  endpoint,
2457
+ endpointTrusted,
2136
2458
  timeoutMs,
2137
2459
  environment,
2138
2460
  environmentEvidence,
@@ -2143,13 +2465,13 @@ async function resolveConfig(options) {
2143
2465
  async function writeConfigFile(cwd, config) {
2144
2466
  const target = path8.join(cwd, CONFIG_FILENAME);
2145
2467
  const content = JSON.stringify(config, null, 2) + "\n";
2146
- await writeFile(target, content, "utf8");
2468
+ await writeAtomicFile(target, content, 420);
2147
2469
  return target;
2148
2470
  }
2149
2471
  var SECRET_CONFIG_FILENAME = SECRET_FILENAME;
2150
2472
  async function writeSecretFile(cwd, secrets) {
2151
2473
  const target = path8.join(cwd, SECRET_FILENAME);
2152
- await writeFile(target, JSON.stringify(secrets, null, 2) + "\n", { encoding: "utf8", mode: 384 });
2474
+ await writeAtomicFile(target, JSON.stringify(secrets, null, 2) + "\n", 384);
2153
2475
  try {
2154
2476
  await chmod(target, 384);
2155
2477
  } catch {
@@ -2157,6 +2479,39 @@ async function writeSecretFile(cwd, secrets) {
2157
2479
  const ignore = await ensureIgnored(cwd, SECRET_FILENAME);
2158
2480
  return { path: target, ...ignore };
2159
2481
  }
2482
+ async function writeAtomicFile(target, content, mode) {
2483
+ const temporary = `${target}.${process.pid}.${randomUUID2()}.tmp`;
2484
+ let handle;
2485
+ try {
2486
+ handle = await open(temporary, "wx", mode);
2487
+ await handle.writeFile(content, "utf8");
2488
+ await handle.sync();
2489
+ await handle.close();
2490
+ handle = void 0;
2491
+ try {
2492
+ await rename(temporary, target);
2493
+ } catch (error) {
2494
+ const code = error.code;
2495
+ if (process.platform !== "win32" || code !== "EEXIST" && code !== "EPERM") throw error;
2496
+ try {
2497
+ await unlink(target);
2498
+ } catch (unlinkError) {
2499
+ if (unlinkError.code !== "ENOENT") throw unlinkError;
2500
+ }
2501
+ await rename(temporary, target);
2502
+ }
2503
+ } catch (error) {
2504
+ try {
2505
+ await handle?.close();
2506
+ } catch {
2507
+ }
2508
+ try {
2509
+ await unlink(temporary);
2510
+ } catch {
2511
+ }
2512
+ throw error;
2513
+ }
2514
+ }
2160
2515
  async function ensureIgnored(cwd, entry) {
2161
2516
  const target = path8.join(cwd, ".gitignore");
2162
2517
  let existing = "";
@@ -2175,7 +2530,7 @@ async function ensureIgnored(cwd, entry) {
2175
2530
  ${entry}
2176
2531
  `;
2177
2532
  try {
2178
- await writeFile(target, existing + block, "utf8");
2533
+ await writeAtomicFile(target, existing + block, 420);
2179
2534
  } catch (err) {
2180
2535
  const code = err.code;
2181
2536
  return { ignored: false, reason: `.gitignore could not be written (${code ?? "unknown error"})` };
@@ -2245,8 +2600,15 @@ async function readJsonFile(cwd, filename) {
2245
2600
  const target = path8.join(cwd, filename);
2246
2601
  let raw;
2247
2602
  try {
2603
+ if ((await lstat2(target)).isSymbolicLink()) {
2604
+ throw new PatchstackError(
2605
+ `Could not read ${target}: configuration files must not be symbolic links.`,
2606
+ "CONFIG_INVALID"
2607
+ );
2608
+ }
2248
2609
  raw = await readFile9(target, "utf8");
2249
2610
  } catch (err) {
2611
+ if (err instanceof PatchstackError) throw err;
2250
2612
  if (err.code === "ENOENT") {
2251
2613
  return {};
2252
2614
  }
@@ -2291,15 +2653,12 @@ function readEnv() {
2291
2653
  environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
2292
2654
  };
2293
2655
  }
2294
- function isUuid(value) {
2295
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
2296
- }
2297
2656
  function isEnvironment(value) {
2298
2657
  return value === "production" || value === "sandbox" || value === "local";
2299
2658
  }
2300
2659
 
2301
2660
  // src/mark-build.ts
2302
- import { existsSync, readFileSync as readFileSync2, readdirSync, realpathSync, statSync, writeFileSync } from "fs";
2661
+ import { existsSync, readFileSync as readFileSync3, readdirSync, realpathSync as realpathSync2, statSync as statSync2 } from "fs";
2303
2662
  import path9 from "path";
2304
2663
 
2305
2664
  // src/stack.ts
@@ -2401,7 +2760,7 @@ function staleBuildAge(files, startedAt) {
2401
2760
  let newest = null;
2402
2761
  for (const file of files) {
2403
2762
  try {
2404
- const at = statSync(file).mtimeMs;
2763
+ const at = statSync2(file).mtimeMs;
2405
2764
  if (newest === null || at > newest) newest = at;
2406
2765
  } catch {
2407
2766
  continue;
@@ -2432,20 +2791,20 @@ function buildDirCandidates(options = {}) {
2432
2791
  function resolveBuildDir(cwd, override, options = {}) {
2433
2792
  if (override !== void 0 && override !== "") {
2434
2793
  const abs = path9.resolve(cwd, override);
2435
- return existsSync(abs) && statSync(abs).isDirectory() ? abs : null;
2794
+ return existsSync(abs) && statSync2(abs).isDirectory() ? abs : null;
2436
2795
  }
2437
2796
  let boundary;
2438
2797
  try {
2439
- boundary = realpathSync(cwd);
2798
+ boundary = realpathSync2(cwd);
2440
2799
  } catch {
2441
2800
  return null;
2442
2801
  }
2443
2802
  for (const candidate of buildDirCandidates(options)) {
2444
2803
  const abs = path9.resolve(cwd, candidate);
2445
- if (!existsSync(abs) || !statSync(abs).isDirectory()) continue;
2804
+ if (!existsSync(abs) || !statSync2(abs).isDirectory()) continue;
2446
2805
  let real;
2447
2806
  try {
2448
- real = realpathSync(abs);
2807
+ real = realpathSync2(abs);
2449
2808
  } catch {
2450
2809
  continue;
2451
2810
  }
@@ -2513,12 +2872,16 @@ function productionGate(framework) {
2513
2872
  function hasJsxShell(framework) {
2514
2873
  return framework !== null && JSX_SHELL_FRAMEWORKS.has(framework);
2515
2874
  }
2516
- function buildSourceMarkerSnippet(framework) {
2875
+ function buildSourceMarkerSnippet(framework, checksum = null) {
2517
2876
  const gate = productionGate(framework);
2877
+ const statements = ["window.__PATCHSTACK_PROD__=true;"];
2878
+ if (checksum !== null && checksum !== "") {
2879
+ statements.push(`window.__PATCHSTACK_BUILD__=${JSON.stringify(checksum)};`);
2880
+ }
2518
2881
  return `{${gate} && (
2519
2882
  <script
2520
2883
  ${MARKER_ATTR}="true"
2521
- dangerouslySetInnerHTML={{ __html: 'window.__PATCHSTACK_PROD__=true;' }}
2884
+ dangerouslySetInnerHTML={{ __html: '${statements.join("")}' }}
2522
2885
  />
2523
2886
  )}`;
2524
2887
  }
@@ -2581,7 +2944,7 @@ function findJsxShellAnchor(source, tagName) {
2581
2944
  }
2582
2945
  return null;
2583
2946
  }
2584
- function ensureMarkerInJsxShell(source, framework) {
2947
+ function ensureMarkerInJsxShell(source, framework, checksum = null) {
2585
2948
  if (!hasJsxShell(framework)) {
2586
2949
  return { source, action: "unsupported" };
2587
2950
  }
@@ -2589,7 +2952,7 @@ function ensureMarkerInJsxShell(source, framework) {
2589
2952
  if (stripped.includes(PROD_MARKER_GLOBAL)) {
2590
2953
  return { source: stripped, action: "manual" };
2591
2954
  }
2592
- const block = (indent) => [REGION_OPEN, ...buildSourceMarkerSnippet(framework).split("\n"), REGION_CLOSE].map((line) => `${indent}${line}`).join("\n");
2955
+ const block = (indent) => [REGION_OPEN, ...buildSourceMarkerSnippet(framework, checksum).split("\n"), REGION_CLOSE].map((line) => `${indent}${line}`).join("\n");
2593
2956
  for (const tagName of ["head", "body"]) {
2594
2957
  const anchor = findJsxShellAnchor(stripped, tagName);
2595
2958
  if (anchor === null) continue;
@@ -2604,30 +2967,30 @@ ${block(indent)}${separator}${remainder}`,
2604
2967
  }
2605
2968
  return { source: stripped, action: "no-anchor" };
2606
2969
  }
2607
- function ensureSourceMarker(cwd, shell, framework) {
2970
+ function ensureSourceMarker(cwd, shell, framework, checksum = null) {
2608
2971
  if (shell === null) {
2609
2972
  return { shell: null, source: "", action: "no-anchor" };
2610
2973
  }
2611
2974
  const file = path9.resolve(cwd, shell);
2612
- const before = readFileSync2(file, "utf8");
2613
- const result = ensureMarkerInJsxShell(before, framework);
2975
+ const before = readFileSync3(file, "utf8");
2976
+ const result = ensureMarkerInJsxShell(before, framework, checksum);
2614
2977
  if (result.source !== before) {
2615
- writeFileSync(file, result.source);
2978
+ writeProjectFileSync(cwd, file, result.source, { encoding: "utf8" });
2616
2979
  }
2617
2980
  return { ...result, shell };
2618
2981
  }
2619
2982
 
2620
2983
  // src/guide.ts
2621
- import { existsSync as existsSync13, readFileSync as readFileSync10, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
2984
+ import { existsSync as existsSync13, readFileSync as readFileSync11, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
2622
2985
  import path12 from "path";
2623
2986
 
2624
2987
  // src/architecture.ts
2625
- import { existsSync as existsSync2, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
2988
+ import { existsSync as existsSync2, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
2626
2989
  import path10 from "path";
2627
2990
 
2628
2991
  // src/map/sources.ts
2629
- import { readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync2, statSync as statSync2 } from "fs";
2630
- import { join as join2, relative, isAbsolute } from "path";
2992
+ import { readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync as realpathSync3, statSync as statSync3 } from "fs";
2993
+ import { join as join3, relative as relative2, isAbsolute as isAbsolute2 } from "path";
2631
2994
 
2632
2995
  // src/map/ast.ts
2633
2996
  function rootIdentifier(node, ts) {
@@ -2836,7 +3199,7 @@ function hasEntrySignal(text) {
2836
3199
  }
2837
3200
  function detectFramework(cwd) {
2838
3201
  try {
2839
- const pkg = JSON.parse(readFileSync3(join2(cwd, "package.json"), "utf8"));
3202
+ const pkg = JSON.parse(readFileSync4(join3(cwd, "package.json"), "utf8"));
2840
3203
  const d = { ...pkg.dependencies, ...pkg.devDependencies };
2841
3204
  if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
2842
3205
  if (d["next"]) return "next";
@@ -2848,11 +3211,11 @@ function detectFramework(cwd) {
2848
3211
  } catch {
2849
3212
  }
2850
3213
  try {
2851
- if (statSync2(join2(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
3214
+ if (statSync3(join3(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
2852
3215
  } catch {
2853
3216
  }
2854
3217
  try {
2855
- if (statSync2(join2(cwd, "functions")).isDirectory()) return "deno-functions";
3218
+ if (statSync3(join3(cwd, "functions")).isDirectory()) return "deno-functions";
2856
3219
  } catch {
2857
3220
  }
2858
3221
  return "unknown";
@@ -2894,7 +3257,7 @@ function probeForSource(dir, budget) {
2894
3257
  for (const e of entries) {
2895
3258
  if (budget.left-- <= 0) return "unknown";
2896
3259
  if (e.isDirectory()) {
2897
- if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join2(dir, e.name));
3260
+ if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join3(dir, e.name));
2898
3261
  } else if (isSourceFile(e.name)) return true;
2899
3262
  else if (e.isSymbolicLink()) return "unknown";
2900
3263
  }
@@ -2907,7 +3270,7 @@ function probeForSource(dir, budget) {
2907
3270
  function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ new Set(), stats = { discovered: 0, unwalked: 0, skippedWithSource: [], skippedUnknown: 0 }, probeBudget = { left: PROBE_BUDGET }) {
2908
3271
  let key;
2909
3272
  try {
2910
- key = realpathSync2(dir);
3273
+ key = realpathSync3(dir);
2911
3274
  } catch {
2912
3275
  stats.unwalked++;
2913
3276
  return out;
@@ -2931,19 +3294,19 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
2931
3294
  const probeable = !SKIP_IS_DEFINITIONAL.has(e.name);
2932
3295
  if (probeable && !e.isDirectory() && e.isSymbolicLink()) stats.skippedUnknown++;
2933
3296
  else if (probeable && e.isDirectory()) {
2934
- const held = probeForSource(join2(dir, e.name), probeBudget);
2935
- if (held === true) stats.skippedWithSource.push(join2(dir, e.name));
3297
+ const held = probeForSource(join3(dir, e.name), probeBudget);
3298
+ if (held === true) stats.skippedWithSource.push(join3(dir, e.name));
2936
3299
  else if (held === "unknown") stats.skippedUnknown++;
2937
3300
  }
2938
3301
  continue;
2939
3302
  }
2940
- const full = join2(dir, e.name);
3303
+ const full = join3(dir, e.name);
2941
3304
  if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
2942
3305
  else if (e.isSymbolicLink()) {
2943
3306
  let st, real;
2944
3307
  try {
2945
- st = statSync2(full);
2946
- real = realpathSync2(full);
3308
+ st = statSync3(full);
3309
+ real = realpathSync3(full);
2947
3310
  } catch {
2948
3311
  stats.unwalked++;
2949
3312
  continue;
@@ -2966,8 +3329,8 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
2966
3329
  }
2967
3330
  function isInside(candidate, boundary) {
2968
3331
  if (candidate === boundary) return true;
2969
- const rel = relative(boundary, candidate);
2970
- return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
3332
+ const rel = relative2(boundary, candidate);
3333
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
2971
3334
  }
2972
3335
  var DEPLOYMENT_SHAPES = [
2973
3336
  // Config first: these are declarations by the project itself, and they survive a build output being
@@ -2993,13 +3356,13 @@ var DEPLOYMENT_SHAPES = [
2993
3356
  function detectDeploymentShapes(cwd, opts = {}) {
2994
3357
  let boundary = opts.boundary ?? cwd;
2995
3358
  try {
2996
- boundary = realpathSync2(boundary);
3359
+ boundary = realpathSync3(boundary);
2997
3360
  } catch {
2998
3361
  }
2999
- const inProject = (path17) => {
3362
+ const inProject = (path18) => {
3000
3363
  if (opts.followOutside) return true;
3001
3364
  try {
3002
- return isInside(realpathSync2(path17), boundary);
3365
+ return isInside(realpathSync3(path18), boundary);
3003
3366
  } catch {
3004
3367
  return false;
3005
3368
  }
@@ -3007,9 +3370,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
3007
3370
  const found = [];
3008
3371
  for (const candidate of DEPLOYMENT_SHAPES) {
3009
3372
  for (const file of candidate.files ?? []) {
3010
- const full = join2(cwd, file);
3373
+ const full = join3(cwd, file);
3011
3374
  try {
3012
- if (statSync2(full).isFile() && inProject(full)) {
3375
+ if (statSync3(full).isFile() && inProject(full)) {
3013
3376
  found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
3014
3377
  break;
3015
3378
  }
@@ -3017,9 +3380,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
3017
3380
  }
3018
3381
  }
3019
3382
  for (const dir of candidate.dirs ?? []) {
3020
- const full = join2(cwd, dir);
3383
+ const full = join3(cwd, dir);
3021
3384
  try {
3022
- if (!statSync2(full).isDirectory() || !inProject(full)) continue;
3385
+ if (!statSync3(full).isDirectory() || !inProject(full)) continue;
3023
3386
  } catch {
3024
3387
  continue;
3025
3388
  }
@@ -3036,7 +3399,7 @@ function holdsFunctionEntry(dir) {
3036
3399
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
3037
3400
  if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
3038
3401
  try {
3039
- for (const nested of readdirSync2(join2(dir, entry.name), { withFileTypes: true })) {
3402
+ for (const nested of readdirSync2(join3(dir, entry.name), { withFileTypes: true })) {
3040
3403
  if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
3041
3404
  }
3042
3405
  } catch {
@@ -3052,7 +3415,7 @@ function holdsSourceFile(dir) {
3052
3415
  if (entry.isFile() && isSourceFile(entry.name)) return true;
3053
3416
  if (entry.isDirectory()) {
3054
3417
  try {
3055
- for (const nested of readdirSync2(join2(dir, entry.name), { withFileTypes: true })) {
3418
+ for (const nested of readdirSync2(join3(dir, entry.name), { withFileTypes: true })) {
3056
3419
  if (nested.isFile() && isSourceFile(nested.name)) return true;
3057
3420
  }
3058
3421
  } catch {
@@ -3065,8 +3428,8 @@ function holdsSourceFile(dir) {
3065
3428
  }
3066
3429
 
3067
3430
  // src/map/surface.ts
3068
- import { readFileSync as readFileSync4 } from "fs";
3069
- import { join as join3 } from "path";
3431
+ import { readFileSync as readFileSync5 } from "fs";
3432
+ import { join as join4 } from "path";
3070
3433
  var STATIC_GENERATORS = [
3071
3434
  { dep: "vite", label: "vite" },
3072
3435
  { dep: "react-scripts", label: "create-react-app" },
@@ -3107,7 +3470,7 @@ var SHAPE_SIGNAL = {
3107
3470
  };
3108
3471
  function readManifest(cwd) {
3109
3472
  try {
3110
- const parsed = JSON.parse(readFileSync4(join3(cwd, "package.json"), "utf8"));
3473
+ const parsed = JSON.parse(readFileSync5(join4(cwd, "package.json"), "utf8"));
3111
3474
  return typeof parsed === "object" && parsed !== null ? parsed : null;
3112
3475
  } catch {
3113
3476
  return null;
@@ -3120,7 +3483,7 @@ function nextExportsStatically(cwd, manifest, ts) {
3120
3483
  for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
3121
3484
  let text;
3122
3485
  try {
3123
- text = readFileSync4(join3(cwd, file), "utf8");
3486
+ text = readFileSync5(join4(cwd, file), "utf8");
3124
3487
  } catch {
3125
3488
  continue;
3126
3489
  }
@@ -3279,7 +3642,7 @@ var SERVING_CALL = /\b(?:createServer|createSecureServer|Bun\.serve|Deno\.serve|
3279
3642
  var MAX_ENTRY_BYTES = 256 * 1024;
3280
3643
  function readManifest2(cwd) {
3281
3644
  try {
3282
- const parsed = JSON.parse(readFileSync5(path10.join(cwd, "package.json"), "utf8"));
3645
+ const parsed = JSON.parse(readFileSync6(path10.join(cwd, "package.json"), "utf8"));
3283
3646
  return typeof parsed === "object" && parsed !== null ? parsed : {};
3284
3647
  } catch {
3285
3648
  return {};
@@ -3313,12 +3676,12 @@ function serverEntries(cwd, manifest) {
3313
3676
  if (!main2.startsWith("..")) candidates.add(main2);
3314
3677
  }
3315
3678
  const found = [];
3316
- for (const relative7 of candidates) {
3317
- const file = path10.join(cwd, relative7);
3679
+ for (const relative8 of candidates) {
3680
+ const file = path10.join(cwd, relative8);
3318
3681
  try {
3319
- if (!existsSync2(file) || !statSync3(file).isFile()) continue;
3320
- const text = readFileSync5(file, "utf8").slice(0, MAX_ENTRY_BYTES);
3321
- if (SERVING_CALL.test(text)) found.push(relative7);
3682
+ if (!existsSync2(file) || !statSync4(file).isFile()) continue;
3683
+ const text = readFileSync6(file, "utf8").slice(0, MAX_ENTRY_BYTES);
3684
+ if (SERVING_CALL.test(text)) found.push(relative8);
3322
3685
  } catch {
3323
3686
  }
3324
3687
  }
@@ -3371,14 +3734,14 @@ function classifyArchitecture(cwd) {
3371
3734
  }
3372
3735
 
3373
3736
  // src/protect/install/util.ts
3374
- import { readFileSync as readFileSync6, existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
3375
- import { join as join4, dirname } from "path";
3737
+ import { readFileSync as readFileSync7, existsSync as existsSync3 } from "fs";
3738
+ import { join as join5, dirname as dirname2 } from "path";
3376
3739
  import { fileURLToPath } from "url";
3377
- var read = (p) => readFileSync6(p, "utf8");
3740
+ var read = (p) => readFileSync7(p, "utf8");
3378
3741
  var log = (msg) => console.log(`patchstack protect: ${msg}`);
3379
3742
  function hasDependency(cwd, name) {
3380
3743
  try {
3381
- const pkg = JSON.parse(read(join4(cwd, "package.json")));
3744
+ const pkg = JSON.parse(read(join5(cwd, "package.json")));
3382
3745
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }[name]);
3383
3746
  } catch {
3384
3747
  return false;
@@ -3390,7 +3753,7 @@ function hasResolvableCredential(cwd, env = process.env) {
3390
3753
  }
3391
3754
  for (const file of [".patchstackrc.local.json", ".patchstackrc.json"]) {
3392
3755
  try {
3393
- const parsed = JSON.parse(read(join4(cwd, file)));
3756
+ const parsed = JSON.parse(read(join5(cwd, file)));
3394
3757
  if (typeof parsed.apiKey === "string" && parsed.apiKey !== "") return true;
3395
3758
  if (typeof parsed.pulseAuth === "string" && parsed.pulseAuth !== "") return true;
3396
3759
  } catch {
@@ -3404,7 +3767,7 @@ function isSiteUuid(value) {
3404
3767
  return typeof value === "string" && UUID_RE.test(value);
3405
3768
  }
3406
3769
  function bakeSiteUuid(cwd, guardRelPath) {
3407
- const rc = join4(cwd, ".patchstackrc.json");
3770
+ const rc = join5(cwd, ".patchstackrc.json");
3408
3771
  if (!existsSync3(rc)) {
3409
3772
  log("no .patchstackrc.json \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
3410
3773
  return false;
@@ -3420,32 +3783,32 @@ function bakeSiteUuid(cwd, guardRelPath) {
3420
3783
  log(".patchstackrc.json siteUuid missing or malformed \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
3421
3784
  return false;
3422
3785
  }
3423
- const guardPath = join4(cwd, guardRelPath);
3786
+ const guardPath = join5(cwd, guardRelPath);
3424
3787
  if (!existsSync3(guardPath)) return false;
3425
3788
  const source = read(guardPath);
3426
3789
  if (!source.includes(SITE_UUID_PLACEHOLDER)) {
3427
3790
  log(`${guardRelPath} site UUID already baked`);
3428
3791
  return false;
3429
3792
  }
3430
- writeFileSync2(guardPath, source.replace(SITE_UUID_PLACEHOLDER, uuid));
3793
+ writeProjectFileSync(cwd, guardPath, source.replace(SITE_UUID_PLACEHOLDER, uuid), { encoding: "utf8" });
3431
3794
  log(`baked site UUID into ${guardRelPath} \u2014 live rules from the Patchstack API`);
3432
3795
  return true;
3433
3796
  }
3434
- var HERE = dirname(fileURLToPath(import.meta.url));
3797
+ var HERE = dirname2(fileURLToPath(import.meta.url));
3435
3798
  function templatesDir() {
3436
- const builtLayout = join4(HERE, "protect", "templates");
3799
+ const builtLayout = join5(HERE, "protect", "templates");
3437
3800
  const candidates = [
3438
3801
  builtLayout,
3439
- join4(HERE, "..", "templates"),
3802
+ join5(HERE, "..", "templates"),
3440
3803
  // source: src/protect/install/ → src/protect/templates
3441
- join4(HERE, "templates")
3804
+ join5(HERE, "templates")
3442
3805
  ];
3443
3806
  return candidates.find((p) => existsSync3(p)) ?? builtLayout;
3444
3807
  }
3445
3808
 
3446
3809
  // src/protect/install/adapters/tanstack-supabase.ts
3447
- import { writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync, copyFileSync } from "fs";
3448
- import { join as join5 } from "path";
3810
+ import { existsSync as existsSync4 } from "fs";
3811
+ import { join as join6 } from "path";
3449
3812
  var CLIENT_TUNNEL = [
3450
3813
  "",
3451
3814
  " // PATCHSTACK: in the browser, tunnel Supabase traffic through the app's own server guard",
@@ -3515,7 +3878,7 @@ function reconcileBlock(s, region, block, legacyConst, insertBefore) {
3515
3878
  return s.replace(insertBefore, block + "\n\n" + insertBefore);
3516
3879
  }
3517
3880
  function detect(cwd) {
3518
- const pkgPath = join5(cwd, "package.json");
3881
+ const pkgPath = join6(cwd, "package.json");
3519
3882
  if (!existsSync4(pkgPath)) return false;
3520
3883
  let pkg;
3521
3884
  try {
@@ -3524,22 +3887,22 @@ function detect(cwd) {
3524
3887
  return false;
3525
3888
  }
3526
3889
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
3527
- return Boolean(deps["@tanstack/react-start"]) && existsSync4(join5(cwd, "src/start.ts")) && existsSync4(join5(cwd, "src/integrations/supabase/client.ts"));
3890
+ return Boolean(deps["@tanstack/react-start"]) && existsSync4(join6(cwd, "src/start.ts")) && existsSync4(join6(cwd, "src/integrations/supabase/client.ts"));
3528
3891
  }
3529
3892
  var GUARD_FILE = "src/integrations/patchstack/guard.ts";
3530
3893
  function scaffold(cwd, opts) {
3531
3894
  const templates = templatesDir();
3532
- const dst = join5(cwd, "src/integrations/patchstack");
3533
- mkdirSync(dst, { recursive: true });
3534
- copyFileSync(join5(templates, "guard.ts"), join5(dst, "guard.ts"));
3895
+ const dst = join6(cwd, "src/integrations/patchstack");
3896
+ ensureProjectDirectorySync(cwd, dst);
3897
+ copyProjectFileSync(cwd, join6(templates, "guard.ts"), join6(dst, "guard.ts"));
3535
3898
  const changed = [GUARD_FILE];
3536
- const rulesDst = join5(dst, "rules.json");
3899
+ const rulesDst = join6(dst, "rules.json");
3537
3900
  if (opts.demo) {
3538
- copyFileSync(join5(templates, "demo-rules.json"), rulesDst);
3901
+ copyProjectFileSync(cwd, join6(templates, "demo-rules.json"), rulesDst);
3539
3902
  changed.push("src/integrations/patchstack/rules.json");
3540
3903
  log("scaffolded guard.ts + rules.json (demo sample rule set)");
3541
3904
  } else if (!existsSync4(rulesDst)) {
3542
- copyFileSync(join5(templates, "rules.json"), rulesDst);
3905
+ copyProjectFileSync(cwd, join6(templates, "rules.json"), rulesDst);
3543
3906
  changed.push("src/integrations/patchstack/rules.json");
3544
3907
  log("scaffolded guard.ts + rules.json (starter rules)");
3545
3908
  } else {
@@ -3548,7 +3911,7 @@ function scaffold(cwd, opts) {
3548
3911
  return changed;
3549
3912
  }
3550
3913
  function patchClient(cwd) {
3551
- const p = join5(cwd, "src/integrations/supabase/client.ts");
3914
+ const p = join6(cwd, "src/integrations/supabase/client.ts");
3552
3915
  const s = read(p);
3553
3916
  if (s.includes("x-ps-target")) {
3554
3917
  log("client.ts already wired");
@@ -3559,12 +3922,12 @@ function patchClient(cwd) {
3559
3922
  log("client.ts anchor not found \u2014 skipping (template changed?)");
3560
3923
  return false;
3561
3924
  }
3562
- writeFileSync3(p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL));
3925
+ writeProjectFileSync(cwd, p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL), { encoding: "utf8" });
3563
3926
  log("patched client.ts (tunnel Supabase through the guard)");
3564
3927
  return true;
3565
3928
  }
3566
3929
  function patchStart(cwd) {
3567
- const p = join5(cwd, "src/start.ts");
3930
+ const p = join6(cwd, "src/start.ts");
3568
3931
  let s = read(p);
3569
3932
  const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
3570
3933
  const exportAnchor = "export const startInstance";
@@ -3596,7 +3959,7 @@ function patchStart(cwd) {
3596
3959
  log("start.ts already wired");
3597
3960
  return false;
3598
3961
  }
3599
- writeFileSync3(p, s);
3962
+ writeProjectFileSync(cwd, p, s, { encoding: "utf8" });
3600
3963
  log("patched start.ts (guard registered as request + function middleware)");
3601
3964
  return true;
3602
3965
  }
@@ -3611,9 +3974,9 @@ function wire(cwd, opts) {
3611
3974
  return { ok: true, changed: [...new Set(changed)] };
3612
3975
  }
3613
3976
  function verify(cwd) {
3614
- const guardPath = join5(cwd, GUARD_FILE);
3615
- const clientPath = join5(cwd, "src/integrations/supabase/client.ts");
3616
- const startPath = join5(cwd, "src/start.ts");
3977
+ const guardPath = join6(cwd, GUARD_FILE);
3978
+ const clientPath = join6(cwd, "src/integrations/supabase/client.ts");
3979
+ const startPath = join6(cwd, "src/start.ts");
3617
3980
  const guard = existsSync4(guardPath) ? read(guardPath) : "";
3618
3981
  const client = existsSync4(clientPath) ? read(clientPath) : "";
3619
3982
  const start = existsSync4(startPath) ? read(startPath) : "";
@@ -3634,11 +3997,11 @@ var tanstackSupabaseAdapter = {
3634
3997
  };
3635
3998
 
3636
3999
  // src/protect/install/adapters/next.ts
3637
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, copyFileSync as copyFileSync2 } from "fs";
3638
- import { join as join6 } from "path";
4000
+ import { existsSync as existsSync5 } from "fs";
4001
+ import { join as join7 } from "path";
3639
4002
  function hasNextDep(cwd) {
3640
4003
  try {
3641
- const pkg = JSON.parse(read(join6(cwd, "package.json")));
4004
+ const pkg = JSON.parse(read(join7(cwd, "package.json")));
3642
4005
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.next);
3643
4006
  } catch {
3644
4007
  return false;
@@ -3647,9 +4010,9 @@ function hasNextDep(cwd) {
3647
4010
  function middlewareInfo(cwd) {
3648
4011
  const candidates = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
3649
4012
  for (const rel of candidates) {
3650
- if (existsSync5(join6(cwd, rel))) return { relDir: rel.includes("/") ? "src" : ".", relFile: rel, exists: true };
4013
+ if (existsSync5(join7(cwd, rel))) return { relDir: rel.includes("/") ? "src" : ".", relFile: rel, exists: true };
3651
4014
  }
3652
- const useSrc = existsSync5(join6(cwd, "src"));
4015
+ const useSrc = existsSync5(join7(cwd, "src"));
3653
4016
  return { relDir: useSrc ? "src" : ".", relFile: useSrc ? "src/middleware.ts" : "middleware.ts", exists: false };
3654
4017
  }
3655
4018
  function detect2(cwd) {
@@ -3661,15 +4024,15 @@ function rulesFile(relDir) {
3661
4024
  function wire2(cwd, opts) {
3662
4025
  const templates = templatesDir();
3663
4026
  const mw = middlewareInfo(cwd);
3664
- const dir = join6(cwd, mw.relDir === "." ? "" : mw.relDir);
3665
- mkdirSync2(dir, { recursive: true });
3666
- const rulesDst = join6(dir, "patchstack.rules.json");
4027
+ const dir = join7(cwd, mw.relDir === "." ? "" : mw.relDir);
4028
+ ensureProjectDirectorySync(cwd, dir);
4029
+ const rulesDst = join7(dir, "patchstack.rules.json");
3667
4030
  const changed = [];
3668
4031
  if (opts.demo || !existsSync5(rulesDst)) {
3669
- copyFileSync2(join6(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
4032
+ copyProjectFileSync(cwd, join7(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
3670
4033
  changed.push(rulesFile(mw.relDir));
3671
4034
  }
3672
- const mwPath = join6(cwd, mw.relFile);
4035
+ const mwPath = join7(cwd, mw.relFile);
3673
4036
  const existing = mw.exists ? read(mwPath) : "";
3674
4037
  if (mw.exists && existing.includes("patchstack-next")) {
3675
4038
  log(`${mw.relFile} already has the Patchstack middleware \u2014 left as-is`);
@@ -3681,7 +4044,7 @@ function wire2(cwd, opts) {
3681
4044
  );
3682
4045
  return { ok: true, changed };
3683
4046
  }
3684
- copyFileSync2(join6(templates, "next-middleware.ts"), mwPath);
4047
+ copyProjectFileSync(cwd, join7(templates, "next-middleware.ts"), mwPath);
3685
4048
  if (!opts.demo) bakeSiteUuid(cwd, mw.relFile);
3686
4049
  changed.push(mw.relFile);
3687
4050
  log(`scaffolded ${mw.relFile} (Patchstack middleware)`);
@@ -3689,8 +4052,8 @@ function wire2(cwd, opts) {
3689
4052
  }
3690
4053
  function verify2(cwd) {
3691
4054
  const mw = middlewareInfo(cwd);
3692
- const present2 = mw.exists && read(join6(cwd, mw.relFile)).includes("patchstack-next");
3693
- const rulesPresent = existsSync5(join6(cwd, rulesFile(mw.relDir)));
4055
+ const present2 = mw.exists && read(join7(cwd, mw.relFile)).includes("patchstack-next");
4056
+ const rulesPresent = existsSync5(join7(cwd, rulesFile(mw.relDir)));
3694
4057
  return {
3695
4058
  wired: present2 && rulesPresent,
3696
4059
  checks: [
@@ -3708,27 +4071,27 @@ var nextAdapter = {
3708
4071
  };
3709
4072
 
3710
4073
  // src/protect/install/adapters/sveltekit.ts
3711
- import { join as join8 } from "path";
4074
+ import { join as join9 } from "path";
3712
4075
 
3713
4076
  // src/protect/install/seam.ts
3714
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, copyFileSync as copyFileSync3 } from "fs";
3715
- import { join as join7, dirname as dirname2 } from "path";
4077
+ import { existsSync as existsSync6 } from "fs";
4078
+ import { join as join8, dirname as dirname3 } from "path";
3716
4079
  function rulesRel(seamRel) {
3717
- const d = dirname2(seamRel);
4080
+ const d = dirname3(seamRel);
3718
4081
  return (d === "." ? "patchstack.rules.json" : `${d}/patchstack.rules.json`).replace(/\\/g, "/");
3719
4082
  }
3720
4083
  function wireSeam(cwd, opts, spec) {
3721
4084
  const templates = templatesDir();
3722
- const existing = spec.candidates.find((c) => existsSync6(join7(cwd, c)));
4085
+ const existing = spec.candidates.find((c) => existsSync6(join8(cwd, c)));
3723
4086
  const seamRel = existing ?? spec.target;
3724
- mkdirSync3(dirname2(join7(cwd, seamRel)), { recursive: true });
3725
- const rulesDst = join7(cwd, rulesRel(seamRel));
4087
+ ensureProjectDirectorySync(cwd, dirname3(join8(cwd, seamRel)));
4088
+ const rulesDst = join8(cwd, rulesRel(seamRel));
3726
4089
  const changed = [];
3727
4090
  if (opts.demo || !existsSync6(rulesDst)) {
3728
- copyFileSync3(join7(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
4091
+ copyProjectFileSync(cwd, join8(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
3729
4092
  changed.push(rulesRel(seamRel));
3730
4093
  }
3731
- const current = existing ? read(join7(cwd, existing)) : "";
4094
+ const current = existing ? read(join8(cwd, existing)) : "";
3732
4095
  if (existing && current.includes(spec.marker)) {
3733
4096
  log(`${existing} already has the Patchstack guard \u2014 left as-is`);
3734
4097
  return { ok: true, changed };
@@ -3737,17 +4100,17 @@ function wireSeam(cwd, opts, spec) {
3737
4100
  log(`existing ${existing} left untouched \u2014 scaffolded ${rulesRel(seamRel)}; ${spec.planHint}`);
3738
4101
  return { ok: true, changed };
3739
4102
  }
3740
- copyFileSync3(join7(templates, spec.templateName), join7(cwd, seamRel));
4103
+ copyProjectFileSync(cwd, join8(templates, spec.templateName), join8(cwd, seamRel));
3741
4104
  if (!opts.demo) bakeSiteUuid(cwd, seamRel);
3742
4105
  changed.push(seamRel);
3743
4106
  log(`scaffolded ${seamRel}`);
3744
4107
  return { ok: true, changed: [...new Set(changed)] };
3745
4108
  }
3746
4109
  function verifySeam(cwd, spec) {
3747
- const existing = spec.candidates.find((c) => existsSync6(join7(cwd, c)));
4110
+ const existing = spec.candidates.find((c) => existsSync6(join8(cwd, c)));
3748
4111
  const seamRel = existing ?? spec.target;
3749
- const present2 = existing ? read(join7(cwd, existing)).includes(spec.marker) : false;
3750
- const rulesPresent = existsSync6(join7(cwd, rulesRel(seamRel)));
4112
+ const present2 = existing ? read(join8(cwd, existing)).includes(spec.marker) : false;
4113
+ const rulesPresent = existsSync6(join8(cwd, rulesRel(seamRel)));
3751
4114
  return {
3752
4115
  wired: present2 && rulesPresent,
3753
4116
  checks: [
@@ -3768,7 +4131,7 @@ var SPEC = {
3768
4131
  };
3769
4132
  function detect3(cwd) {
3770
4133
  try {
3771
- const pkg = JSON.parse(read(join8(cwd, "package.json")));
4134
+ const pkg = JSON.parse(read(join9(cwd, "package.json")));
3772
4135
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }["@sveltejs/kit"]);
3773
4136
  } catch {
3774
4137
  return false;
@@ -3789,7 +4152,7 @@ var sveltekitAdapter = {
3789
4152
  };
3790
4153
 
3791
4154
  // src/protect/install/adapters/astro.ts
3792
- import { join as join9 } from "path";
4155
+ import { join as join10 } from "path";
3793
4156
  var SPEC2 = {
3794
4157
  templateName: "astro-middleware.ts",
3795
4158
  candidates: ["src/middleware.ts", "src/middleware.js", "src/middleware/index.ts", "src/middleware/index.js"],
@@ -3800,7 +4163,7 @@ var SPEC2 = {
3800
4163
  };
3801
4164
  function detect4(cwd) {
3802
4165
  try {
3803
- const pkg = JSON.parse(read(join9(cwd, "package.json")));
4166
+ const pkg = JSON.parse(read(join10(cwd, "package.json")));
3804
4167
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.astro);
3805
4168
  } catch {
3806
4169
  return false;
@@ -3847,8 +4210,8 @@ var nuxtAdapter = {
3847
4210
  };
3848
4211
 
3849
4212
  // src/protect/install/find-app.ts
3850
- import { readFileSync as readFileSync7, existsSync as existsSync7, readdirSync as readdirSync3, lstatSync } from "fs";
3851
- import { join as join10, relative as relative2, dirname as dirname3 } from "path";
4213
+ import { readFileSync as readFileSync8, existsSync as existsSync7, readdirSync as readdirSync3, lstatSync as lstatSync2 } from "fs";
4214
+ import { join as join11, relative as relative3, dirname as dirname4 } from "path";
3852
4215
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3853
4216
  "node_modules",
3854
4217
  "patchstack",
@@ -3869,7 +4232,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3869
4232
  ]);
3870
4233
  var MAX_DEPTH = 8;
3871
4234
  function findAppInstance(cwd, re) {
3872
- const root = existsSync7(join10(cwd, "src")) ? join10(cwd, "src") : cwd;
4235
+ const root = existsSync7(join11(cwd, "src")) ? join11(cwd, "src") : cwd;
3873
4236
  const found = [];
3874
4237
  const walk2 = (dir, depth) => {
3875
4238
  if (depth > MAX_DEPTH) return;
@@ -3881,10 +4244,10 @@ function findAppInstance(cwd, re) {
3881
4244
  }
3882
4245
  for (const name of entries) {
3883
4246
  if (SKIP_DIRS2.has(name) || name.startsWith(".")) continue;
3884
- const p = join10(dir, name);
4247
+ const p = join11(dir, name);
3885
4248
  let st;
3886
4249
  try {
3887
- st = lstatSync(p);
4250
+ st = lstatSync2(p);
3888
4251
  } catch {
3889
4252
  continue;
3890
4253
  }
@@ -3893,7 +4256,7 @@ function findAppInstance(cwd, re) {
3893
4256
  else if (/\.(?:ts|js|mjs|cjs)$/.test(name)) {
3894
4257
  let src;
3895
4258
  try {
3896
- src = readFileSync7(p, "utf8");
4259
+ src = readFileSync8(p, "utf8");
3897
4260
  } catch {
3898
4261
  continue;
3899
4262
  }
@@ -3901,7 +4264,7 @@ function findAppInstance(cwd, re) {
3901
4264
  if (!m) continue;
3902
4265
  const appVar = m[1];
3903
4266
  found.push({
3904
- relPath: relative2(cwd, p).replace(/\\/g, "/"),
4267
+ relPath: relative3(cwd, p).replace(/\\/g, "/"),
3905
4268
  appVar,
3906
4269
  listens: listensOn(src, appVar)
3907
4270
  });
@@ -3940,7 +4303,7 @@ function declaredEntries(cwd) {
3940
4303
  const paths = /* @__PURE__ */ new Set();
3941
4304
  let pkg;
3942
4305
  try {
3943
- pkg = JSON.parse(readFileSync7(join10(cwd, "package.json"), "utf8"));
4306
+ pkg = JSON.parse(readFileSync8(join11(cwd, "package.json"), "utf8"));
3944
4307
  } catch {
3945
4308
  return paths;
3946
4309
  }
@@ -3959,24 +4322,24 @@ function declaredEntries(cwd) {
3959
4322
  return paths;
3960
4323
  }
3961
4324
  function importSpecifier(fromRel, toRel, preserveExtension = false) {
3962
- let spec = relative2(dirname3(fromRel), toRel).replace(/\\/g, "/");
4325
+ let spec = relative3(dirname4(fromRel), toRel).replace(/\\/g, "/");
3963
4326
  if (!preserveExtension) spec = spec.replace(/\.(?:ts|js)$/, "");
3964
4327
  if (!spec.startsWith(".")) spec = `./${spec}`;
3965
4328
  return spec;
3966
4329
  }
3967
4330
 
3968
4331
  // src/protect/install/register.ts
3969
- import { writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
3970
- import { join as join13, resolve as resolve2 } from "path";
4332
+ import { existsSync as existsSync10 } from "fs";
4333
+ import { join as join14, resolve as resolve3 } from "path";
3971
4334
 
3972
4335
  // src/protect/install/generic.ts
3973
- import { readFileSync as readFileSync8, existsSync as existsSync9, mkdirSync as mkdirSync4, copyFileSync as copyFileSync4, readdirSync as readdirSync4, lstatSync as lstatSync2 } from "fs";
3974
- import { join as join12, dirname as dirname5 } from "path";
4336
+ import { readFileSync as readFileSync9, existsSync as existsSync9, readdirSync as readdirSync4, lstatSync as lstatSync3 } from "fs";
4337
+ import { join as join13, dirname as dirname6 } from "path";
3975
4338
 
3976
4339
  // src/protect/install/source-scope.ts
3977
4340
  import { execFileSync } from "child_process";
3978
4341
  import { existsSync as existsSync8 } from "fs";
3979
- import { dirname as dirname4, join as join11, resolve } from "path";
4342
+ import { dirname as dirname5, join as join12, resolve as resolve2 } from "path";
3980
4343
 
3981
4344
  // src/protect/install/node-flags.ts
3982
4345
  var flagName = (token) => token.split("=")[0];
@@ -4265,9 +4628,9 @@ function moduleKindOf(filePath, packageIsModule) {
4265
4628
  return packageIsModule ? "esm" : "cjs";
4266
4629
  }
4267
4630
  function resolvesToGuardModule(specifier, query) {
4268
- const resolved = resolve(dirname4(query.fromFile), specifier);
4269
- if (withoutModuleExtension(resolved) !== join11(query.guardDir, "guard")) return false;
4270
- const existing = GUARD_FILENAMES.filter((name) => existsSync8(join11(query.guardDir, name)));
4631
+ const resolved = resolve2(dirname5(query.fromFile), specifier);
4632
+ if (withoutModuleExtension(resolved) !== join12(query.guardDir, "guard")) return false;
4633
+ const existing = GUARD_FILENAMES.filter((name) => existsSync8(join12(query.guardDir, name)));
4271
4634
  if (existing.length === 0) return false;
4272
4635
  const extension = /(\.[A-Za-z0-9]+)$/.exec(specifier)?.[1] ?? "";
4273
4636
  if (!query.strictExtensions) return true;
@@ -4286,16 +4649,16 @@ function withoutModuleExtension(filePath) {
4286
4649
  // src/protect/install/generic.ts
4287
4650
  function packageIsEsm(cwd) {
4288
4651
  try {
4289
- return JSON.parse(read(join12(cwd, "package.json"))).type === "module";
4652
+ return JSON.parse(read(join13(cwd, "package.json"))).type === "module";
4290
4653
  } catch {
4291
4654
  return false;
4292
4655
  }
4293
4656
  }
4294
4657
  function genericDir(cwd) {
4295
- return existsSync9(join12(cwd, "src")) ? "src/patchstack" : "patchstack";
4658
+ return existsSync9(join13(cwd, "src")) ? "src/patchstack" : "patchstack";
4296
4659
  }
4297
4660
  function hasTypeScript(cwd) {
4298
- return existsSync9(join12(cwd, "tsconfig.json")) || candidateEntries(cwd).some((entry) => /\.tsx?$/.test(entry));
4661
+ return existsSync9(join13(cwd, "tsconfig.json")) || candidateEntries(cwd).some((entry) => /\.tsx?$/.test(entry));
4299
4662
  }
4300
4663
  function genericGuardTarget(cwd) {
4301
4664
  if (hasTypeScript(cwd)) {
@@ -4309,7 +4672,7 @@ function genericGuardTarget(cwd) {
4309
4672
  function genericScaffoldFiles(cwd) {
4310
4673
  const dir = genericDir(cwd);
4311
4674
  const candidates = ["guard.ts", "guard.js", "guard.cjs", "rules.json"];
4312
- return candidates.map((file) => `${dir}/${file}`).filter((relative7) => existsSync9(join12(cwd, relative7)));
4675
+ return candidates.map((file) => `${dir}/${file}`).filter((relative8) => existsSync9(join13(cwd, relative8)));
4313
4676
  }
4314
4677
  function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
4315
4678
  const target = genericGuardTarget(cwd);
@@ -4317,15 +4680,15 @@ function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
4317
4680
  guardFile ??= target.file;
4318
4681
  const templates = templatesDir();
4319
4682
  const dir = genericDir(cwd);
4320
- const dst = join12(cwd, dir);
4321
- mkdirSync4(dst, { recursive: true });
4322
- copyFileSync4(join12(templates, guardTemplate), join12(dst, guardFile));
4683
+ const dst = join13(cwd, dir);
4684
+ ensureProjectDirectorySync(cwd, dst);
4685
+ copyProjectFileSync(cwd, join13(templates, guardTemplate), join13(dst, guardFile));
4323
4686
  const guardRel = `${dir}/${guardFile}`;
4324
4687
  const changed = [guardRel];
4325
4688
  if (!opts.demo) bakeSiteUuid(cwd, guardRel);
4326
- const rulesDst = join12(dst, "rules.json");
4689
+ const rulesDst = join13(dst, "rules.json");
4327
4690
  if (opts.demo || !existsSync9(rulesDst)) {
4328
- copyFileSync4(join12(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
4691
+ copyProjectFileSync(cwd, join13(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
4329
4692
  changed.push(`${dir}/rules.json`);
4330
4693
  }
4331
4694
  return { changed, dir };
@@ -4346,17 +4709,17 @@ function candidateEntries(cwd) {
4346
4709
  "app.ts",
4347
4710
  "app.js"
4348
4711
  ];
4349
- const hits = names.filter((n) => existsSync9(join12(cwd, n)));
4712
+ const hits = names.filter((n) => existsSync9(join13(cwd, n)));
4350
4713
  try {
4351
- const pkg = JSON.parse(read(join12(cwd, "package.json")));
4352
- if (typeof pkg.main === "string" && existsSync9(join12(cwd, pkg.main))) hits.push(pkg.main);
4714
+ const pkg = JSON.parse(read(join13(cwd, "package.json")));
4715
+ if (typeof pkg.main === "string" && existsSync9(join13(cwd, pkg.main))) hits.push(pkg.main);
4353
4716
  } catch {
4354
4717
  }
4355
4718
  return [...new Set(hits)];
4356
4719
  }
4357
4720
  function usesExpress(cwd) {
4358
4721
  try {
4359
- const pkg = JSON.parse(read(join12(cwd, "package.json")));
4722
+ const pkg = JSON.parse(read(join13(cwd, "package.json")));
4360
4723
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.express);
4361
4724
  } catch {
4362
4725
  return false;
@@ -4460,9 +4823,9 @@ function escapeForRegExp(value) {
4460
4823
  function genericVerify(cwd) {
4461
4824
  const dir = genericDir(cwd);
4462
4825
  const target = genericGuardTarget(cwd);
4463
- const present2 = GUARD_FILENAMES.find((name) => existsSync9(join12(cwd, dir, name)));
4826
+ const present2 = GUARD_FILENAMES.find((name) => existsSync9(join13(cwd, dir, name)));
4464
4827
  const scaffolded = present2 !== void 0;
4465
- const imported = scaffolded && guardIsImported(cwd, join12(cwd, dir, present2));
4828
+ const imported = scaffolded && guardIsImported(cwd, join13(cwd, dir, present2));
4466
4829
  return {
4467
4830
  wired: scaffolded && imported,
4468
4831
  checks: [
@@ -4496,9 +4859,9 @@ var NON_RUNTIME_DIRS = /* @__PURE__ */ new Set([
4496
4859
  "__mocks__"
4497
4860
  ]);
4498
4861
  function guardIsImported(cwd, guardPath) {
4499
- const guardDir = dirname5(guardPath);
4862
+ const guardDir = dirname6(guardPath);
4500
4863
  const packageIsModule = packageIsEsm(cwd);
4501
- const root = existsSync9(join12(cwd, "src")) ? join12(cwd, "src") : cwd;
4864
+ const root = existsSync9(join13(cwd, "src")) ? join13(cwd, "src") : cwd;
4502
4865
  let found = false;
4503
4866
  const walk2 = (d, depth) => {
4504
4867
  if (found || depth > 8) return;
@@ -4511,10 +4874,10 @@ function guardIsImported(cwd, guardPath) {
4511
4874
  for (const name of entries) {
4512
4875
  if (found) return;
4513
4876
  if (NON_RUNTIME_DIRS.has(name) || name.startsWith(".")) continue;
4514
- const p = join12(d, name);
4877
+ const p = join13(d, name);
4515
4878
  let st;
4516
4879
  try {
4517
- st = lstatSync2(p);
4880
+ st = lstatSync3(p);
4518
4881
  } catch {
4519
4882
  continue;
4520
4883
  }
@@ -4522,7 +4885,7 @@ function guardIsImported(cwd, guardPath) {
4522
4885
  if (st.isDirectory()) walk2(p, depth + 1);
4523
4886
  else if (/\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(name) && p !== guardPath) {
4524
4887
  try {
4525
- if (importsAndUsesGuard(readFileSync8(p, "utf8"), p, guardDir, packageIsModule)) found = true;
4888
+ if (importsAndUsesGuard(readFileSync9(p, "utf8"), p, guardDir, packageIsModule)) found = true;
4526
4889
  } catch {
4527
4890
  }
4528
4891
  }
@@ -4570,7 +4933,7 @@ function guardTarget(cwd, entryRel, spec) {
4570
4933
  }
4571
4934
  function packageType(cwd) {
4572
4935
  try {
4573
- return JSON.parse(read(join13(cwd, "package.json"))).type;
4936
+ return JSON.parse(read(join14(cwd, "package.json"))).type;
4574
4937
  } catch {
4575
4938
  return void 0;
4576
4939
  }
@@ -4606,8 +4969,8 @@ function bindsGuard(line, maskedLine, importName, call, fileRel, guard) {
4606
4969
  }
4607
4970
  function namesGuardModule(specifier, fileRel, guard) {
4608
4971
  return resolvesToGuardModule(specifier, {
4609
- fromFile: join13(guard.cwd, fileRel),
4610
- guardDir: resolve2(join13(guard.cwd, guard.guardDir)),
4972
+ fromFile: join14(guard.cwd, fileRel),
4973
+ guardDir: resolve3(join14(guard.cwd, guard.guardDir)),
4611
4974
  kind: moduleKindOf(fileRel, packageType(guard.cwd) === "module"),
4612
4975
  strictExtensions: true
4613
4976
  });
@@ -4626,7 +4989,7 @@ function wireRegister(cwd, opts, spec) {
4626
4989
  }
4627
4990
  const target = guardTarget(cwd, entry.relPath, spec);
4628
4991
  const { changed, dir } = scaffoldGeneric(cwd, opts, target.template, target.file);
4629
- const p = join13(cwd, entry.relPath);
4992
+ const p = join14(cwd, entry.relPath);
4630
4993
  const s = read(p);
4631
4994
  const state = wiringState(s, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir });
4632
4995
  if (state.importAtTopLevel && state.callInAppScope) {
@@ -4665,7 +5028,7 @@ function wireRegister(cwd, opts, spec) {
4665
5028
  lines.splice(callIdx + 1, 0, REGION, spec.call(entry.appVar), "// #endregion patchstack");
4666
5029
  }
4667
5030
  const patched = lines.join("\n");
4668
- writeFileSync5(p, patched);
5031
+ writeProjectFileSync(cwd, p, patched, { encoding: "utf8" });
4669
5032
  if (spec.callAfter) {
4670
5033
  const guardIndex = lines.findIndex((line) => line.includes(spec.call(entry.appVar)));
4671
5034
  const early = routesBefore(patched, entry.appVar, guardIndex);
@@ -4677,7 +5040,7 @@ function wireRegister(cwd, opts, spec) {
4677
5040
  }
4678
5041
  const parsed = parses(p);
4679
5042
  if (parsed === false) {
4680
- writeFileSync5(p, s);
5043
+ writeProjectFileSync(cwd, p, s, { encoding: "utf8" });
4681
5044
  log(`${entry.relPath} would not parse after patching \u2014 reverted; ${spec.manualHint}`);
4682
5045
  return { ok: true, changed };
4683
5046
  }
@@ -4696,17 +5059,17 @@ function firstStatementLine(lines) {
4696
5059
  return i;
4697
5060
  }
4698
5061
  function serverIsGuarded(cwd, spec, server, guardDir) {
4699
- const source = read(join13(cwd, server.relPath));
5062
+ const source = read(join14(cwd, server.relPath));
4700
5063
  const state = wiringState(source, spec, server.appVar, server.relPath, { cwd, guardDir });
4701
5064
  if (!state.importAtTopLevel || !state.callInAppScope || !state.ordered) return false;
4702
5065
  return !spec.callAfter || routesBefore(source, server.appVar, state.callIndex).length === 0;
4703
5066
  }
4704
5067
  function verifyRegister(cwd, spec) {
4705
- const dir = existsSync10(join13(cwd, "src")) ? "src/patchstack" : "patchstack";
5068
+ const dir = existsSync10(join14(cwd, "src")) ? "src/patchstack" : "patchstack";
4706
5069
  const entry = findAppInstance(cwd, spec.appRe);
4707
5070
  const target = entry ? guardTarget(cwd, entry.relPath, spec) : null;
4708
- const scaffolded = target ? existsSync10(join13(cwd, dir, target.file)) : false;
4709
- const entrySource = entry ? read(join13(cwd, entry.relPath)) : "";
5071
+ const scaffolded = target ? existsSync10(join14(cwd, dir, target.file)) : false;
5072
+ const entrySource = entry ? read(join14(cwd, entry.relPath)) : "";
4710
5073
  const state = entry ? wiringState(entrySource, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir }) : { importAtTopLevel: false, callInAppScope: false, ordered: false, callIndex: -1, appIndex: -1 };
4711
5074
  const wired = state.importAtTopLevel && state.callInAppScope && state.ordered;
4712
5075
  const early = spec.callAfter && entry ? routesBefore(entrySource, entry.appVar, state.callIndex) : [];
@@ -4797,7 +5160,7 @@ var expressAdapter = {
4797
5160
 
4798
5161
  // src/protect/install/reporting.ts
4799
5162
  import { existsSync as existsSync11 } from "fs";
4800
- import { join as join14 } from "path";
5163
+ import { join as join15 } from "path";
4801
5164
 
4802
5165
  // src/protect/reporting-state.js
4803
5166
  var REPORTING_STATES = Object.freeze([
@@ -4855,7 +5218,7 @@ function explainReportingState(state) {
4855
5218
 
4856
5219
  // src/protect/install/reporting.ts
4857
5220
  function siteIdentity(cwd, env) {
4858
- const rc = join14(cwd, ".patchstackrc.json");
5221
+ const rc = join15(cwd, ".patchstackrc.json");
4859
5222
  let recorded;
4860
5223
  if (existsSync11(rc)) {
4861
5224
  try {
@@ -5006,7 +5369,7 @@ function runVerify(cwd) {
5006
5369
  }
5007
5370
 
5008
5371
  // src/widget.ts
5009
- import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
5372
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
5010
5373
  import path11 from "path";
5011
5374
  var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
5012
5375
  var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
@@ -5051,16 +5414,19 @@ function findSourceShell(cwd) {
5051
5414
  }
5052
5415
  return null;
5053
5416
  }
5054
- function ensureSourceWidget(cwd, siteUuid) {
5055
- const shell = findSourceShell(cwd);
5417
+ function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
5418
+ const shell = findSourceShell(cwd) ?? jsxShell;
5056
5419
  if (shell === null) {
5057
5420
  return { shell: null, action: "no-shell" };
5058
5421
  }
5059
5422
  const file = path11.join(cwd, shell);
5060
- const before = readFileSync9(file, "utf8");
5423
+ if (!existsSync12(file)) {
5424
+ return { shell: null, action: "no-shell" };
5425
+ }
5426
+ const before = readFileSync10(file, "utf8");
5061
5427
  const { html, action } = ensureWidgetInHtml(before, siteUuid);
5062
5428
  if (html !== before) {
5063
- writeFileSync6(file, html);
5429
+ writeProjectFileSync(cwd, file, html, { encoding: "utf8" });
5064
5430
  }
5065
5431
  return { shell, action };
5066
5432
  }
@@ -5150,7 +5516,7 @@ function installCommand(pm) {
5150
5516
  }
5151
5517
  function readPackageJson(cwd) {
5152
5518
  try {
5153
- return JSON.parse(readFileSync10(path12.join(cwd, "package.json"), "utf8"));
5519
+ return JSON.parse(readFileSync11(path12.join(cwd, "package.json"), "utf8"));
5154
5520
  } catch {
5155
5521
  return null;
5156
5522
  }
@@ -5158,7 +5524,7 @@ function readPackageJson(cwd) {
5158
5524
  function installedVersion(cwd, declaredRange) {
5159
5525
  try {
5160
5526
  const pkg = JSON.parse(
5161
- readFileSync10(path12.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
5527
+ readFileSync11(path12.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
5162
5528
  );
5163
5529
  if (typeof pkg.version === "string" && pkg.version.length > 0) {
5164
5530
  return pkg.version;
@@ -5199,10 +5565,10 @@ function findWidgetMarker(cwd, siteUuid) {
5199
5565
  }
5200
5566
  budget -= 1;
5201
5567
  try {
5202
- if (statSync4(full).size > WIDGET_SCAN_MAX_BYTES) {
5568
+ if (statSync5(full).size > WIDGET_SCAN_MAX_BYTES) {
5203
5569
  continue;
5204
5570
  }
5205
- const content = readFileSync10(full, "utf8");
5571
+ const content = readFileSync11(full, "utf8");
5206
5572
  if (!content.includes(WIDGET_NEEDLE2)) {
5207
5573
  continue;
5208
5574
  }
@@ -5225,7 +5591,7 @@ function findProductionMarker(cwd, widgetFileHint) {
5225
5591
  return false;
5226
5592
  }
5227
5593
  try {
5228
- return readFileSync10(path12.join(cwd, widgetFileHint), "utf8").includes(PROD_MARKER_NEEDLE);
5594
+ return readFileSync11(path12.join(cwd, widgetFileHint), "utf8").includes(PROD_MARKER_NEEDLE);
5229
5595
  } catch {
5230
5596
  return false;
5231
5597
  }
@@ -5264,7 +5630,7 @@ async function collectGuideState(cwd) {
5264
5630
  try {
5265
5631
  const config = await resolveConfig({ cwd });
5266
5632
  siteUuid = config.siteUuid;
5267
- if (siteUuid !== null) {
5633
+ if (siteUuid !== null && config.endpointTrusted !== false) {
5268
5634
  claimUrl = buildClaimUrl(config.endpoint, siteUuid);
5269
5635
  }
5270
5636
  if (config.endpoint !== DEFAULT_ENDPOINT) {
@@ -5528,10 +5894,19 @@ import { tmpdir as tmpdir2 } from "os";
5528
5894
  import path14 from "path";
5529
5895
 
5530
5896
  // src/device-flow.ts
5531
- import { readFileSync as readFileSync11, rmSync, writeFileSync as writeFileSync7 } from "fs";
5897
+ import {
5898
+ closeSync as closeSync2,
5899
+ constants,
5900
+ fstatSync,
5901
+ openSync as openSync2,
5902
+ readFileSync as readFileSync12,
5903
+ renameSync as renameSync2,
5904
+ rmSync,
5905
+ writeFileSync as writeFileSync2
5906
+ } from "fs";
5532
5907
  import { tmpdir } from "os";
5533
5908
  import path13 from "path";
5534
- import { createHash as createHash2 } from "crypto";
5909
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
5535
5910
  function baseFrom(manifestEndpoint) {
5536
5911
  const url = new URL(manifestEndpoint);
5537
5912
  const p = url.pathname.replace(/\/$/, "");
@@ -5545,16 +5920,61 @@ function pendingPath(siteUuid, intent) {
5545
5920
  return path13.join(tmpdir(), `patchstack-${intent}-${key}.json`);
5546
5921
  }
5547
5922
  function savePending(siteUuid, intent, pending) {
5548
- writeFileSync7(pendingPath(siteUuid, intent), JSON.stringify(pending), {
5549
- encoding: "utf8",
5550
- mode: 384
5551
- });
5923
+ const target = pendingPath(siteUuid, intent);
5924
+ const temporary = `${target}.${process.pid}.${randomUUID3()}.tmp`;
5925
+ let descriptor = null;
5926
+ try {
5927
+ descriptor = openSync2(temporary, "wx", 384);
5928
+ writeFileSync2(descriptor, JSON.stringify(pending), "utf8");
5929
+ closeSync2(descriptor);
5930
+ descriptor = null;
5931
+ try {
5932
+ renameSync2(temporary, target);
5933
+ } catch (error) {
5934
+ const code = error.code;
5935
+ if (process.platform !== "win32" || code !== "EEXIST" && code !== "EPERM") throw error;
5936
+ try {
5937
+ rmSync(target);
5938
+ } catch (removeError) {
5939
+ if (removeError.code !== "ENOENT") throw removeError;
5940
+ }
5941
+ renameSync2(temporary, target);
5942
+ }
5943
+ } catch (error) {
5944
+ if (descriptor !== null) {
5945
+ try {
5946
+ closeSync2(descriptor);
5947
+ } catch {
5948
+ }
5949
+ }
5950
+ try {
5951
+ rmSync(temporary);
5952
+ } catch {
5953
+ }
5954
+ throw error;
5955
+ }
5552
5956
  }
5553
5957
  function readPending(siteUuid, intent) {
5958
+ let descriptor = null;
5554
5959
  try {
5555
- return JSON.parse(readFileSync11(pendingPath(siteUuid, intent), "utf8"));
5960
+ const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
5961
+ descriptor = openSync2(pendingPath(siteUuid, intent), constants.O_RDONLY | noFollow);
5962
+ const stat2 = fstatSync(descriptor);
5963
+ if (!stat2.isFile()) return null;
5964
+ if (typeof process.getuid === "function" && typeof stat2.uid === "number" && stat2.uid !== process.getuid()) {
5965
+ return null;
5966
+ }
5967
+ if (process.platform !== "win32" && (stat2.mode & 63) !== 0) return null;
5968
+ return JSON.parse(readFileSync12(descriptor, "utf8"));
5556
5969
  } catch {
5557
5970
  return null;
5971
+ } finally {
5972
+ if (descriptor !== null) {
5973
+ try {
5974
+ closeSync2(descriptor);
5975
+ } catch {
5976
+ }
5977
+ }
5558
5978
  }
5559
5979
  }
5560
5980
  function clearPending(siteUuid, intent) {
@@ -5572,17 +5992,25 @@ async function requestDeviceCode(config, intent, conflictMessage, deps = {}) {
5572
5992
  message: "No site UUID configured \u2014 run `patchstack-connect scan` first."
5573
5993
  };
5574
5994
  }
5575
- const base = baseFrom(config.endpoint);
5576
- const started = await pulseFetch(
5577
- config,
5578
- `${base}/device/code`,
5579
- {
5580
- method: "POST",
5581
- headers: { "Content-Type": "application/json", Accept: "application/json" },
5582
- body: JSON.stringify({ site_uuid: config.siteUuid, intent })
5583
- },
5584
- fetchImpl
5585
- );
5995
+ let base;
5996
+ let started;
5997
+ try {
5998
+ base = baseFrom(config.endpoint);
5999
+ assertConnectableEndpoint(config, base);
6000
+ started = await pulseFetch(
6001
+ config,
6002
+ `${base}/device/code`,
6003
+ {
6004
+ method: "POST",
6005
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
6006
+ body: JSON.stringify({ site_uuid: config.siteUuid, intent }),
6007
+ signal: AbortSignal.timeout(config.timeoutMs)
6008
+ },
6009
+ fetchImpl
6010
+ );
6011
+ } catch {
6012
+ return { status: "failed", message: "Could not start the request." };
6013
+ }
5586
6014
  if (started.status === 409) return { status: "conflict", message: conflictMessage };
5587
6015
  if (started.status === 404) {
5588
6016
  return { status: "not-found", message: "Patchstack does not recognise this site UUID." };
@@ -5590,34 +6018,76 @@ async function requestDeviceCode(config, intent, conflictMessage, deps = {}) {
5590
6018
  if (!started.ok) {
5591
6019
  return { status: "failed", message: `Could not start the request (HTTP ${started.status}).` };
5592
6020
  }
5593
- const body = await started.json();
6021
+ let body;
6022
+ try {
6023
+ const raw = await readBoundedJson(started);
6024
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error("unexpected response");
6025
+ body = raw;
6026
+ } catch {
6027
+ return { status: "failed", message: "Patchstack returned an unexpected response." };
6028
+ }
6029
+ const deviceCode = safeDisplayString(body.device_code, 2048);
6030
+ const userCode = safeDisplayString(body.user_code, 128);
6031
+ const expiresIn = Number(body.expires_in);
6032
+ const interval = Number(body.interval);
6033
+ if (deviceCode === null || userCode === null || !Number.isInteger(expiresIn) || expiresIn < 1 || expiresIn > 86400 || !Number.isFinite(interval) || interval < 1 || interval > 300) {
6034
+ return { status: "failed", message: "Patchstack returned an unexpected response." };
6035
+ }
5594
6036
  const pending = {
5595
- deviceCode: body.device_code,
5596
- userCode: body.user_code,
6037
+ deviceCode,
6038
+ userCode,
5597
6039
  // Points at the API, which redirects to the dashboard SPA — the CLI only knows the API origin,
5598
6040
  // and the approval page lives on the app.
5599
- verificationUri: `${base}/device?code=${encodeURIComponent(body.user_code)}`,
5600
- expiresAt: now() + body.expires_in * 1e3,
5601
- intervalMs: Math.max(1, body.interval) * 1e3
6041
+ verificationUri: `${base}/device?code=${encodeURIComponent(userCode)}`,
6042
+ expiresAt: now() + expiresIn * 1e3,
6043
+ intervalMs: interval * 1e3
5602
6044
  };
5603
6045
  savePending(config.siteUuid, intent, pending);
5604
6046
  return { status: "started", pending };
5605
6047
  }
5606
6048
  async function pollDeviceToken(config, intent, pending, deps = {}) {
5607
6049
  const fetchImpl = deps.fetchImpl ?? fetch;
5608
- const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
5609
- method: "POST",
5610
- headers: { "Content-Type": "application/json", Accept: "application/json" },
5611
- body: JSON.stringify({ device_code: pending.deviceCode })
5612
- });
6050
+ let polled;
6051
+ try {
6052
+ const url = `${baseFrom(config.endpoint)}/device/token`;
6053
+ assertConnectableEndpoint(config, url);
6054
+ polled = await fetchImpl(url, {
6055
+ method: "POST",
6056
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
6057
+ body: JSON.stringify({ device_code: pending.deviceCode }),
6058
+ signal: AbortSignal.timeout(config.timeoutMs)
6059
+ });
6060
+ } catch {
6061
+ return { state: "expired" };
6062
+ }
5613
6063
  if (polled.status === 428) return { state: "pending" };
5614
6064
  if (!polled.ok) {
5615
6065
  if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
5616
6066
  return { state: "expired" };
5617
6067
  }
5618
- const body = await polled.json();
6068
+ let raw;
6069
+ try {
6070
+ raw = await readBoundedJson(polled);
6071
+ } catch {
6072
+ raw = null;
6073
+ }
5619
6074
  if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
5620
- return { state: "approved", body };
6075
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return { state: "expired" };
6076
+ const value = raw;
6077
+ const apiKey = value.api_key === void 0 ? void 0 : isCredential(value.api_key) ? value.api_key : null;
6078
+ if (apiKey === null) return { state: "expired" };
6079
+ const dashboardUrl = value.dashboard_url === void 0 ? void 0 : safeRemoteUrl(value.dashboard_url);
6080
+ if (value.dashboard_url !== void 0 && dashboardUrl === null) return { state: "expired" };
6081
+ const account = value.account === void 0 ? void 0 : safeDisplayString(value.account, 256);
6082
+ if (value.account !== void 0 && account === null) return { state: "expired" };
6083
+ return {
6084
+ state: "approved",
6085
+ body: {
6086
+ ...apiKey !== void 0 ? { api_key: apiKey } : {},
6087
+ ...dashboardUrl !== void 0 ? { dashboard_url: dashboardUrl } : {},
6088
+ ...account !== void 0 ? { account } : {}
6089
+ }
6090
+ };
5621
6091
  }
5622
6092
  async function pollUntilApproved(config, intent, pending, deps = {}) {
5623
6093
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
@@ -5719,11 +6189,11 @@ async function claim2(config, onPrompt, deps = {}) {
5719
6189
  }
5720
6190
 
5721
6191
  // src/protect/install/runtime/check.ts
5722
- import { relative as relative3 } from "path";
6192
+ import { relative as relative4 } from "path";
5723
6193
 
5724
6194
  // src/protect/install/runtime/entry.ts
5725
- import { existsSync as existsSync14, readFileSync as readFileSync12, realpathSync as realpathSync3, statSync as statSync5 } from "fs";
5726
- import { join as join15, resolve as resolve3, sep } from "path";
6195
+ import { existsSync as existsSync14, readFileSync as readFileSync13, realpathSync as realpathSync4, statSync as statSync6 } from "fs";
6196
+ import { join as join16, resolve as resolve4, sep as sep2 } from "path";
5727
6197
  var CONVENTIONAL = [
5728
6198
  "server.js",
5729
6199
  "server.mjs",
@@ -5749,31 +6219,31 @@ var CONVENTIONAL = [
5749
6219
  var LOADABLE = /\.(?:js|mjs|cjs)$/;
5750
6220
  function readPackage2(cwd) {
5751
6221
  try {
5752
- const parsed = JSON.parse(readFileSync12(join15(cwd, "package.json"), "utf8"));
6222
+ const parsed = JSON.parse(readFileSync13(join16(cwd, "package.json"), "utf8"));
5753
6223
  return parsed !== null && typeof parsed === "object" ? parsed : null;
5754
6224
  } catch {
5755
6225
  return null;
5756
6226
  }
5757
6227
  }
5758
- function isFile(path17) {
6228
+ function isFile(path18) {
5759
6229
  try {
5760
- return statSync5(path17).isFile();
6230
+ return statSync6(path18).isFile();
5761
6231
  } catch {
5762
6232
  return false;
5763
6233
  }
5764
6234
  }
5765
6235
  function loadable(cwd, candidate) {
5766
- const written = resolve3(cwd, candidate);
6236
+ const written = resolve4(cwd, candidate);
5767
6237
  if (!LOADABLE.test(written) || !isFile(written)) return null;
5768
6238
  let root;
5769
- let path17;
6239
+ let path18;
5770
6240
  try {
5771
- root = realpathSync3(cwd);
5772
- path17 = realpathSync3(written);
6241
+ root = realpathSync4(cwd);
6242
+ path18 = realpathSync4(written);
5773
6243
  } catch {
5774
6244
  return null;
5775
6245
  }
5776
- if (path17 !== root && !path17.startsWith(root + sep)) return null;
6246
+ if (path18 !== root && !path18.startsWith(root + sep2)) return null;
5777
6247
  return written;
5778
6248
  }
5779
6249
  function passable(token) {
@@ -5819,7 +6289,7 @@ function resolveEntry(cwd) {
5819
6289
  }
5820
6290
  return {
5821
6291
  kind: "unavailable",
5822
- reason: existsSync14(join15(cwd, "package.json")) ? 'no directly loadable entry was found \u2014 declare one as a "start" script of the form `node <file>`, or run the check against a built output' : "there is no package.json here"
6292
+ reason: existsSync14(join16(cwd, "package.json")) ? 'no directly loadable entry was found \u2014 declare one as a "start" script of the form `node <file>`, or run the check against a built output' : "there is no package.json here"
5823
6293
  };
5824
6294
  }
5825
6295
 
@@ -5853,8 +6323,8 @@ function preloadPath() {
5853
6323
  // beside this file, in the repository
5854
6324
  ];
5855
6325
  for (const candidate of candidates) {
5856
- const path17 = fileURLToPath2(candidate);
5857
- if (existsSync15(path17)) return path17;
6326
+ const path18 = fileURLToPath2(candidate);
6327
+ if (existsSync15(path18)) return path18;
5858
6328
  }
5859
6329
  return null;
5860
6330
  }
@@ -5888,7 +6358,7 @@ async function askListener(listener, challenge, answer, budgetMs) {
5888
6358
  timeout: budgetMs
5889
6359
  };
5890
6360
  if (listener.scheme === "https") options.agent = new HttpsAgent({ rejectUnauthorized: false });
5891
- return new Promise((resolve4) => {
6361
+ return new Promise((resolve5) => {
5892
6362
  const req = send(options, (res) => {
5893
6363
  const body = [];
5894
6364
  let read2 = 0;
@@ -5902,7 +6372,7 @@ async function askListener(listener, challenge, answer, budgetMs) {
5902
6372
  });
5903
6373
  const finish = () => {
5904
6374
  const text = Buffer.concat(body).toString("utf8").trim();
5905
- resolve4({
6375
+ resolve5({
5906
6376
  ...listener,
5907
6377
  outcome: text === answer ? "traversed" : "answered-without-sentinel",
5908
6378
  detail: text === answer ? void 0 : `answered with ${res.statusCode ?? "?"} and no sentinel`
@@ -5912,7 +6382,7 @@ async function askListener(listener, challenge, answer, budgetMs) {
5912
6382
  res.on("close", finish);
5913
6383
  });
5914
6384
  req.on("timeout", () => req.destroy(new Error("timed out")));
5915
- req.on("error", (err) => resolve4({ ...listener, outcome: "unreachable", detail: err.message }));
6385
+ req.on("error", (err) => resolve5({ ...listener, outcome: "unreachable", detail: err.message }));
5916
6386
  req.end();
5917
6387
  });
5918
6388
  }
@@ -6006,12 +6476,12 @@ async function probeRuntimeTraversal(options) {
6006
6476
  let onFrozen = null;
6007
6477
  const freeze = async () => {
6008
6478
  if (!child.connected) return false;
6009
- return new Promise((resolve4) => {
6479
+ return new Promise((resolve5) => {
6010
6480
  const settle2 = (confirmed2) => {
6011
6481
  clearTimeout(timer);
6012
6482
  onFrozen = null;
6013
6483
  child.removeListener("exit", onGone);
6014
- resolve4(confirmed2);
6484
+ resolve5(confirmed2);
6015
6485
  };
6016
6486
  const onGone = () => settle2(false);
6017
6487
  const timer = setTimeout(() => settle2(false), Math.min(FREEZE_MS, remaining()));
@@ -6024,13 +6494,13 @@ async function probeRuntimeTraversal(options) {
6024
6494
  }
6025
6495
  });
6026
6496
  };
6027
- await new Promise((resolve4) => {
6497
+ await new Promise((resolve5) => {
6028
6498
  let settleTimer = null;
6029
6499
  const done = () => {
6030
6500
  discovering = false;
6031
6501
  clearTimeout(runTimer);
6032
6502
  if (settleTimer) clearTimeout(settleTimer);
6033
- resolve4();
6503
+ resolve5();
6034
6504
  };
6035
6505
  const runTimer = setTimeout(done, remaining());
6036
6506
  const settle2 = () => {
@@ -6170,7 +6640,7 @@ async function runRuntimeCheck(cwd, options = {}) {
6170
6640
  settleMs: options.settleMs,
6171
6641
  platform: options.platform
6172
6642
  });
6173
- return { ...result, entry: { file: relative3(cwd, entry.file).replace(/\\/g, "/"), from: entry.from } };
6643
+ return { ...result, entry: { file: relative4(cwd, entry.file).replace(/\\/g, "/"), from: entry.from } };
6174
6644
  }
6175
6645
  function runtimeExitCode(report) {
6176
6646
  if (report.outcome === "proven") return 0;
@@ -6201,15 +6671,15 @@ function formatRuntimeCheck(report) {
6201
6671
  }
6202
6672
 
6203
6673
  // src/map-command.ts
6204
- import { writeFileSync as writeFileSync9 } from "fs";
6674
+ import path15 from "path";
6205
6675
 
6206
6676
  // src/map/ts-loader.ts
6207
6677
  import { createRequire } from "module";
6208
6678
  import { pathToFileURL } from "url";
6209
- import { join as join16 } from "path";
6679
+ import { join as join17 } from "path";
6210
6680
  async function loadTypeScript(cwd) {
6211
6681
  try {
6212
- const req = createRequire(pathToFileURL(join16(cwd, "package.json")));
6682
+ const req = createRequire(pathToFileURL(join17(cwd, "package.json")));
6213
6683
  const resolved = req.resolve("typescript");
6214
6684
  const mod = await import(pathToFileURL(resolved).href);
6215
6685
  return mod.default ?? mod;
@@ -6224,9 +6694,9 @@ async function loadTypeScript(cwd) {
6224
6694
  }
6225
6695
 
6226
6696
  // src/map/extract.ts
6227
- import { readFileSync as readFileSync15, realpathSync as realpathSync5 } from "fs";
6697
+ import { readFileSync as readFileSync16, realpathSync as realpathSync6 } from "fs";
6228
6698
  import { createHash as createHash5 } from "crypto";
6229
- import { relative as relative5 } from "path";
6699
+ import { relative as relative6 } from "path";
6230
6700
 
6231
6701
  // src/map/bindings.ts
6232
6702
  import { builtinModules } from "module";
@@ -6454,14 +6924,14 @@ function directSinks(node, ts, bindings, ctx) {
6454
6924
  const root = base ? rootIdentifier(base, ts) : void 0;
6455
6925
  if (!root) return {};
6456
6926
  const spec = bindings.resolve(root);
6457
- const relative7 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
6927
+ const relative8 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
6458
6928
  const pkg = npmPackageOf(spec);
6459
6929
  if (pkg) return { pkg, root, spec };
6460
- if (relative7 && ctx && spec) {
6930
+ if (relative8 && ctx && spec) {
6461
6931
  const viaModule = ctx.graph.importedBinding(ctx.file, spec, bindings.exportNameOf(root) ?? root);
6462
6932
  if (viaModule) return { pkg: viaModule.package, root, spec };
6463
6933
  }
6464
- return { local: bindings.locals.has(root), root, spec, relative: relative7 };
6934
+ return { local: bindings.locals.has(root), root, spec, relative: relative8 };
6465
6935
  };
6466
6936
  const attributionOf = (b, pkg) => b.pkg ? "import" : pkg ? "inferred" : void 0;
6467
6937
  const dbApi = (pkg, attribution) => {
@@ -6638,8 +7108,8 @@ function dedupeSinks(sinks, owner) {
6638
7108
  }
6639
7109
 
6640
7110
  // src/map/module-graph.ts
6641
- import { readFileSync as readFileSync13, realpathSync as realpathSync4, statSync as statSync6 } from "fs";
6642
- import { dirname as dirname7, join as join17, relative as relative4, resolve as resolvePath } from "path";
7111
+ import { readFileSync as readFileSync14, realpathSync as realpathSync5, statSync as statSync7 } from "fs";
7112
+ import { dirname as dirname8, join as join18, relative as relative5, resolve as resolvePath } from "path";
6643
7113
  var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
6644
7114
  function createModuleGraph(ts, opts) {
6645
7115
  const cache = /* @__PURE__ */ new Map();
@@ -6647,7 +7117,7 @@ function createModuleGraph(ts, opts) {
6647
7117
  if (cache.has(file)) return cache.get(file) ?? null;
6648
7118
  let entry = null;
6649
7119
  try {
6650
- const text = readFileSync13(file, "utf8");
7120
+ const text = readFileSync14(file, "utf8");
6651
7121
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
6652
7122
  const bindings = buildModuleBindings(sf, ts);
6653
7123
  entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
@@ -6663,7 +7133,7 @@ function createModuleGraph(ts, opts) {
6663
7133
  if (!opts.followOutside) {
6664
7134
  let real = target;
6665
7135
  try {
6666
- real = realpathSync4(target);
7136
+ real = realpathSync5(target);
6667
7137
  } catch {
6668
7138
  }
6669
7139
  if (!isInside(real, opts.boundary)) return void 0;
@@ -6681,7 +7151,7 @@ function createModuleGraph(ts, opts) {
6681
7151
  for (const callee of mod.calleesOf.get(exportName) ?? []) {
6682
7152
  for (const s of mod.fnSinks.get(callee) ?? []) collected.push(s);
6683
7153
  }
6684
- const rel = relative4(opts.cwd, target);
7154
+ const rel = relative5(opts.cwd, target);
6685
7155
  return collected.map((s) => ({ ...s, file: rel }));
6686
7156
  },
6687
7157
  // A different question about the same module: not "what sinks are in there" but "what does this
@@ -6707,7 +7177,7 @@ function createModuleGraph(ts, opts) {
6707
7177
  }
6708
7178
  function resolveRelativeModule(fromFile, spec) {
6709
7179
  if (!spec.startsWith(".")) return void 0;
6710
- const dir = dirname7(fromFile);
7180
+ const dir = dirname8(fromFile);
6711
7181
  const base = resolvePath(dir, spec);
6712
7182
  const candidates = [];
6713
7183
  const jsLike = /\.(js|jsx|mjs|cjs)$/.exec(base);
@@ -6717,10 +7187,10 @@ function resolveRelativeModule(fromFile, spec) {
6717
7187
  }
6718
7188
  candidates.push(base);
6719
7189
  for (const e of RESOLVE_EXTS) candidates.push(base + e);
6720
- for (const e of RESOLVE_EXTS) candidates.push(join17(base, "index" + e));
7190
+ for (const e of RESOLVE_EXTS) candidates.push(join18(base, "index" + e));
6721
7191
  for (const c of candidates) {
6722
7192
  try {
6723
- if (statSync6(c).isFile()) return c;
7193
+ if (statSync7(c).isFile()) return c;
6724
7194
  } catch {
6725
7195
  }
6726
7196
  }
@@ -6746,8 +7216,8 @@ function collectCallees(sf, ts) {
6746
7216
  }
6747
7217
 
6748
7218
  // src/map/coordinates.ts
6749
- function runtimeCoordinate(source, path17) {
6750
- if (/\[\d*\]/.test(path17)) {
7219
+ function runtimeCoordinate(source, path18) {
7220
+ if (/\[\d*\]/.test(path18)) {
6751
7221
  return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
6752
7222
  }
6753
7223
  switch (source) {
@@ -6756,15 +7226,15 @@ function runtimeCoordinate(source, path17) {
6756
7226
  case "multipart":
6757
7227
  case "body":
6758
7228
  case "server-fn-data":
6759
- return { runtimeParameter: `post.${path17}` };
7229
+ return { runtimeParameter: `post.${path18}` };
6760
7230
  case "query":
6761
- return { runtimeParameter: `get.${path17}` };
7231
+ return { runtimeParameter: `get.${path18}` };
6762
7232
  case "cookie":
6763
- return { runtimeParameter: `cookie.${path17}` };
7233
+ return { runtimeParameter: `cookie.${path18}` };
6764
7234
  case "file":
6765
- return { runtimeParameter: `files.${path17}` };
7235
+ return { runtimeParameter: `files.${path18}` };
6766
7236
  case "header":
6767
- return { runtimeParameter: `server.HTTP_${path17.toUpperCase().replace(/-/g, "_")}` };
7237
+ return { runtimeParameter: `server.HTTP_${path18.toUpperCase().replace(/-/g, "_")}` };
6768
7238
  case "route-param":
6769
7239
  return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
6770
7240
  default:
@@ -6796,8 +7266,8 @@ function addressSpaceOf(source) {
6796
7266
  function isProvenFlow(confidence) {
6797
7267
  return confidence === "exact-local" || confidence === "transformed-local";
6798
7268
  }
6799
- function inputIdOf(source, path17) {
6800
- return `${addressSpaceOf(source)}:${path17}`;
7269
+ function inputIdOf(source, path18) {
7270
+ return `${addressSpaceOf(source)}:${path18}`;
6801
7271
  }
6802
7272
  function withCoordinates(fields, source) {
6803
7273
  return fields.map((f) => {
@@ -6989,6 +7459,7 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
6989
7459
  }
6990
7460
 
6991
7461
  // src/map/flows.ts
7462
+ var MAX_DEPENDENCY_INPUT_FLOWS_PER_ENDPOINT = 100;
6992
7463
  function spaceOfKey(key) {
6993
7464
  if (key === "query") return "get";
6994
7465
  if (key === "params") return "route-param";
@@ -6999,16 +7470,23 @@ function spaceOfKey(key) {
6999
7470
  if (key === "files") return "files";
7000
7471
  return void 0;
7001
7472
  }
7002
- function linkedFlows(body, params, inputs, sinks, ts) {
7003
- const { flows, limitations } = linkFlows(body, params, inputs, sinks, ts);
7004
- return limitations.length > 0 ? { flows, limitations } : { flows };
7473
+ function linkedFlows(body, params, inputs, sinks, ts, invocations = []) {
7474
+ const { flows, limitations, dependencyInputFlows, dependencyInputFlowsTruncated } = linkFlows(body, params, inputs, sinks, ts, invocations);
7475
+ return {
7476
+ flows,
7477
+ ...limitations.length > 0 ? { limitations } : {},
7478
+ ...dependencyInputFlows.length > 0 ? { dependencyInputFlows } : {},
7479
+ ...dependencyInputFlowsTruncated ? { dependencyInputFlowsTruncated: true } : {}
7480
+ };
7005
7481
  }
7006
- function linkFlows(bodyNode, params, inputs, sinks, ts) {
7007
- if (!bodyNode || sinks.length === 0 || inputs.length === 0) return { flows: [], limitations: [] };
7482
+ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
7483
+ if (!bodyNode || inputs.length === 0 || sinks.length === 0 && invocations.length === 0) {
7484
+ return { flows: [], limitations: [], dependencyInputFlows: [], dependencyInputFlowsTruncated: false };
7485
+ }
7008
7486
  const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
7009
7487
  const rootPath = /* @__PURE__ */ new Map();
7010
- const addRoot = (name, path17, space) => {
7011
- if (!rootPath.has(name)) rootPath.set(name, { path: path17, space });
7488
+ const addRoot = (name, path18, space) => {
7489
+ if (!rootPath.has(name)) rootPath.set(name, { path: path18, space });
7012
7490
  };
7013
7491
  for (const p of params ?? []) {
7014
7492
  if (!p?.name) continue;
@@ -7139,7 +7617,57 @@ function linkFlows(bodyNode, params, inputs, sinks, ts) {
7139
7617
  }
7140
7618
  for (const l of sinkLimits) allLimits.push(l);
7141
7619
  }
7142
- return { flows, limitations: dedupeLimitations(allLimits) };
7620
+ const dependencyInputFlows = [];
7621
+ let dependencyInputFlowsTruncated = false;
7622
+ const seenDependencyInputs = /* @__PURE__ */ new Set();
7623
+ const modeledCalls = new Set(sinks.filter((s) => s.file === void 0 && s.start !== void 0 && s.end !== void 0).map((s) => `${s.start}:${s.end}`));
7624
+ for (const invocation of invocations) {
7625
+ if (invocation.package.startsWith("node:")) continue;
7626
+ for (const site of invocation.sites) {
7627
+ if (site.start === void 0 || site.end === void 0) continue;
7628
+ const span = `${site.start}:${site.end}`;
7629
+ if (modeledCalls.has(span)) continue;
7630
+ const call = callBySpan.get(span);
7631
+ if (!call) continue;
7632
+ const args = call.arguments ?? [];
7633
+ for (let argumentIndex = 0; argumentIndex < args.length; argumentIndex++) {
7634
+ const argument = args[argumentIndex];
7635
+ if (sinkArgumentLimitations(argument, ts, rootPath).length > 0) continue;
7636
+ const whole = pathFromTainted(argument, ts, rootPath);
7637
+ for (const read2 of taintedReadPaths(argument, ts, rootPath, false)) {
7638
+ if (read2.space === void 0) continue;
7639
+ for (const input of inputs) {
7640
+ if (addressSpaceOf(input.source) !== read2.space) continue;
7641
+ const inputPath = normalizePath(input.name);
7642
+ if (read2.path !== inputPath && !read2.path.startsWith(inputPath + ".")) continue;
7643
+ const key = `${span}:${argumentIndex}:${input.id}:${invocation.package}:${invocation.symbol}`;
7644
+ if (seenDependencyInputs.has(key)) continue;
7645
+ seenDependencyInputs.add(key);
7646
+ if (dependencyInputFlows.length >= MAX_DEPENDENCY_INPUT_FLOWS_PER_ENDPOINT) {
7647
+ dependencyInputFlowsTruncated = true;
7648
+ continue;
7649
+ }
7650
+ dependencyInputFlows.push({
7651
+ input: input.name,
7652
+ inputId: input.id,
7653
+ package: invocation.package,
7654
+ specifier: invocation.specifiers[0],
7655
+ api: invocation.api,
7656
+ symbol: invocation.symbol,
7657
+ kind: invocation.kind,
7658
+ resolution: invocation.resolution,
7659
+ argumentIndex,
7660
+ argumentUse: whole?.path === read2.path && whole.space === read2.space ? "direct" : "within-expression",
7661
+ line: site.line,
7662
+ start: site.start,
7663
+ end: site.end
7664
+ });
7665
+ }
7666
+ }
7667
+ }
7668
+ }
7669
+ }
7670
+ return { flows, limitations: dedupeLimitations(allLimits), dependencyInputFlows, dependencyInputFlowsTruncated };
7143
7671
  }
7144
7672
  function dedupeLimitations(list) {
7145
7673
  const seen = /* @__PURE__ */ new Set();
@@ -7153,8 +7681,8 @@ function dedupeLimitations(list) {
7153
7681
  function join22(base, seg) {
7154
7682
  return base ? `${base}.${seg}` : seg;
7155
7683
  }
7156
- function normalizePath(path17) {
7157
- return path17.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
7684
+ function normalizePath(path18) {
7685
+ return path18.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
7158
7686
  }
7159
7687
  function fluentChainCalls(call, ts) {
7160
7688
  let root = call;
@@ -7185,7 +7713,7 @@ function fluentChainCalls(call, ts) {
7185
7713
  collect(root);
7186
7714
  return out;
7187
7715
  }
7188
- function taintedReadPaths(node, ts, rootPath) {
7716
+ function taintedReadPaths(node, ts, rootPath, includeDeferredBodies = true) {
7189
7717
  const out = [];
7190
7718
  const seen = /* @__PURE__ */ new Set();
7191
7719
  const add = (r) => {
@@ -7197,6 +7725,7 @@ function taintedReadPaths(node, ts, rootPath) {
7197
7725
  };
7198
7726
  const visit = (n) => {
7199
7727
  if (!n) return;
7728
+ if (!includeDeferredBodies && (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n))) return;
7200
7729
  if (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n)) {
7201
7730
  const read2 = pathFromTainted(n, ts, rootPath);
7202
7731
  if (read2 !== void 0) {
@@ -7279,6 +7808,120 @@ function pathFromTainted(node, ts, rootPath) {
7279
7808
  return { path: normalizePath([base.path, ...segs].filter(Boolean).join(".")), space };
7280
7809
  }
7281
7810
 
7811
+ // src/map/invocations.ts
7812
+ var MAX_SITES = 3;
7813
+ function collectInvocations(sf, ts, bindings, ctx, root = sf) {
7814
+ const found = [];
7815
+ const counts = { total: 0, dependency: 0, local: 0, ambiguous: 0 };
7816
+ const isLocalName = (name, node) => bindings.locals.has(name) || isShadowedByEnclosingBinding(node, name, ts);
7817
+ const lineOf2 = (node) => {
7818
+ try {
7819
+ return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
7820
+ } catch {
7821
+ return void 0;
7822
+ }
7823
+ };
7824
+ const traceRoot = (root2) => {
7825
+ const specifier = bindings.resolve(root2);
7826
+ if (specifier === void 0) return null;
7827
+ const direct = npmPackageOf(specifier);
7828
+ if (direct !== void 0) {
7829
+ const resolution = bindings.originOf(root2) ?? "direct";
7830
+ return { pkg: direct, specifier, resolution, named: resolution === "direct" };
7831
+ }
7832
+ const exportName = bindings.exportNameOf(root2) ?? root2;
7833
+ const viaModule = ctx.graph.importedBinding(ctx.file, specifier, exportName);
7834
+ if (viaModule !== void 0) {
7835
+ return {
7836
+ pkg: viaModule.package,
7837
+ specifier,
7838
+ resolution: "reexport",
7839
+ named: viaModule.origin === "direct",
7840
+ apiName: viaModule.name
7841
+ };
7842
+ }
7843
+ return null;
7844
+ };
7845
+ const push = (traced, api, receiver, kind, node) => {
7846
+ const span = spanOf(node);
7847
+ found.push({
7848
+ package: traced.pkg,
7849
+ specifiers: [traced.specifier],
7850
+ api,
7851
+ ...receiver !== void 0 ? { receiver } : {},
7852
+ symbol: receiver !== void 0 ? `${receiver}.${api}` : api,
7853
+ kind,
7854
+ // Only ever `import`: a global has no package to correlate against, and an inferred package is not
7855
+ // evidence about the value being called.
7856
+ attribution: "import",
7857
+ resolution: traced.resolution,
7858
+ callCount: 1,
7859
+ sites: [{ file: ctx.owner, line: lineOf2(node), start: span.start, end: span.end }]
7860
+ });
7861
+ };
7862
+ const visit = (node) => {
7863
+ if (root !== sf && node !== root && isUninvokedFunctionDeclaration(node, ts)) return;
7864
+ if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
7865
+ const callee = node.expression;
7866
+ counts.total++;
7867
+ if (ts.isIdentifier(callee)) {
7868
+ const traced = traceRoot(callee.text);
7869
+ if (traced?.named) {
7870
+ const api = traced.apiName ?? bindings.exportNameOf(callee.text) ?? callee.text;
7871
+ push(traced, api, void 0, ts.isNewExpression(node) ? "construct" : "call", node);
7872
+ counts.dependency++;
7873
+ } else if (traced) {
7874
+ counts.ambiguous++;
7875
+ } else if (isLocalName(callee.text, node)) {
7876
+ counts.local++;
7877
+ } else {
7878
+ counts.ambiguous++;
7879
+ }
7880
+ } else if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
7881
+ const root2 = rootIdentifier(callee.expression, ts);
7882
+ const traced = root2 !== void 0 ? traceRoot(root2) : null;
7883
+ if (traced && root2 !== void 0) {
7884
+ counts.dependency++;
7885
+ const receiver = traced.resolution === "direct" && bindings.isPackageMember(root2) ? bindings.exportNameOf(root2) ?? root2 : void 0;
7886
+ push(traced, callee.name.text, receiver, "member", node);
7887
+ } else if (root2 !== void 0 && ts.isIdentifier(callee.expression) && isLocalName(root2, node)) {
7888
+ counts.local++;
7889
+ } else {
7890
+ counts.ambiguous++;
7891
+ }
7892
+ } else {
7893
+ counts.ambiguous++;
7894
+ }
7895
+ }
7896
+ ts.forEachChild(node, visit);
7897
+ };
7898
+ visit(root);
7899
+ return { invocations: found, counts };
7900
+ }
7901
+ function createInvocationInventory() {
7902
+ const byKey = /* @__PURE__ */ new Map();
7903
+ return {
7904
+ add(invocations) {
7905
+ for (const invocation of invocations) {
7906
+ const key = `${invocation.package}|${invocation.symbol}|${invocation.kind}|${invocation.resolution}`;
7907
+ const existing = byKey.get(key);
7908
+ if (existing === void 0) {
7909
+ byKey.set(key, { ...invocation });
7910
+ continue;
7911
+ }
7912
+ existing.callCount += invocation.callCount;
7913
+ for (const specifier of invocation.specifiers) {
7914
+ if (!existing.specifiers.includes(specifier)) existing.specifiers.push(specifier);
7915
+ }
7916
+ if (existing.sites.length < MAX_SITES) existing.sites.push(...invocation.sites.slice(0, MAX_SITES - existing.sites.length));
7917
+ }
7918
+ },
7919
+ list() {
7920
+ return [...byKey.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, invocation]) => ({ ...invocation, specifiers: [...invocation.specifiers].sort() }));
7921
+ }
7922
+ };
7923
+ }
7924
+
7282
7925
  // src/map/entries.ts
7283
7926
  var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
7284
7927
  function extractFromFile(sf, ts, localSinks, bindings, ctx) {
@@ -7302,7 +7945,7 @@ function extractFromFile(sf, ts, localSinks, bindings, ctx) {
7302
7945
  ...spanOf(decl),
7303
7946
  inputs,
7304
7947
  sinks,
7305
- ...linkedFlows(handlerBody, handlerFn?.parameters, inputs, sinks, ts)
7948
+ ...linkedFlows(handlerBody, handlerFn?.parameters, inputs, sinks, ts, handlerInvocations(handlerBody, ts, bindings, ctx))
7306
7949
  };
7307
7950
  if (validatorCall && inputs.length === 0) ep.inputsResolved = false;
7308
7951
  out.push(ep);
@@ -7377,6 +8020,10 @@ function hasUseServerDirective(fn, ts) {
7377
8020
  const first = fn.body?.statements?.[0];
7378
8021
  return Boolean(first && ts.isExpressionStatement(first) && ts.isStringLiteralLike(first.expression) && first.expression.text === "use server");
7379
8022
  }
8023
+ function handlerInvocations(body, ts, bindings, ctx) {
8024
+ const sf = body?.getSourceFile?.();
8025
+ return sf ? collectInvocations(sf, ts, bindings, ctx, body).invocations : [];
8026
+ }
7380
8027
  function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, ctx, extra = {}) {
7381
8028
  const entryKind = kindLabel === "route-registration" || kindLabel === "server-action" || kindLabel === "edge-function" ? kindLabel : "route-handler";
7382
8029
  const payloadStyle = kindLabel === "server-action";
@@ -7395,14 +8042,14 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
7395
8042
  end: extra.end,
7396
8043
  inputs,
7397
8044
  sinks,
7398
- ...linkedFlows(body, params, inputs, sinks, ts)
8045
+ ...linkedFlows(body, params, inputs, sinks, ts, handlerInvocations(body, ts, bindings, ctx))
7399
8046
  };
7400
8047
  }
7401
8048
 
7402
8049
  // src/map/imports.ts
7403
- import { readFileSync as readFileSync14 } from "fs";
7404
- import { join as join18 } from "path";
7405
- var MAX_SITES = 5;
8050
+ import { readFileSync as readFileSync15 } from "fs";
8051
+ import { join as join19 } from "path";
8052
+ var MAX_SITES2 = 5;
7406
8053
  var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
7407
8054
  function isNpmPackageName(pkg) {
7408
8055
  return pkg.startsWith("node:") || NPM_NAME.test(pkg);
@@ -7412,7 +8059,7 @@ function readPathAliases(cwd) {
7412
8059
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
7413
8060
  let paths;
7414
8061
  try {
7415
- const raw = readFileSync14(join18(cwd, name), "utf8");
8062
+ const raw = readFileSync15(join19(cwd, name), "utf8");
7416
8063
  const parsed = JSON.parse(stripJsonComments(raw));
7417
8064
  paths = parsed.compilerOptions?.paths;
7418
8065
  } catch {
@@ -7616,7 +8263,7 @@ function createImportInventory(aliases = []) {
7616
8263
  for (const n of edge.names) acc.names.add(n);
7617
8264
  if (!parsed) acc.namesComplete = false;
7618
8265
  acc.siteCount++;
7619
- if (acc.sites.length < MAX_SITES) acc.sites.push({ file: relFile, line: edge.line });
8266
+ if (acc.sites.length < MAX_SITES2) acc.sites.push({ file: relFile, line: edge.line });
7620
8267
  }
7621
8268
  },
7622
8269
  list() {
@@ -7652,127 +8299,15 @@ function lineAt(starts, pos) {
7652
8299
  return lo + 1;
7653
8300
  }
7654
8301
 
7655
- // src/map/invocations.ts
7656
- var MAX_SITES2 = 3;
7657
- function collectInvocations(sf, ts, bindings, ctx) {
7658
- const found = [];
7659
- const counts = { total: 0, dependency: 0, local: 0, ambiguous: 0 };
7660
- const isLocalName = (name, node) => bindings.locals.has(name) || isShadowedByEnclosingBinding(node, name, ts);
7661
- const lineOf2 = (node) => {
7662
- try {
7663
- return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
7664
- } catch {
7665
- return void 0;
7666
- }
7667
- };
7668
- const traceRoot = (root) => {
7669
- const specifier = bindings.resolve(root);
7670
- if (specifier === void 0) return null;
7671
- const direct = npmPackageOf(specifier);
7672
- if (direct !== void 0) {
7673
- const resolution = bindings.originOf(root) ?? "direct";
7674
- return { pkg: direct, specifier, resolution, named: resolution === "direct" };
7675
- }
7676
- const exportName = bindings.exportNameOf(root) ?? root;
7677
- const viaModule = ctx.graph.importedBinding(ctx.file, specifier, exportName);
7678
- if (viaModule !== void 0) {
7679
- return {
7680
- pkg: viaModule.package,
7681
- specifier,
7682
- resolution: "reexport",
7683
- named: viaModule.origin === "direct",
7684
- apiName: viaModule.name
7685
- };
7686
- }
7687
- return null;
7688
- };
7689
- const push = (traced, api, receiver, kind, node) => {
7690
- const span = spanOf(node);
7691
- found.push({
7692
- package: traced.pkg,
7693
- specifiers: [traced.specifier],
7694
- api,
7695
- ...receiver !== void 0 ? { receiver } : {},
7696
- symbol: receiver !== void 0 ? `${receiver}.${api}` : api,
7697
- kind,
7698
- // Only ever `import`: a global has no package to correlate against, and an inferred package is not
7699
- // evidence about the value being called.
7700
- attribution: "import",
7701
- resolution: traced.resolution,
7702
- callCount: 1,
7703
- sites: [{ file: ctx.owner, line: lineOf2(node), start: span.start, end: span.end }]
7704
- });
7705
- };
7706
- const visit = (node) => {
7707
- if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
7708
- const callee = node.expression;
7709
- counts.total++;
7710
- if (ts.isIdentifier(callee)) {
7711
- const traced = traceRoot(callee.text);
7712
- if (traced?.named) {
7713
- const api = traced.apiName ?? bindings.exportNameOf(callee.text) ?? callee.text;
7714
- push(traced, api, void 0, ts.isNewExpression(node) ? "construct" : "call", node);
7715
- counts.dependency++;
7716
- } else if (traced) {
7717
- counts.ambiguous++;
7718
- } else if (isLocalName(callee.text, node)) {
7719
- counts.local++;
7720
- } else {
7721
- counts.ambiguous++;
7722
- }
7723
- } else if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
7724
- const root = rootIdentifier(callee.expression, ts);
7725
- const traced = root !== void 0 ? traceRoot(root) : null;
7726
- if (traced && root !== void 0) {
7727
- counts.dependency++;
7728
- const receiver = traced.resolution === "direct" && bindings.isPackageMember(root) ? bindings.exportNameOf(root) ?? root : void 0;
7729
- push(traced, callee.name.text, receiver, "member", node);
7730
- } else if (root !== void 0 && ts.isIdentifier(callee.expression) && isLocalName(root, node)) {
7731
- counts.local++;
7732
- } else {
7733
- counts.ambiguous++;
7734
- }
7735
- } else {
7736
- counts.ambiguous++;
7737
- }
7738
- }
7739
- ts.forEachChild(node, visit);
7740
- };
7741
- visit(sf);
7742
- return { invocations: found, counts };
7743
- }
7744
- function createInvocationInventory() {
7745
- const byKey = /* @__PURE__ */ new Map();
7746
- return {
7747
- add(invocations) {
7748
- for (const invocation of invocations) {
7749
- const key = `${invocation.package}|${invocation.symbol}|${invocation.kind}|${invocation.resolution}`;
7750
- const existing = byKey.get(key);
7751
- if (existing === void 0) {
7752
- byKey.set(key, { ...invocation });
7753
- continue;
7754
- }
7755
- existing.callCount += invocation.callCount;
7756
- for (const specifier of invocation.specifiers) {
7757
- if (!existing.specifiers.includes(specifier)) existing.specifiers.push(specifier);
7758
- }
7759
- if (existing.sites.length < MAX_SITES2) existing.sites.push(...invocation.sites.slice(0, MAX_SITES2 - existing.sites.length));
7760
- }
7761
- },
7762
- list() {
7763
- return [...byKey.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, invocation]) => ({ ...invocation, specifiers: [...invocation.specifiers].sort() }));
7764
- }
7765
- };
7766
- }
7767
-
7768
8302
  // src/map/extract.ts
8303
+ var MAX_DEPENDENCY_INPUT_FLOWS_PER_MAP = 500;
7769
8304
  async function extractInputMap(cwd, ts, options = {}) {
7770
8305
  const notes = [];
7771
8306
  const endpoints = [];
7772
8307
  const failed = [];
7773
8308
  let boundary = cwd;
7774
8309
  try {
7775
- boundary = realpathSync5(cwd);
8310
+ boundary = realpathSync6(cwd);
7776
8311
  } catch {
7777
8312
  }
7778
8313
  const graph = createModuleGraph(ts, { cwd, boundary, followOutside: options.followSymlinks });
@@ -7780,6 +8315,7 @@ async function extractInputMap(cwd, ts, options = {}) {
7780
8315
  const files = collectSources(cwd, boundary, { followOutside: options.followSymlinks }, [], /* @__PURE__ */ new Set(), stats);
7781
8316
  const imports = createImportInventory(readPathAliases(cwd));
7782
8317
  const invocations = createInvocationInventory();
8318
+ let dependencyInputFlowCount = 0;
7783
8319
  let parsed = 0;
7784
8320
  let preFiltered = 0;
7785
8321
  let importScanFailures = 0;
@@ -7791,8 +8327,8 @@ async function extractInputMap(cwd, ts, options = {}) {
7791
8327
  const hopTargets = /* @__PURE__ */ new Set();
7792
8328
  for (const file of files) {
7793
8329
  try {
7794
- const text = readFileSync15(file, "utf8");
7795
- const relFile = relative5(cwd, file);
8330
+ const text = readFileSync16(file, "utf8");
8331
+ const relFile = relative6(cwd, file);
7796
8332
  sourceBytes += text.length;
7797
8333
  if (!hasEntrySignal(text)) {
7798
8334
  preFiltered++;
@@ -7823,6 +8359,13 @@ async function extractInputMap(cwd, ts, options = {}) {
7823
8359
  calls.ambiguous += called.counts.ambiguous;
7824
8360
  const localSinks = collectLocalSinks(sf, ts, bindings, ctx);
7825
8361
  for (const ep of extractFromFile(sf, ts, localSinks, bindings, ctx)) {
8362
+ const links = ep.dependencyInputFlows ?? [];
8363
+ const remaining = Math.max(0, MAX_DEPENDENCY_INPUT_FLOWS_PER_MAP - dependencyInputFlowCount);
8364
+ if (links.length > remaining) {
8365
+ ep.dependencyInputFlows = links.slice(0, remaining);
8366
+ ep.dependencyInputFlowsTruncated = true;
8367
+ }
8368
+ dependencyInputFlowCount += ep.dependencyInputFlows?.length ?? 0;
7826
8369
  if (ep.route === void 0 && ep.entryKind === "edge-function") {
7827
8370
  const fn = functionNameFromPath(relFile);
7828
8371
  if (fn) ep.route = "/" + fn;
@@ -7838,7 +8381,7 @@ async function extractInputMap(cwd, ts, options = {}) {
7838
8381
  }
7839
8382
  } catch (e) {
7840
8383
  if (typeof process !== "undefined" && process.env?.PS_MAP_DEBUG) console.error("[patchstack] map error", file, e);
7841
- failed.push(relative5(cwd, file));
8384
+ failed.push(relative6(cwd, file));
7842
8385
  }
7843
8386
  }
7844
8387
  let hopParsed = 0;
@@ -7846,10 +8389,10 @@ async function extractInputMap(cwd, ts, options = {}) {
7846
8389
  for (const target of hopTargets) {
7847
8390
  if (entryFiles.has(target)) continue;
7848
8391
  try {
7849
- const text = readFileSync15(target, "utf8");
8392
+ const text = readFileSync16(target, "utf8");
7850
8393
  const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
7851
8394
  const bindings = buildModuleBindings(sf, ts);
7852
- const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative5(cwd, target), graph });
8395
+ const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative6(cwd, target), graph });
7853
8396
  invocations.add(called.invocations);
7854
8397
  calls.total += called.counts.total;
7855
8398
  calls.dependency += called.counts.dependency;
@@ -7863,6 +8406,9 @@ async function extractInputMap(cwd, ts, options = {}) {
7863
8406
  }
7864
8407
  notes.push("Static analysis is best-effort \u2014 this is the DETECTED surface, not a proof of completeness.");
7865
8408
  notes.push('`inputs` and `sinks` are INVENTORIES (both present in the handler). Only `flows` asserts that an input reaches a sink: require confidence "exact-local" or "transformed-local" before pinning a rule, and identify the input by `inputId` \u2014 a field NAME can occur in more than one request namespace.');
8409
+ if (dependencyInputFlowCount > 0) {
8410
+ notes.push("`dependencyInputFlows` records request inputs observed in arguments of dependency API calls. This is weaker than a modeled sink: it does not establish what the dependency does internally or authorize a blocking rule. Absence is not evidence that no such call exists.");
8411
+ }
7866
8412
  notes.push("Sinks are followed into same-file helpers and ONE hop into an imported relative module (a dependency\u2019s internals are not followed); deeper or dynamic indirection is not traced. Sinks inside declared-but-uncalled local functions are excluded.");
7867
8413
  notes.push('A sink `package` is resolved from the file\u2019s imports (`attribution: "import"`) or inferred from another import in the same file (`"inferred"`); an inferred package is a hint for a reviewer, not evidence about the receiver, and never licenses a rule.');
7868
8414
  if (!options.followSymlinks) notes.push("Symlinks leaving the project directory were not followed (use --follow-symlinks to include them).");
@@ -7871,6 +8417,10 @@ async function extractInputMap(cwd, ts, options = {}) {
7871
8417
  notes.push(`${failed.length} file(s) could not be analyzed and were skipped (fail-open): ${sample}${failed.length > 5 ? ", \u2026" : ""}.`);
7872
8418
  }
7873
8419
  const unresolved = endpoints.filter((e) => e.inputsResolved === false).length;
8420
+ const truncatedDependencyFlows = endpoints.filter((e) => e.dependencyInputFlowsTruncated).length;
8421
+ if (truncatedDependencyFlows > 0) {
8422
+ notes.push(`${truncatedDependencyFlows} endpoint(s) had more dependency-input links than the bounded map carries; those endpoint records are marked dependencyInputFlowsTruncated.`);
8423
+ }
7874
8424
  if (unresolved > 0) {
7875
8425
  notes.push(`${unresolved} endpoint(s) declare an input validator that could not be statically parsed \u2014 their inputs are UNKNOWN, not empty (marked inputsResolved: false).`);
7876
8426
  }
@@ -7881,7 +8431,7 @@ async function extractInputMap(cwd, ts, options = {}) {
7881
8431
  if (endpoints.length === 0) notes.push("No recognized server-side entry points found under the analyzed roots.");
7882
8432
  const importList = imports.list();
7883
8433
  const unmodelled = importList.filter((d) => d.recognizedSinkKinds.length === 0).length;
7884
- const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative5(cwd, d));
8434
+ const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative6(cwd, d));
7885
8435
  const importCoverageGaps = {
7886
8436
  unreadableFiles: failed.length,
7887
8437
  unscannableFiles: importScanFailures,
@@ -7995,8 +8545,8 @@ function getStringFlag(flags, name) {
7995
8545
  }
7996
8546
 
7997
8547
  // src/build-stamp.ts
7998
- import { chmodSync, lstatSync as lstatSync3, readFileSync as readFileSync16, readdirSync as readdirSync6, realpathSync as realpathSync6, renameSync, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
7999
- import { join as join19, relative as relative6, sep as sep2 } from "path";
8548
+ import { lstatSync as lstatSync4, readFileSync as readFileSync17, readdirSync as readdirSync6, realpathSync as realpathSync7 } from "fs";
8549
+ import { join as join20, relative as relative7, sep as sep3 } from "path";
8000
8550
  var RULES_FILE_NAMES = /* @__PURE__ */ new Set(["rules.json", "patchstack.rules.json"]);
8001
8551
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "tmp", "vendor"]);
8002
8552
  var MAX_DEPTH2 = 6;
@@ -8014,11 +8564,11 @@ function guardImportsRulesFile(dir, rulesName) {
8014
8564
  }
8015
8565
  for (const name of siblings) {
8016
8566
  if (!GUARD_SOURCE.test(name)) continue;
8017
- const path17 = join19(dir, name);
8567
+ const path18 = join20(dir, name);
8018
8568
  try {
8019
- const entry = lstatSync3(path17);
8569
+ const entry = lstatSync4(path18);
8020
8570
  if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_GUARD_SOURCE_BYTES) continue;
8021
- const source = stripComments(readFileSync16(path17, "utf8"));
8571
+ const source = stripComments(readFileSync17(path18, "utf8"));
8022
8572
  if (source.includes("@patchstack/connect/protect") && importsRules.test(source)) return true;
8023
8573
  } catch {
8024
8574
  }
@@ -8035,14 +8585,14 @@ function findRulesFile(cwd) {
8035
8585
  const unreadable = [];
8036
8586
  let root;
8037
8587
  try {
8038
- root = realpathSync6(cwd);
8588
+ root = realpathSync7(cwd);
8039
8589
  } catch {
8040
8590
  return { kind: "none" };
8041
8591
  }
8042
- const contained = (path17) => {
8592
+ const contained = (path18) => {
8043
8593
  try {
8044
- const real = realpathSync6(path17);
8045
- return real === root || real.startsWith(root + sep2);
8594
+ const real = realpathSync7(path18);
8595
+ return real === root || real.startsWith(root + sep3);
8046
8596
  } catch {
8047
8597
  return false;
8048
8598
  }
@@ -8057,32 +8607,32 @@ function findRulesFile(cwd) {
8057
8607
  }
8058
8608
  for (const name of entries) {
8059
8609
  if (name.startsWith(".")) continue;
8060
- const path17 = join19(dir, name);
8610
+ const path18 = join20(dir, name);
8061
8611
  let entry;
8062
8612
  try {
8063
- entry = lstatSync3(path17);
8613
+ entry = lstatSync4(path18);
8064
8614
  } catch {
8065
8615
  continue;
8066
8616
  }
8067
8617
  if (entry.isSymbolicLink()) continue;
8068
8618
  if (entry.isDirectory()) {
8069
- if (!SKIP_DIRECTORIES.has(name)) walk2(path17, depth + 1);
8619
+ if (!SKIP_DIRECTORIES.has(name)) walk2(path18, depth + 1);
8070
8620
  continue;
8071
8621
  }
8072
8622
  if (!entry.isFile() || !RULES_FILE_NAMES.has(name)) continue;
8073
- if (!contained(path17)) continue;
8623
+ if (!contained(path18)) continue;
8074
8624
  if (!guardImportsRulesFile(dir, name)) continue;
8075
8625
  try {
8076
- if (isRuleBundle(JSON.parse(readFileSync16(path17, "utf8")))) found.push(path17);
8626
+ if (isRuleBundle(JSON.parse(readFileSync17(path18, "utf8")))) found.push(path18);
8077
8627
  } catch {
8078
- unreadable.push(path17);
8628
+ unreadable.push(path18);
8079
8629
  }
8080
8630
  }
8081
8631
  };
8082
8632
  walk2(cwd, 0);
8083
- if (found.length > 1) return { kind: "ambiguous", paths: found.map((p) => relative6(cwd, p)) };
8633
+ if (found.length > 1) return { kind: "ambiguous", paths: found.map((p) => relative7(cwd, p)) };
8084
8634
  if (found.length === 1) return { kind: "one", path: found[0] };
8085
- if (unreadable.length > 0) return { kind: "unreadable", paths: unreadable.map((p) => relative6(cwd, p)) };
8635
+ if (unreadable.length > 0) return { kind: "unreadable", paths: unreadable.map((p) => relative7(cwd, p)) };
8086
8636
  return { kind: "none" };
8087
8637
  }
8088
8638
  function detectIndent(text) {
@@ -8103,10 +8653,10 @@ function applyBuildStamp(cwd, id) {
8103
8653
  reason: `more than one candidate guard rules file (${location.paths.join(", ")}) \u2014 none was stamped`
8104
8654
  };
8105
8655
  }
8106
- const file = relative6(cwd, location.path);
8656
+ const file = relative7(cwd, location.path);
8107
8657
  let text;
8108
8658
  try {
8109
- text = readFileSync16(location.path, "utf8");
8659
+ text = readFileSync17(location.path, "utf8");
8110
8660
  } catch (err) {
8111
8661
  return { kind: "skipped", reason: `${file} could not be read (${err.message})` };
8112
8662
  }
@@ -8126,17 +8676,9 @@ function applyBuildStamp(cwd, id) {
8126
8676
  if (Object.keys(namespace).length === 0) delete bundle[BUILD_STAMP_KEY];
8127
8677
  else bundle[BUILD_STAMP_KEY] = namespace;
8128
8678
  const serialised = JSON.stringify(bundle, null, detectIndent(text)) + (text.endsWith("\n") ? "\n" : "");
8129
- const temporary = `${location.path}.patchstack-${process.pid}.tmp`;
8130
8679
  try {
8131
- const mode = lstatSync3(location.path).mode & 4095;
8132
- writeFileSync8(temporary, serialised, { encoding: "utf8", mode });
8133
- chmodSync(temporary, mode);
8134
- renameSync(temporary, location.path);
8680
+ writeProjectFileSync(cwd, location.path, serialised, { encoding: "utf8" });
8135
8681
  } catch (err) {
8136
- try {
8137
- unlinkSync(temporary);
8138
- } catch {
8139
- }
8140
8682
  return { kind: "skipped", reason: `${file} could not be written (${err.message})` };
8141
8683
  }
8142
8684
  return id === null ? { kind: "cleared", file } : { kind: "stamped", file, id };
@@ -8275,7 +8817,7 @@ async function runMap(flags) {
8275
8817
  const json = JSON.stringify(map, null, 2);
8276
8818
  const out = getStringFlag(flags, "out");
8277
8819
  if (out) {
8278
- writeFileSync9(out, json);
8820
+ atomicWriteFileSync(path15.resolve(out), json, { encoding: "utf8" });
8279
8821
  console.error(`patchstack: wrote ${out}`);
8280
8822
  } else if (flags.get("upload") !== true) {
8281
8823
  console.log(json);
@@ -8324,8 +8866,8 @@ async function runMap(flags) {
8324
8866
  }
8325
8867
 
8326
8868
  // src/setup.ts
8327
- import { readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
8328
- import path15 from "path";
8869
+ import { readFileSync as readFileSync18 } from "fs";
8870
+ import path16 from "path";
8329
8871
  var SCAN_COMMAND = "patchstack-connect scan";
8330
8872
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
8331
8873
  function setupProtection(cwd) {
@@ -8350,8 +8892,8 @@ function prependHook(existing, command) {
8350
8892
  return [command, ...remaining].join(" && ");
8351
8893
  }
8352
8894
  function wireBuildScripts(cwd, packageManager) {
8353
- const target = path15.join(cwd, "package.json");
8354
- const raw = readFileSync17(target, "utf8");
8895
+ const target = path16.join(cwd, "package.json");
8896
+ const raw = readFileSync18(target, "utf8");
8355
8897
  const pkg = JSON.parse(raw);
8356
8898
  const scripts = pkg.scripts ?? {};
8357
8899
  const build = scripts.build;
@@ -8397,7 +8939,7 @@ function wireBuildScripts(cwd, packageManager) {
8397
8939
  const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
8398
8940
  const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
8399
8941
  const trailingNewline = raw.endsWith("\n") ? "\n" : "";
8400
- writeFileSync10(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
8942
+ writeProjectFileSync(cwd, target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, { encoding: "utf8" });
8401
8943
  if (build === void 0 || build.trim().length === 0) {
8402
8944
  return {
8403
8945
  changed: true,
@@ -8927,8 +9469,9 @@ async function runScan(args, options = {}) {
8927
9469
  const cleared = applyBuildStamp(process.cwd(), null);
8928
9470
  if (cleared.kind === "cleared") console.log(`Removed the previous map binding from ${cleared.file}.`);
8929
9471
  }
9472
+ const shellFramework = detectStack(payload.packages).framework;
8930
9473
  if (config.widget) {
8931
- reportSourceMarker(detectStack(payload.packages).framework);
9474
+ reportSourceMarker(shellFramework, computeManifestChecksum(payload.packages));
8932
9475
  }
8933
9476
  const provisioning = config.siteUuid === null;
8934
9477
  if (provisioning) {
@@ -8974,7 +9517,7 @@ async function runScan(args, options = {}) {
8974
9517
  const connected = response.claim?.state === "claimed" || response.claim?.state === "owned-by-you";
8975
9518
  const effectiveUuid = config.siteUuid ?? response.uuid ?? null;
8976
9519
  if (config.widget && effectiveUuid !== null && effectiveUuid.length > 0) {
8977
- reportSourceWidget(effectiveUuid);
9520
+ reportSourceWidget(effectiveUuid, shellFramework);
8978
9521
  }
8979
9522
  const linkUuid = response.uuid ?? config.siteUuid;
8980
9523
  if (!connected && (provisioning || claimLines.length > 0) && linkUuid !== null && linkUuid !== void 0 && linkUuid.length > 0) {
@@ -9005,9 +9548,11 @@ async function runScan(args, options = {}) {
9005
9548
  }
9006
9549
  return 0;
9007
9550
  }
9008
- function reportSourceWidget(siteUuid) {
9551
+ function reportSourceWidget(siteUuid, framework) {
9009
9552
  try {
9010
- const result = ensureSourceWidget(process.cwd(), siteUuid);
9553
+ const hint = hasJsxShell(framework) ? resolveWidgetFileHint(process.cwd(), framework) : null;
9554
+ const jsxShell = hint !== null && !hint.toLowerCase().endsWith(".html") ? hint : null;
9555
+ const result = ensureSourceWidget(process.cwd(), siteUuid, jsxShell);
9011
9556
  switch (result.action) {
9012
9557
  case "added":
9013
9558
  console.log(`Widget: added the "Report a vulnerability" tag to ${result.shell}. Reload your preview to see it.`);
@@ -9026,7 +9571,7 @@ function reportSourceWidget(siteUuid) {
9026
9571
  console.log(` ${buildWidgetTag(siteUuid)}`);
9027
9572
  break;
9028
9573
  case "no-shell":
9029
- console.log("Widget: no plain HTML shell found (index.html / public/index.html / src/app.html).");
9574
+ console.log("Widget: no root shell found to edit (index.html / public/index.html / src/app.html, or a JSX root).");
9030
9575
  console.log("Add this tag to your root layout before </body> (run `guide` for framework-specific placement):");
9031
9576
  console.log(` ${buildWidgetTag(siteUuid)}`);
9032
9577
  break;
@@ -9038,13 +9583,13 @@ function reportSourceWidget(siteUuid) {
9038
9583
  console.warn(`Widget: skipped (${err.message}).`);
9039
9584
  }
9040
9585
  }
9041
- function reportSourceMarker(framework) {
9586
+ function reportSourceMarker(framework, checksum = null) {
9042
9587
  try {
9043
9588
  const shell = resolveWidgetFileHint(process.cwd(), framework);
9044
9589
  if (shell === null || shell.toLowerCase().endsWith(".html")) {
9045
9590
  return;
9046
9591
  }
9047
- const result = ensureSourceMarker(process.cwd(), shell, framework);
9592
+ const result = ensureSourceMarker(process.cwd(), shell, framework, checksum);
9048
9593
  switch (result.action) {
9049
9594
  case "added":
9050
9595
  console.log(`Production marker: added to ${shell} (guarded by ${productionGate(framework)}).`);
@@ -9231,7 +9776,7 @@ async function runGuide(args) {
9231
9776
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
9232
9777
  console.log("");
9233
9778
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
9234
- console.log(readFileSync18(guidePath, "utf8"));
9779
+ console.log(readFileSync19(guidePath, "utf8"));
9235
9780
  return 0;
9236
9781
  }
9237
9782
  async function runSetup(args) {
@@ -9406,7 +9951,7 @@ function describeStack(stack) {
9406
9951
  }
9407
9952
  function readBuildScript(cwd) {
9408
9953
  try {
9409
- const pkg = JSON.parse(readFileSync18(path16.join(cwd, "package.json"), "utf8"));
9954
+ const pkg = JSON.parse(readFileSync19(path17.join(cwd, "package.json"), "utf8"));
9410
9955
  return pkg.scripts?.build;
9411
9956
  } catch {
9412
9957
  return void 0;
@@ -9486,7 +10031,7 @@ async function runMarkBuild(args) {
9486
10031
  let marked = 0;
9487
10032
  let widgetTouched = 0;
9488
10033
  for (const file of files) {
9489
- const before = readFileSync18(file, "utf8");
10034
+ const before = readFileSync19(file, "utf8");
9490
10035
  let after = injectMarker(before, snippet);
9491
10036
  if (widgetUuid !== null) {
9492
10037
  const ensured = ensureWidgetInHtml(after, widgetUuid);
@@ -9496,7 +10041,7 @@ async function runMarkBuild(args) {
9496
10041
  after = ensured.html;
9497
10042
  }
9498
10043
  if (after !== before) {
9499
- writeFileSync11(file, after);
10044
+ writeProjectFileSync(cwd, file, after, { encoding: "utf8" });
9500
10045
  marked += 1;
9501
10046
  }
9502
10047
  }