@patchstack/connect 0.5.6 → 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;
1270
1531
  }
1271
- if (inflight !== null) return inflight;
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;
1537
+ }
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
  }
@@ -2613,25 +2972,25 @@ function ensureSourceMarker(cwd, shell, framework, checksum = null) {
2613
2972
  return { shell: null, source: "", action: "no-anchor" };
2614
2973
  }
2615
2974
  const file = path9.resolve(cwd, shell);
2616
- const before = readFileSync2(file, "utf8");
2975
+ const before = readFileSync3(file, "utf8");
2617
2976
  const result = ensureMarkerInJsxShell(before, framework, checksum);
2618
2977
  if (result.source !== before) {
2619
- writeFileSync(file, result.source);
2978
+ writeProjectFileSync(cwd, file, result.source, { encoding: "utf8" });
2620
2979
  }
2621
2980
  return { ...result, shell };
2622
2981
  }
2623
2982
 
2624
2983
  // src/guide.ts
2625
- 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";
2626
2985
  import path12 from "path";
2627
2986
 
2628
2987
  // src/architecture.ts
2629
- 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";
2630
2989
  import path10 from "path";
2631
2990
 
2632
2991
  // src/map/sources.ts
2633
- import { readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync2, statSync as statSync2 } from "fs";
2634
- 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";
2635
2994
 
2636
2995
  // src/map/ast.ts
2637
2996
  function rootIdentifier(node, ts) {
@@ -2840,7 +3199,7 @@ function hasEntrySignal(text) {
2840
3199
  }
2841
3200
  function detectFramework(cwd) {
2842
3201
  try {
2843
- const pkg = JSON.parse(readFileSync3(join2(cwd, "package.json"), "utf8"));
3202
+ const pkg = JSON.parse(readFileSync4(join3(cwd, "package.json"), "utf8"));
2844
3203
  const d = { ...pkg.dependencies, ...pkg.devDependencies };
2845
3204
  if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
2846
3205
  if (d["next"]) return "next";
@@ -2852,11 +3211,11 @@ function detectFramework(cwd) {
2852
3211
  } catch {
2853
3212
  }
2854
3213
  try {
2855
- if (statSync2(join2(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
3214
+ if (statSync3(join3(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
2856
3215
  } catch {
2857
3216
  }
2858
3217
  try {
2859
- if (statSync2(join2(cwd, "functions")).isDirectory()) return "deno-functions";
3218
+ if (statSync3(join3(cwd, "functions")).isDirectory()) return "deno-functions";
2860
3219
  } catch {
2861
3220
  }
2862
3221
  return "unknown";
@@ -2898,7 +3257,7 @@ function probeForSource(dir, budget) {
2898
3257
  for (const e of entries) {
2899
3258
  if (budget.left-- <= 0) return "unknown";
2900
3259
  if (e.isDirectory()) {
2901
- 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));
2902
3261
  } else if (isSourceFile(e.name)) return true;
2903
3262
  else if (e.isSymbolicLink()) return "unknown";
2904
3263
  }
@@ -2911,7 +3270,7 @@ function probeForSource(dir, budget) {
2911
3270
  function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ new Set(), stats = { discovered: 0, unwalked: 0, skippedWithSource: [], skippedUnknown: 0 }, probeBudget = { left: PROBE_BUDGET }) {
2912
3271
  let key;
2913
3272
  try {
2914
- key = realpathSync2(dir);
3273
+ key = realpathSync3(dir);
2915
3274
  } catch {
2916
3275
  stats.unwalked++;
2917
3276
  return out;
@@ -2935,19 +3294,19 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
2935
3294
  const probeable = !SKIP_IS_DEFINITIONAL.has(e.name);
2936
3295
  if (probeable && !e.isDirectory() && e.isSymbolicLink()) stats.skippedUnknown++;
2937
3296
  else if (probeable && e.isDirectory()) {
2938
- const held = probeForSource(join2(dir, e.name), probeBudget);
2939
- 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));
2940
3299
  else if (held === "unknown") stats.skippedUnknown++;
2941
3300
  }
2942
3301
  continue;
2943
3302
  }
2944
- const full = join2(dir, e.name);
3303
+ const full = join3(dir, e.name);
2945
3304
  if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
2946
3305
  else if (e.isSymbolicLink()) {
2947
3306
  let st, real;
2948
3307
  try {
2949
- st = statSync2(full);
2950
- real = realpathSync2(full);
3308
+ st = statSync3(full);
3309
+ real = realpathSync3(full);
2951
3310
  } catch {
2952
3311
  stats.unwalked++;
2953
3312
  continue;
@@ -2970,8 +3329,8 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
2970
3329
  }
2971
3330
  function isInside(candidate, boundary) {
2972
3331
  if (candidate === boundary) return true;
2973
- const rel = relative(boundary, candidate);
2974
- return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
3332
+ const rel = relative2(boundary, candidate);
3333
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
2975
3334
  }
2976
3335
  var DEPLOYMENT_SHAPES = [
2977
3336
  // Config first: these are declarations by the project itself, and they survive a build output being
@@ -2997,13 +3356,13 @@ var DEPLOYMENT_SHAPES = [
2997
3356
  function detectDeploymentShapes(cwd, opts = {}) {
2998
3357
  let boundary = opts.boundary ?? cwd;
2999
3358
  try {
3000
- boundary = realpathSync2(boundary);
3359
+ boundary = realpathSync3(boundary);
3001
3360
  } catch {
3002
3361
  }
3003
- const inProject = (path17) => {
3362
+ const inProject = (path18) => {
3004
3363
  if (opts.followOutside) return true;
3005
3364
  try {
3006
- return isInside(realpathSync2(path17), boundary);
3365
+ return isInside(realpathSync3(path18), boundary);
3007
3366
  } catch {
3008
3367
  return false;
3009
3368
  }
@@ -3011,9 +3370,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
3011
3370
  const found = [];
3012
3371
  for (const candidate of DEPLOYMENT_SHAPES) {
3013
3372
  for (const file of candidate.files ?? []) {
3014
- const full = join2(cwd, file);
3373
+ const full = join3(cwd, file);
3015
3374
  try {
3016
- if (statSync2(full).isFile() && inProject(full)) {
3375
+ if (statSync3(full).isFile() && inProject(full)) {
3017
3376
  found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
3018
3377
  break;
3019
3378
  }
@@ -3021,9 +3380,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
3021
3380
  }
3022
3381
  }
3023
3382
  for (const dir of candidate.dirs ?? []) {
3024
- const full = join2(cwd, dir);
3383
+ const full = join3(cwd, dir);
3025
3384
  try {
3026
- if (!statSync2(full).isDirectory() || !inProject(full)) continue;
3385
+ if (!statSync3(full).isDirectory() || !inProject(full)) continue;
3027
3386
  } catch {
3028
3387
  continue;
3029
3388
  }
@@ -3040,7 +3399,7 @@ function holdsFunctionEntry(dir) {
3040
3399
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
3041
3400
  if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
3042
3401
  try {
3043
- for (const nested of readdirSync2(join2(dir, entry.name), { withFileTypes: true })) {
3402
+ for (const nested of readdirSync2(join3(dir, entry.name), { withFileTypes: true })) {
3044
3403
  if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
3045
3404
  }
3046
3405
  } catch {
@@ -3056,7 +3415,7 @@ function holdsSourceFile(dir) {
3056
3415
  if (entry.isFile() && isSourceFile(entry.name)) return true;
3057
3416
  if (entry.isDirectory()) {
3058
3417
  try {
3059
- for (const nested of readdirSync2(join2(dir, entry.name), { withFileTypes: true })) {
3418
+ for (const nested of readdirSync2(join3(dir, entry.name), { withFileTypes: true })) {
3060
3419
  if (nested.isFile() && isSourceFile(nested.name)) return true;
3061
3420
  }
3062
3421
  } catch {
@@ -3069,8 +3428,8 @@ function holdsSourceFile(dir) {
3069
3428
  }
3070
3429
 
3071
3430
  // src/map/surface.ts
3072
- import { readFileSync as readFileSync4 } from "fs";
3073
- import { join as join3 } from "path";
3431
+ import { readFileSync as readFileSync5 } from "fs";
3432
+ import { join as join4 } from "path";
3074
3433
  var STATIC_GENERATORS = [
3075
3434
  { dep: "vite", label: "vite" },
3076
3435
  { dep: "react-scripts", label: "create-react-app" },
@@ -3111,7 +3470,7 @@ var SHAPE_SIGNAL = {
3111
3470
  };
3112
3471
  function readManifest(cwd) {
3113
3472
  try {
3114
- const parsed = JSON.parse(readFileSync4(join3(cwd, "package.json"), "utf8"));
3473
+ const parsed = JSON.parse(readFileSync5(join4(cwd, "package.json"), "utf8"));
3115
3474
  return typeof parsed === "object" && parsed !== null ? parsed : null;
3116
3475
  } catch {
3117
3476
  return null;
@@ -3124,7 +3483,7 @@ function nextExportsStatically(cwd, manifest, ts) {
3124
3483
  for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
3125
3484
  let text;
3126
3485
  try {
3127
- text = readFileSync4(join3(cwd, file), "utf8");
3486
+ text = readFileSync5(join4(cwd, file), "utf8");
3128
3487
  } catch {
3129
3488
  continue;
3130
3489
  }
@@ -3283,7 +3642,7 @@ var SERVING_CALL = /\b(?:createServer|createSecureServer|Bun\.serve|Deno\.serve|
3283
3642
  var MAX_ENTRY_BYTES = 256 * 1024;
3284
3643
  function readManifest2(cwd) {
3285
3644
  try {
3286
- const parsed = JSON.parse(readFileSync5(path10.join(cwd, "package.json"), "utf8"));
3645
+ const parsed = JSON.parse(readFileSync6(path10.join(cwd, "package.json"), "utf8"));
3287
3646
  return typeof parsed === "object" && parsed !== null ? parsed : {};
3288
3647
  } catch {
3289
3648
  return {};
@@ -3317,12 +3676,12 @@ function serverEntries(cwd, manifest) {
3317
3676
  if (!main2.startsWith("..")) candidates.add(main2);
3318
3677
  }
3319
3678
  const found = [];
3320
- for (const relative7 of candidates) {
3321
- const file = path10.join(cwd, relative7);
3679
+ for (const relative8 of candidates) {
3680
+ const file = path10.join(cwd, relative8);
3322
3681
  try {
3323
- if (!existsSync2(file) || !statSync3(file).isFile()) continue;
3324
- const text = readFileSync5(file, "utf8").slice(0, MAX_ENTRY_BYTES);
3325
- 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);
3326
3685
  } catch {
3327
3686
  }
3328
3687
  }
@@ -3375,14 +3734,14 @@ function classifyArchitecture(cwd) {
3375
3734
  }
3376
3735
 
3377
3736
  // src/protect/install/util.ts
3378
- import { readFileSync as readFileSync6, existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
3379
- 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";
3380
3739
  import { fileURLToPath } from "url";
3381
- var read = (p) => readFileSync6(p, "utf8");
3740
+ var read = (p) => readFileSync7(p, "utf8");
3382
3741
  var log = (msg) => console.log(`patchstack protect: ${msg}`);
3383
3742
  function hasDependency(cwd, name) {
3384
3743
  try {
3385
- const pkg = JSON.parse(read(join4(cwd, "package.json")));
3744
+ const pkg = JSON.parse(read(join5(cwd, "package.json")));
3386
3745
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }[name]);
3387
3746
  } catch {
3388
3747
  return false;
@@ -3394,7 +3753,7 @@ function hasResolvableCredential(cwd, env = process.env) {
3394
3753
  }
3395
3754
  for (const file of [".patchstackrc.local.json", ".patchstackrc.json"]) {
3396
3755
  try {
3397
- const parsed = JSON.parse(read(join4(cwd, file)));
3756
+ const parsed = JSON.parse(read(join5(cwd, file)));
3398
3757
  if (typeof parsed.apiKey === "string" && parsed.apiKey !== "") return true;
3399
3758
  if (typeof parsed.pulseAuth === "string" && parsed.pulseAuth !== "") return true;
3400
3759
  } catch {
@@ -3408,7 +3767,7 @@ function isSiteUuid(value) {
3408
3767
  return typeof value === "string" && UUID_RE.test(value);
3409
3768
  }
3410
3769
  function bakeSiteUuid(cwd, guardRelPath) {
3411
- const rc = join4(cwd, ".patchstackrc.json");
3770
+ const rc = join5(cwd, ".patchstackrc.json");
3412
3771
  if (!existsSync3(rc)) {
3413
3772
  log("no .patchstackrc.json \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
3414
3773
  return false;
@@ -3424,32 +3783,32 @@ function bakeSiteUuid(cwd, guardRelPath) {
3424
3783
  log(".patchstackrc.json siteUuid missing or malformed \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
3425
3784
  return false;
3426
3785
  }
3427
- const guardPath = join4(cwd, guardRelPath);
3786
+ const guardPath = join5(cwd, guardRelPath);
3428
3787
  if (!existsSync3(guardPath)) return false;
3429
3788
  const source = read(guardPath);
3430
3789
  if (!source.includes(SITE_UUID_PLACEHOLDER)) {
3431
3790
  log(`${guardRelPath} site UUID already baked`);
3432
3791
  return false;
3433
3792
  }
3434
- writeFileSync2(guardPath, source.replace(SITE_UUID_PLACEHOLDER, uuid));
3793
+ writeProjectFileSync(cwd, guardPath, source.replace(SITE_UUID_PLACEHOLDER, uuid), { encoding: "utf8" });
3435
3794
  log(`baked site UUID into ${guardRelPath} \u2014 live rules from the Patchstack API`);
3436
3795
  return true;
3437
3796
  }
3438
- var HERE = dirname(fileURLToPath(import.meta.url));
3797
+ var HERE = dirname2(fileURLToPath(import.meta.url));
3439
3798
  function templatesDir() {
3440
- const builtLayout = join4(HERE, "protect", "templates");
3799
+ const builtLayout = join5(HERE, "protect", "templates");
3441
3800
  const candidates = [
3442
3801
  builtLayout,
3443
- join4(HERE, "..", "templates"),
3802
+ join5(HERE, "..", "templates"),
3444
3803
  // source: src/protect/install/ → src/protect/templates
3445
- join4(HERE, "templates")
3804
+ join5(HERE, "templates")
3446
3805
  ];
3447
3806
  return candidates.find((p) => existsSync3(p)) ?? builtLayout;
3448
3807
  }
3449
3808
 
3450
3809
  // src/protect/install/adapters/tanstack-supabase.ts
3451
- import { writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync, copyFileSync } from "fs";
3452
- import { join as join5 } from "path";
3810
+ import { existsSync as existsSync4 } from "fs";
3811
+ import { join as join6 } from "path";
3453
3812
  var CLIENT_TUNNEL = [
3454
3813
  "",
3455
3814
  " // PATCHSTACK: in the browser, tunnel Supabase traffic through the app's own server guard",
@@ -3519,7 +3878,7 @@ function reconcileBlock(s, region, block, legacyConst, insertBefore) {
3519
3878
  return s.replace(insertBefore, block + "\n\n" + insertBefore);
3520
3879
  }
3521
3880
  function detect(cwd) {
3522
- const pkgPath = join5(cwd, "package.json");
3881
+ const pkgPath = join6(cwd, "package.json");
3523
3882
  if (!existsSync4(pkgPath)) return false;
3524
3883
  let pkg;
3525
3884
  try {
@@ -3528,22 +3887,22 @@ function detect(cwd) {
3528
3887
  return false;
3529
3888
  }
3530
3889
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
3531
- 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"));
3532
3891
  }
3533
3892
  var GUARD_FILE = "src/integrations/patchstack/guard.ts";
3534
3893
  function scaffold(cwd, opts) {
3535
3894
  const templates = templatesDir();
3536
- const dst = join5(cwd, "src/integrations/patchstack");
3537
- mkdirSync(dst, { recursive: true });
3538
- 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"));
3539
3898
  const changed = [GUARD_FILE];
3540
- const rulesDst = join5(dst, "rules.json");
3899
+ const rulesDst = join6(dst, "rules.json");
3541
3900
  if (opts.demo) {
3542
- copyFileSync(join5(templates, "demo-rules.json"), rulesDst);
3901
+ copyProjectFileSync(cwd, join6(templates, "demo-rules.json"), rulesDst);
3543
3902
  changed.push("src/integrations/patchstack/rules.json");
3544
3903
  log("scaffolded guard.ts + rules.json (demo sample rule set)");
3545
3904
  } else if (!existsSync4(rulesDst)) {
3546
- copyFileSync(join5(templates, "rules.json"), rulesDst);
3905
+ copyProjectFileSync(cwd, join6(templates, "rules.json"), rulesDst);
3547
3906
  changed.push("src/integrations/patchstack/rules.json");
3548
3907
  log("scaffolded guard.ts + rules.json (starter rules)");
3549
3908
  } else {
@@ -3552,7 +3911,7 @@ function scaffold(cwd, opts) {
3552
3911
  return changed;
3553
3912
  }
3554
3913
  function patchClient(cwd) {
3555
- const p = join5(cwd, "src/integrations/supabase/client.ts");
3914
+ const p = join6(cwd, "src/integrations/supabase/client.ts");
3556
3915
  const s = read(p);
3557
3916
  if (s.includes("x-ps-target")) {
3558
3917
  log("client.ts already wired");
@@ -3563,12 +3922,12 @@ function patchClient(cwd) {
3563
3922
  log("client.ts anchor not found \u2014 skipping (template changed?)");
3564
3923
  return false;
3565
3924
  }
3566
- writeFileSync3(p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL));
3925
+ writeProjectFileSync(cwd, p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL), { encoding: "utf8" });
3567
3926
  log("patched client.ts (tunnel Supabase through the guard)");
3568
3927
  return true;
3569
3928
  }
3570
3929
  function patchStart(cwd) {
3571
- const p = join5(cwd, "src/start.ts");
3930
+ const p = join6(cwd, "src/start.ts");
3572
3931
  let s = read(p);
3573
3932
  const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
3574
3933
  const exportAnchor = "export const startInstance";
@@ -3600,7 +3959,7 @@ function patchStart(cwd) {
3600
3959
  log("start.ts already wired");
3601
3960
  return false;
3602
3961
  }
3603
- writeFileSync3(p, s);
3962
+ writeProjectFileSync(cwd, p, s, { encoding: "utf8" });
3604
3963
  log("patched start.ts (guard registered as request + function middleware)");
3605
3964
  return true;
3606
3965
  }
@@ -3615,9 +3974,9 @@ function wire(cwd, opts) {
3615
3974
  return { ok: true, changed: [...new Set(changed)] };
3616
3975
  }
3617
3976
  function verify(cwd) {
3618
- const guardPath = join5(cwd, GUARD_FILE);
3619
- const clientPath = join5(cwd, "src/integrations/supabase/client.ts");
3620
- 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");
3621
3980
  const guard = existsSync4(guardPath) ? read(guardPath) : "";
3622
3981
  const client = existsSync4(clientPath) ? read(clientPath) : "";
3623
3982
  const start = existsSync4(startPath) ? read(startPath) : "";
@@ -3638,11 +3997,11 @@ var tanstackSupabaseAdapter = {
3638
3997
  };
3639
3998
 
3640
3999
  // src/protect/install/adapters/next.ts
3641
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, copyFileSync as copyFileSync2 } from "fs";
3642
- import { join as join6 } from "path";
4000
+ import { existsSync as existsSync5 } from "fs";
4001
+ import { join as join7 } from "path";
3643
4002
  function hasNextDep(cwd) {
3644
4003
  try {
3645
- const pkg = JSON.parse(read(join6(cwd, "package.json")));
4004
+ const pkg = JSON.parse(read(join7(cwd, "package.json")));
3646
4005
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.next);
3647
4006
  } catch {
3648
4007
  return false;
@@ -3651,9 +4010,9 @@ function hasNextDep(cwd) {
3651
4010
  function middlewareInfo(cwd) {
3652
4011
  const candidates = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
3653
4012
  for (const rel of candidates) {
3654
- 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 };
3655
4014
  }
3656
- const useSrc = existsSync5(join6(cwd, "src"));
4015
+ const useSrc = existsSync5(join7(cwd, "src"));
3657
4016
  return { relDir: useSrc ? "src" : ".", relFile: useSrc ? "src/middleware.ts" : "middleware.ts", exists: false };
3658
4017
  }
3659
4018
  function detect2(cwd) {
@@ -3665,15 +4024,15 @@ function rulesFile(relDir) {
3665
4024
  function wire2(cwd, opts) {
3666
4025
  const templates = templatesDir();
3667
4026
  const mw = middlewareInfo(cwd);
3668
- const dir = join6(cwd, mw.relDir === "." ? "" : mw.relDir);
3669
- mkdirSync2(dir, { recursive: true });
3670
- 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");
3671
4030
  const changed = [];
3672
4031
  if (opts.demo || !existsSync5(rulesDst)) {
3673
- copyFileSync2(join6(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
4032
+ copyProjectFileSync(cwd, join7(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
3674
4033
  changed.push(rulesFile(mw.relDir));
3675
4034
  }
3676
- const mwPath = join6(cwd, mw.relFile);
4035
+ const mwPath = join7(cwd, mw.relFile);
3677
4036
  const existing = mw.exists ? read(mwPath) : "";
3678
4037
  if (mw.exists && existing.includes("patchstack-next")) {
3679
4038
  log(`${mw.relFile} already has the Patchstack middleware \u2014 left as-is`);
@@ -3685,7 +4044,7 @@ function wire2(cwd, opts) {
3685
4044
  );
3686
4045
  return { ok: true, changed };
3687
4046
  }
3688
- copyFileSync2(join6(templates, "next-middleware.ts"), mwPath);
4047
+ copyProjectFileSync(cwd, join7(templates, "next-middleware.ts"), mwPath);
3689
4048
  if (!opts.demo) bakeSiteUuid(cwd, mw.relFile);
3690
4049
  changed.push(mw.relFile);
3691
4050
  log(`scaffolded ${mw.relFile} (Patchstack middleware)`);
@@ -3693,8 +4052,8 @@ function wire2(cwd, opts) {
3693
4052
  }
3694
4053
  function verify2(cwd) {
3695
4054
  const mw = middlewareInfo(cwd);
3696
- const present2 = mw.exists && read(join6(cwd, mw.relFile)).includes("patchstack-next");
3697
- 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)));
3698
4057
  return {
3699
4058
  wired: present2 && rulesPresent,
3700
4059
  checks: [
@@ -3712,27 +4071,27 @@ var nextAdapter = {
3712
4071
  };
3713
4072
 
3714
4073
  // src/protect/install/adapters/sveltekit.ts
3715
- import { join as join8 } from "path";
4074
+ import { join as join9 } from "path";
3716
4075
 
3717
4076
  // src/protect/install/seam.ts
3718
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, copyFileSync as copyFileSync3 } from "fs";
3719
- 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";
3720
4079
  function rulesRel(seamRel) {
3721
- const d = dirname2(seamRel);
4080
+ const d = dirname3(seamRel);
3722
4081
  return (d === "." ? "patchstack.rules.json" : `${d}/patchstack.rules.json`).replace(/\\/g, "/");
3723
4082
  }
3724
4083
  function wireSeam(cwd, opts, spec) {
3725
4084
  const templates = templatesDir();
3726
- const existing = spec.candidates.find((c) => existsSync6(join7(cwd, c)));
4085
+ const existing = spec.candidates.find((c) => existsSync6(join8(cwd, c)));
3727
4086
  const seamRel = existing ?? spec.target;
3728
- mkdirSync3(dirname2(join7(cwd, seamRel)), { recursive: true });
3729
- const rulesDst = join7(cwd, rulesRel(seamRel));
4087
+ ensureProjectDirectorySync(cwd, dirname3(join8(cwd, seamRel)));
4088
+ const rulesDst = join8(cwd, rulesRel(seamRel));
3730
4089
  const changed = [];
3731
4090
  if (opts.demo || !existsSync6(rulesDst)) {
3732
- copyFileSync3(join7(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
4091
+ copyProjectFileSync(cwd, join8(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
3733
4092
  changed.push(rulesRel(seamRel));
3734
4093
  }
3735
- const current = existing ? read(join7(cwd, existing)) : "";
4094
+ const current = existing ? read(join8(cwd, existing)) : "";
3736
4095
  if (existing && current.includes(spec.marker)) {
3737
4096
  log(`${existing} already has the Patchstack guard \u2014 left as-is`);
3738
4097
  return { ok: true, changed };
@@ -3741,17 +4100,17 @@ function wireSeam(cwd, opts, spec) {
3741
4100
  log(`existing ${existing} left untouched \u2014 scaffolded ${rulesRel(seamRel)}; ${spec.planHint}`);
3742
4101
  return { ok: true, changed };
3743
4102
  }
3744
- copyFileSync3(join7(templates, spec.templateName), join7(cwd, seamRel));
4103
+ copyProjectFileSync(cwd, join8(templates, spec.templateName), join8(cwd, seamRel));
3745
4104
  if (!opts.demo) bakeSiteUuid(cwd, seamRel);
3746
4105
  changed.push(seamRel);
3747
4106
  log(`scaffolded ${seamRel}`);
3748
4107
  return { ok: true, changed: [...new Set(changed)] };
3749
4108
  }
3750
4109
  function verifySeam(cwd, spec) {
3751
- const existing = spec.candidates.find((c) => existsSync6(join7(cwd, c)));
4110
+ const existing = spec.candidates.find((c) => existsSync6(join8(cwd, c)));
3752
4111
  const seamRel = existing ?? spec.target;
3753
- const present2 = existing ? read(join7(cwd, existing)).includes(spec.marker) : false;
3754
- 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)));
3755
4114
  return {
3756
4115
  wired: present2 && rulesPresent,
3757
4116
  checks: [
@@ -3772,7 +4131,7 @@ var SPEC = {
3772
4131
  };
3773
4132
  function detect3(cwd) {
3774
4133
  try {
3775
- const pkg = JSON.parse(read(join8(cwd, "package.json")));
4134
+ const pkg = JSON.parse(read(join9(cwd, "package.json")));
3776
4135
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }["@sveltejs/kit"]);
3777
4136
  } catch {
3778
4137
  return false;
@@ -3793,7 +4152,7 @@ var sveltekitAdapter = {
3793
4152
  };
3794
4153
 
3795
4154
  // src/protect/install/adapters/astro.ts
3796
- import { join as join9 } from "path";
4155
+ import { join as join10 } from "path";
3797
4156
  var SPEC2 = {
3798
4157
  templateName: "astro-middleware.ts",
3799
4158
  candidates: ["src/middleware.ts", "src/middleware.js", "src/middleware/index.ts", "src/middleware/index.js"],
@@ -3804,7 +4163,7 @@ var SPEC2 = {
3804
4163
  };
3805
4164
  function detect4(cwd) {
3806
4165
  try {
3807
- const pkg = JSON.parse(read(join9(cwd, "package.json")));
4166
+ const pkg = JSON.parse(read(join10(cwd, "package.json")));
3808
4167
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.astro);
3809
4168
  } catch {
3810
4169
  return false;
@@ -3851,8 +4210,8 @@ var nuxtAdapter = {
3851
4210
  };
3852
4211
 
3853
4212
  // src/protect/install/find-app.ts
3854
- import { readFileSync as readFileSync7, existsSync as existsSync7, readdirSync as readdirSync3, lstatSync } from "fs";
3855
- 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";
3856
4215
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3857
4216
  "node_modules",
3858
4217
  "patchstack",
@@ -3873,7 +4232,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3873
4232
  ]);
3874
4233
  var MAX_DEPTH = 8;
3875
4234
  function findAppInstance(cwd, re) {
3876
- const root = existsSync7(join10(cwd, "src")) ? join10(cwd, "src") : cwd;
4235
+ const root = existsSync7(join11(cwd, "src")) ? join11(cwd, "src") : cwd;
3877
4236
  const found = [];
3878
4237
  const walk2 = (dir, depth) => {
3879
4238
  if (depth > MAX_DEPTH) return;
@@ -3885,10 +4244,10 @@ function findAppInstance(cwd, re) {
3885
4244
  }
3886
4245
  for (const name of entries) {
3887
4246
  if (SKIP_DIRS2.has(name) || name.startsWith(".")) continue;
3888
- const p = join10(dir, name);
4247
+ const p = join11(dir, name);
3889
4248
  let st;
3890
4249
  try {
3891
- st = lstatSync(p);
4250
+ st = lstatSync2(p);
3892
4251
  } catch {
3893
4252
  continue;
3894
4253
  }
@@ -3897,7 +4256,7 @@ function findAppInstance(cwd, re) {
3897
4256
  else if (/\.(?:ts|js|mjs|cjs)$/.test(name)) {
3898
4257
  let src;
3899
4258
  try {
3900
- src = readFileSync7(p, "utf8");
4259
+ src = readFileSync8(p, "utf8");
3901
4260
  } catch {
3902
4261
  continue;
3903
4262
  }
@@ -3905,7 +4264,7 @@ function findAppInstance(cwd, re) {
3905
4264
  if (!m) continue;
3906
4265
  const appVar = m[1];
3907
4266
  found.push({
3908
- relPath: relative2(cwd, p).replace(/\\/g, "/"),
4267
+ relPath: relative3(cwd, p).replace(/\\/g, "/"),
3909
4268
  appVar,
3910
4269
  listens: listensOn(src, appVar)
3911
4270
  });
@@ -3944,7 +4303,7 @@ function declaredEntries(cwd) {
3944
4303
  const paths = /* @__PURE__ */ new Set();
3945
4304
  let pkg;
3946
4305
  try {
3947
- pkg = JSON.parse(readFileSync7(join10(cwd, "package.json"), "utf8"));
4306
+ pkg = JSON.parse(readFileSync8(join11(cwd, "package.json"), "utf8"));
3948
4307
  } catch {
3949
4308
  return paths;
3950
4309
  }
@@ -3963,24 +4322,24 @@ function declaredEntries(cwd) {
3963
4322
  return paths;
3964
4323
  }
3965
4324
  function importSpecifier(fromRel, toRel, preserveExtension = false) {
3966
- let spec = relative2(dirname3(fromRel), toRel).replace(/\\/g, "/");
4325
+ let spec = relative3(dirname4(fromRel), toRel).replace(/\\/g, "/");
3967
4326
  if (!preserveExtension) spec = spec.replace(/\.(?:ts|js)$/, "");
3968
4327
  if (!spec.startsWith(".")) spec = `./${spec}`;
3969
4328
  return spec;
3970
4329
  }
3971
4330
 
3972
4331
  // src/protect/install/register.ts
3973
- import { writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
3974
- 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";
3975
4334
 
3976
4335
  // src/protect/install/generic.ts
3977
- import { readFileSync as readFileSync8, existsSync as existsSync9, mkdirSync as mkdirSync4, copyFileSync as copyFileSync4, readdirSync as readdirSync4, lstatSync as lstatSync2 } from "fs";
3978
- 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";
3979
4338
 
3980
4339
  // src/protect/install/source-scope.ts
3981
4340
  import { execFileSync } from "child_process";
3982
4341
  import { existsSync as existsSync8 } from "fs";
3983
- import { dirname as dirname4, join as join11, resolve } from "path";
4342
+ import { dirname as dirname5, join as join12, resolve as resolve2 } from "path";
3984
4343
 
3985
4344
  // src/protect/install/node-flags.ts
3986
4345
  var flagName = (token) => token.split("=")[0];
@@ -4269,9 +4628,9 @@ function moduleKindOf(filePath, packageIsModule) {
4269
4628
  return packageIsModule ? "esm" : "cjs";
4270
4629
  }
4271
4630
  function resolvesToGuardModule(specifier, query) {
4272
- const resolved = resolve(dirname4(query.fromFile), specifier);
4273
- if (withoutModuleExtension(resolved) !== join11(query.guardDir, "guard")) return false;
4274
- 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)));
4275
4634
  if (existing.length === 0) return false;
4276
4635
  const extension = /(\.[A-Za-z0-9]+)$/.exec(specifier)?.[1] ?? "";
4277
4636
  if (!query.strictExtensions) return true;
@@ -4290,16 +4649,16 @@ function withoutModuleExtension(filePath) {
4290
4649
  // src/protect/install/generic.ts
4291
4650
  function packageIsEsm(cwd) {
4292
4651
  try {
4293
- return JSON.parse(read(join12(cwd, "package.json"))).type === "module";
4652
+ return JSON.parse(read(join13(cwd, "package.json"))).type === "module";
4294
4653
  } catch {
4295
4654
  return false;
4296
4655
  }
4297
4656
  }
4298
4657
  function genericDir(cwd) {
4299
- return existsSync9(join12(cwd, "src")) ? "src/patchstack" : "patchstack";
4658
+ return existsSync9(join13(cwd, "src")) ? "src/patchstack" : "patchstack";
4300
4659
  }
4301
4660
  function hasTypeScript(cwd) {
4302
- 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));
4303
4662
  }
4304
4663
  function genericGuardTarget(cwd) {
4305
4664
  if (hasTypeScript(cwd)) {
@@ -4313,7 +4672,7 @@ function genericGuardTarget(cwd) {
4313
4672
  function genericScaffoldFiles(cwd) {
4314
4673
  const dir = genericDir(cwd);
4315
4674
  const candidates = ["guard.ts", "guard.js", "guard.cjs", "rules.json"];
4316
- 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)));
4317
4676
  }
4318
4677
  function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
4319
4678
  const target = genericGuardTarget(cwd);
@@ -4321,15 +4680,15 @@ function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
4321
4680
  guardFile ??= target.file;
4322
4681
  const templates = templatesDir();
4323
4682
  const dir = genericDir(cwd);
4324
- const dst = join12(cwd, dir);
4325
- mkdirSync4(dst, { recursive: true });
4326
- 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));
4327
4686
  const guardRel = `${dir}/${guardFile}`;
4328
4687
  const changed = [guardRel];
4329
4688
  if (!opts.demo) bakeSiteUuid(cwd, guardRel);
4330
- const rulesDst = join12(dst, "rules.json");
4689
+ const rulesDst = join13(dst, "rules.json");
4331
4690
  if (opts.demo || !existsSync9(rulesDst)) {
4332
- copyFileSync4(join12(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
4691
+ copyProjectFileSync(cwd, join13(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
4333
4692
  changed.push(`${dir}/rules.json`);
4334
4693
  }
4335
4694
  return { changed, dir };
@@ -4350,17 +4709,17 @@ function candidateEntries(cwd) {
4350
4709
  "app.ts",
4351
4710
  "app.js"
4352
4711
  ];
4353
- const hits = names.filter((n) => existsSync9(join12(cwd, n)));
4712
+ const hits = names.filter((n) => existsSync9(join13(cwd, n)));
4354
4713
  try {
4355
- const pkg = JSON.parse(read(join12(cwd, "package.json")));
4356
- 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);
4357
4716
  } catch {
4358
4717
  }
4359
4718
  return [...new Set(hits)];
4360
4719
  }
4361
4720
  function usesExpress(cwd) {
4362
4721
  try {
4363
- const pkg = JSON.parse(read(join12(cwd, "package.json")));
4722
+ const pkg = JSON.parse(read(join13(cwd, "package.json")));
4364
4723
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.express);
4365
4724
  } catch {
4366
4725
  return false;
@@ -4464,9 +4823,9 @@ function escapeForRegExp(value) {
4464
4823
  function genericVerify(cwd) {
4465
4824
  const dir = genericDir(cwd);
4466
4825
  const target = genericGuardTarget(cwd);
4467
- const present2 = GUARD_FILENAMES.find((name) => existsSync9(join12(cwd, dir, name)));
4826
+ const present2 = GUARD_FILENAMES.find((name) => existsSync9(join13(cwd, dir, name)));
4468
4827
  const scaffolded = present2 !== void 0;
4469
- const imported = scaffolded && guardIsImported(cwd, join12(cwd, dir, present2));
4828
+ const imported = scaffolded && guardIsImported(cwd, join13(cwd, dir, present2));
4470
4829
  return {
4471
4830
  wired: scaffolded && imported,
4472
4831
  checks: [
@@ -4500,9 +4859,9 @@ var NON_RUNTIME_DIRS = /* @__PURE__ */ new Set([
4500
4859
  "__mocks__"
4501
4860
  ]);
4502
4861
  function guardIsImported(cwd, guardPath) {
4503
- const guardDir = dirname5(guardPath);
4862
+ const guardDir = dirname6(guardPath);
4504
4863
  const packageIsModule = packageIsEsm(cwd);
4505
- const root = existsSync9(join12(cwd, "src")) ? join12(cwd, "src") : cwd;
4864
+ const root = existsSync9(join13(cwd, "src")) ? join13(cwd, "src") : cwd;
4506
4865
  let found = false;
4507
4866
  const walk2 = (d, depth) => {
4508
4867
  if (found || depth > 8) return;
@@ -4515,10 +4874,10 @@ function guardIsImported(cwd, guardPath) {
4515
4874
  for (const name of entries) {
4516
4875
  if (found) return;
4517
4876
  if (NON_RUNTIME_DIRS.has(name) || name.startsWith(".")) continue;
4518
- const p = join12(d, name);
4877
+ const p = join13(d, name);
4519
4878
  let st;
4520
4879
  try {
4521
- st = lstatSync2(p);
4880
+ st = lstatSync3(p);
4522
4881
  } catch {
4523
4882
  continue;
4524
4883
  }
@@ -4526,7 +4885,7 @@ function guardIsImported(cwd, guardPath) {
4526
4885
  if (st.isDirectory()) walk2(p, depth + 1);
4527
4886
  else if (/\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(name) && p !== guardPath) {
4528
4887
  try {
4529
- if (importsAndUsesGuard(readFileSync8(p, "utf8"), p, guardDir, packageIsModule)) found = true;
4888
+ if (importsAndUsesGuard(readFileSync9(p, "utf8"), p, guardDir, packageIsModule)) found = true;
4530
4889
  } catch {
4531
4890
  }
4532
4891
  }
@@ -4574,7 +4933,7 @@ function guardTarget(cwd, entryRel, spec) {
4574
4933
  }
4575
4934
  function packageType(cwd) {
4576
4935
  try {
4577
- return JSON.parse(read(join13(cwd, "package.json"))).type;
4936
+ return JSON.parse(read(join14(cwd, "package.json"))).type;
4578
4937
  } catch {
4579
4938
  return void 0;
4580
4939
  }
@@ -4610,8 +4969,8 @@ function bindsGuard(line, maskedLine, importName, call, fileRel, guard) {
4610
4969
  }
4611
4970
  function namesGuardModule(specifier, fileRel, guard) {
4612
4971
  return resolvesToGuardModule(specifier, {
4613
- fromFile: join13(guard.cwd, fileRel),
4614
- guardDir: resolve2(join13(guard.cwd, guard.guardDir)),
4972
+ fromFile: join14(guard.cwd, fileRel),
4973
+ guardDir: resolve3(join14(guard.cwd, guard.guardDir)),
4615
4974
  kind: moduleKindOf(fileRel, packageType(guard.cwd) === "module"),
4616
4975
  strictExtensions: true
4617
4976
  });
@@ -4630,7 +4989,7 @@ function wireRegister(cwd, opts, spec) {
4630
4989
  }
4631
4990
  const target = guardTarget(cwd, entry.relPath, spec);
4632
4991
  const { changed, dir } = scaffoldGeneric(cwd, opts, target.template, target.file);
4633
- const p = join13(cwd, entry.relPath);
4992
+ const p = join14(cwd, entry.relPath);
4634
4993
  const s = read(p);
4635
4994
  const state = wiringState(s, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir });
4636
4995
  if (state.importAtTopLevel && state.callInAppScope) {
@@ -4669,7 +5028,7 @@ function wireRegister(cwd, opts, spec) {
4669
5028
  lines.splice(callIdx + 1, 0, REGION, spec.call(entry.appVar), "// #endregion patchstack");
4670
5029
  }
4671
5030
  const patched = lines.join("\n");
4672
- writeFileSync5(p, patched);
5031
+ writeProjectFileSync(cwd, p, patched, { encoding: "utf8" });
4673
5032
  if (spec.callAfter) {
4674
5033
  const guardIndex = lines.findIndex((line) => line.includes(spec.call(entry.appVar)));
4675
5034
  const early = routesBefore(patched, entry.appVar, guardIndex);
@@ -4681,7 +5040,7 @@ function wireRegister(cwd, opts, spec) {
4681
5040
  }
4682
5041
  const parsed = parses(p);
4683
5042
  if (parsed === false) {
4684
- writeFileSync5(p, s);
5043
+ writeProjectFileSync(cwd, p, s, { encoding: "utf8" });
4685
5044
  log(`${entry.relPath} would not parse after patching \u2014 reverted; ${spec.manualHint}`);
4686
5045
  return { ok: true, changed };
4687
5046
  }
@@ -4700,17 +5059,17 @@ function firstStatementLine(lines) {
4700
5059
  return i;
4701
5060
  }
4702
5061
  function serverIsGuarded(cwd, spec, server, guardDir) {
4703
- const source = read(join13(cwd, server.relPath));
5062
+ const source = read(join14(cwd, server.relPath));
4704
5063
  const state = wiringState(source, spec, server.appVar, server.relPath, { cwd, guardDir });
4705
5064
  if (!state.importAtTopLevel || !state.callInAppScope || !state.ordered) return false;
4706
5065
  return !spec.callAfter || routesBefore(source, server.appVar, state.callIndex).length === 0;
4707
5066
  }
4708
5067
  function verifyRegister(cwd, spec) {
4709
- const dir = existsSync10(join13(cwd, "src")) ? "src/patchstack" : "patchstack";
5068
+ const dir = existsSync10(join14(cwd, "src")) ? "src/patchstack" : "patchstack";
4710
5069
  const entry = findAppInstance(cwd, spec.appRe);
4711
5070
  const target = entry ? guardTarget(cwd, entry.relPath, spec) : null;
4712
- const scaffolded = target ? existsSync10(join13(cwd, dir, target.file)) : false;
4713
- 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)) : "";
4714
5073
  const state = entry ? wiringState(entrySource, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir }) : { importAtTopLevel: false, callInAppScope: false, ordered: false, callIndex: -1, appIndex: -1 };
4715
5074
  const wired = state.importAtTopLevel && state.callInAppScope && state.ordered;
4716
5075
  const early = spec.callAfter && entry ? routesBefore(entrySource, entry.appVar, state.callIndex) : [];
@@ -4801,7 +5160,7 @@ var expressAdapter = {
4801
5160
 
4802
5161
  // src/protect/install/reporting.ts
4803
5162
  import { existsSync as existsSync11 } from "fs";
4804
- import { join as join14 } from "path";
5163
+ import { join as join15 } from "path";
4805
5164
 
4806
5165
  // src/protect/reporting-state.js
4807
5166
  var REPORTING_STATES = Object.freeze([
@@ -4859,7 +5218,7 @@ function explainReportingState(state) {
4859
5218
 
4860
5219
  // src/protect/install/reporting.ts
4861
5220
  function siteIdentity(cwd, env) {
4862
- const rc = join14(cwd, ".patchstackrc.json");
5221
+ const rc = join15(cwd, ".patchstackrc.json");
4863
5222
  let recorded;
4864
5223
  if (existsSync11(rc)) {
4865
5224
  try {
@@ -5010,7 +5369,7 @@ function runVerify(cwd) {
5010
5369
  }
5011
5370
 
5012
5371
  // src/widget.ts
5013
- import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
5372
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
5014
5373
  import path11 from "path";
5015
5374
  var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
5016
5375
  var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
@@ -5064,10 +5423,10 @@ function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
5064
5423
  if (!existsSync12(file)) {
5065
5424
  return { shell: null, action: "no-shell" };
5066
5425
  }
5067
- const before = readFileSync9(file, "utf8");
5426
+ const before = readFileSync10(file, "utf8");
5068
5427
  const { html, action } = ensureWidgetInHtml(before, siteUuid);
5069
5428
  if (html !== before) {
5070
- writeFileSync6(file, html);
5429
+ writeProjectFileSync(cwd, file, html, { encoding: "utf8" });
5071
5430
  }
5072
5431
  return { shell, action };
5073
5432
  }
@@ -5157,7 +5516,7 @@ function installCommand(pm) {
5157
5516
  }
5158
5517
  function readPackageJson(cwd) {
5159
5518
  try {
5160
- return JSON.parse(readFileSync10(path12.join(cwd, "package.json"), "utf8"));
5519
+ return JSON.parse(readFileSync11(path12.join(cwd, "package.json"), "utf8"));
5161
5520
  } catch {
5162
5521
  return null;
5163
5522
  }
@@ -5165,7 +5524,7 @@ function readPackageJson(cwd) {
5165
5524
  function installedVersion(cwd, declaredRange) {
5166
5525
  try {
5167
5526
  const pkg = JSON.parse(
5168
- readFileSync10(path12.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
5527
+ readFileSync11(path12.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
5169
5528
  );
5170
5529
  if (typeof pkg.version === "string" && pkg.version.length > 0) {
5171
5530
  return pkg.version;
@@ -5206,10 +5565,10 @@ function findWidgetMarker(cwd, siteUuid) {
5206
5565
  }
5207
5566
  budget -= 1;
5208
5567
  try {
5209
- if (statSync4(full).size > WIDGET_SCAN_MAX_BYTES) {
5568
+ if (statSync5(full).size > WIDGET_SCAN_MAX_BYTES) {
5210
5569
  continue;
5211
5570
  }
5212
- const content = readFileSync10(full, "utf8");
5571
+ const content = readFileSync11(full, "utf8");
5213
5572
  if (!content.includes(WIDGET_NEEDLE2)) {
5214
5573
  continue;
5215
5574
  }
@@ -5232,7 +5591,7 @@ function findProductionMarker(cwd, widgetFileHint) {
5232
5591
  return false;
5233
5592
  }
5234
5593
  try {
5235
- return readFileSync10(path12.join(cwd, widgetFileHint), "utf8").includes(PROD_MARKER_NEEDLE);
5594
+ return readFileSync11(path12.join(cwd, widgetFileHint), "utf8").includes(PROD_MARKER_NEEDLE);
5236
5595
  } catch {
5237
5596
  return false;
5238
5597
  }
@@ -5271,7 +5630,7 @@ async function collectGuideState(cwd) {
5271
5630
  try {
5272
5631
  const config = await resolveConfig({ cwd });
5273
5632
  siteUuid = config.siteUuid;
5274
- if (siteUuid !== null) {
5633
+ if (siteUuid !== null && config.endpointTrusted !== false) {
5275
5634
  claimUrl = buildClaimUrl(config.endpoint, siteUuid);
5276
5635
  }
5277
5636
  if (config.endpoint !== DEFAULT_ENDPOINT) {
@@ -5535,10 +5894,19 @@ import { tmpdir as tmpdir2 } from "os";
5535
5894
  import path14 from "path";
5536
5895
 
5537
5896
  // src/device-flow.ts
5538
- 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";
5539
5907
  import { tmpdir } from "os";
5540
5908
  import path13 from "path";
5541
- import { createHash as createHash2 } from "crypto";
5909
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
5542
5910
  function baseFrom(manifestEndpoint) {
5543
5911
  const url = new URL(manifestEndpoint);
5544
5912
  const p = url.pathname.replace(/\/$/, "");
@@ -5552,16 +5920,61 @@ function pendingPath(siteUuid, intent) {
5552
5920
  return path13.join(tmpdir(), `patchstack-${intent}-${key}.json`);
5553
5921
  }
5554
5922
  function savePending(siteUuid, intent, pending) {
5555
- writeFileSync7(pendingPath(siteUuid, intent), JSON.stringify(pending), {
5556
- encoding: "utf8",
5557
- mode: 384
5558
- });
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
+ }
5559
5956
  }
5560
5957
  function readPending(siteUuid, intent) {
5958
+ let descriptor = null;
5561
5959
  try {
5562
- 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"));
5563
5969
  } catch {
5564
5970
  return null;
5971
+ } finally {
5972
+ if (descriptor !== null) {
5973
+ try {
5974
+ closeSync2(descriptor);
5975
+ } catch {
5976
+ }
5977
+ }
5565
5978
  }
5566
5979
  }
5567
5980
  function clearPending(siteUuid, intent) {
@@ -5579,17 +5992,25 @@ async function requestDeviceCode(config, intent, conflictMessage, deps = {}) {
5579
5992
  message: "No site UUID configured \u2014 run `patchstack-connect scan` first."
5580
5993
  };
5581
5994
  }
5582
- const base = baseFrom(config.endpoint);
5583
- const started = await pulseFetch(
5584
- config,
5585
- `${base}/device/code`,
5586
- {
5587
- method: "POST",
5588
- headers: { "Content-Type": "application/json", Accept: "application/json" },
5589
- body: JSON.stringify({ site_uuid: config.siteUuid, intent })
5590
- },
5591
- fetchImpl
5592
- );
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
+ }
5593
6014
  if (started.status === 409) return { status: "conflict", message: conflictMessage };
5594
6015
  if (started.status === 404) {
5595
6016
  return { status: "not-found", message: "Patchstack does not recognise this site UUID." };
@@ -5597,34 +6018,76 @@ async function requestDeviceCode(config, intent, conflictMessage, deps = {}) {
5597
6018
  if (!started.ok) {
5598
6019
  return { status: "failed", message: `Could not start the request (HTTP ${started.status}).` };
5599
6020
  }
5600
- 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
+ }
5601
6036
  const pending = {
5602
- deviceCode: body.device_code,
5603
- userCode: body.user_code,
6037
+ deviceCode,
6038
+ userCode,
5604
6039
  // Points at the API, which redirects to the dashboard SPA — the CLI only knows the API origin,
5605
6040
  // and the approval page lives on the app.
5606
- verificationUri: `${base}/device?code=${encodeURIComponent(body.user_code)}`,
5607
- expiresAt: now() + body.expires_in * 1e3,
5608
- intervalMs: Math.max(1, body.interval) * 1e3
6041
+ verificationUri: `${base}/device?code=${encodeURIComponent(userCode)}`,
6042
+ expiresAt: now() + expiresIn * 1e3,
6043
+ intervalMs: interval * 1e3
5609
6044
  };
5610
6045
  savePending(config.siteUuid, intent, pending);
5611
6046
  return { status: "started", pending };
5612
6047
  }
5613
6048
  async function pollDeviceToken(config, intent, pending, deps = {}) {
5614
6049
  const fetchImpl = deps.fetchImpl ?? fetch;
5615
- const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
5616
- method: "POST",
5617
- headers: { "Content-Type": "application/json", Accept: "application/json" },
5618
- body: JSON.stringify({ device_code: pending.deviceCode })
5619
- });
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
+ }
5620
6063
  if (polled.status === 428) return { state: "pending" };
5621
6064
  if (!polled.ok) {
5622
6065
  if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
5623
6066
  return { state: "expired" };
5624
6067
  }
5625
- const body = await polled.json();
6068
+ let raw;
6069
+ try {
6070
+ raw = await readBoundedJson(polled);
6071
+ } catch {
6072
+ raw = null;
6073
+ }
5626
6074
  if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
5627
- 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
+ };
5628
6091
  }
5629
6092
  async function pollUntilApproved(config, intent, pending, deps = {}) {
5630
6093
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
@@ -5726,11 +6189,11 @@ async function claim2(config, onPrompt, deps = {}) {
5726
6189
  }
5727
6190
 
5728
6191
  // src/protect/install/runtime/check.ts
5729
- import { relative as relative3 } from "path";
6192
+ import { relative as relative4 } from "path";
5730
6193
 
5731
6194
  // src/protect/install/runtime/entry.ts
5732
- import { existsSync as existsSync14, readFileSync as readFileSync12, realpathSync as realpathSync3, statSync as statSync5 } from "fs";
5733
- 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";
5734
6197
  var CONVENTIONAL = [
5735
6198
  "server.js",
5736
6199
  "server.mjs",
@@ -5756,31 +6219,31 @@ var CONVENTIONAL = [
5756
6219
  var LOADABLE = /\.(?:js|mjs|cjs)$/;
5757
6220
  function readPackage2(cwd) {
5758
6221
  try {
5759
- const parsed = JSON.parse(readFileSync12(join15(cwd, "package.json"), "utf8"));
6222
+ const parsed = JSON.parse(readFileSync13(join16(cwd, "package.json"), "utf8"));
5760
6223
  return parsed !== null && typeof parsed === "object" ? parsed : null;
5761
6224
  } catch {
5762
6225
  return null;
5763
6226
  }
5764
6227
  }
5765
- function isFile(path17) {
6228
+ function isFile(path18) {
5766
6229
  try {
5767
- return statSync5(path17).isFile();
6230
+ return statSync6(path18).isFile();
5768
6231
  } catch {
5769
6232
  return false;
5770
6233
  }
5771
6234
  }
5772
6235
  function loadable(cwd, candidate) {
5773
- const written = resolve3(cwd, candidate);
6236
+ const written = resolve4(cwd, candidate);
5774
6237
  if (!LOADABLE.test(written) || !isFile(written)) return null;
5775
6238
  let root;
5776
- let path17;
6239
+ let path18;
5777
6240
  try {
5778
- root = realpathSync3(cwd);
5779
- path17 = realpathSync3(written);
6241
+ root = realpathSync4(cwd);
6242
+ path18 = realpathSync4(written);
5780
6243
  } catch {
5781
6244
  return null;
5782
6245
  }
5783
- if (path17 !== root && !path17.startsWith(root + sep)) return null;
6246
+ if (path18 !== root && !path18.startsWith(root + sep2)) return null;
5784
6247
  return written;
5785
6248
  }
5786
6249
  function passable(token) {
@@ -5826,7 +6289,7 @@ function resolveEntry(cwd) {
5826
6289
  }
5827
6290
  return {
5828
6291
  kind: "unavailable",
5829
- 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"
5830
6293
  };
5831
6294
  }
5832
6295
 
@@ -5860,8 +6323,8 @@ function preloadPath() {
5860
6323
  // beside this file, in the repository
5861
6324
  ];
5862
6325
  for (const candidate of candidates) {
5863
- const path17 = fileURLToPath2(candidate);
5864
- if (existsSync15(path17)) return path17;
6326
+ const path18 = fileURLToPath2(candidate);
6327
+ if (existsSync15(path18)) return path18;
5865
6328
  }
5866
6329
  return null;
5867
6330
  }
@@ -5895,7 +6358,7 @@ async function askListener(listener, challenge, answer, budgetMs) {
5895
6358
  timeout: budgetMs
5896
6359
  };
5897
6360
  if (listener.scheme === "https") options.agent = new HttpsAgent({ rejectUnauthorized: false });
5898
- return new Promise((resolve4) => {
6361
+ return new Promise((resolve5) => {
5899
6362
  const req = send(options, (res) => {
5900
6363
  const body = [];
5901
6364
  let read2 = 0;
@@ -5909,7 +6372,7 @@ async function askListener(listener, challenge, answer, budgetMs) {
5909
6372
  });
5910
6373
  const finish = () => {
5911
6374
  const text = Buffer.concat(body).toString("utf8").trim();
5912
- resolve4({
6375
+ resolve5({
5913
6376
  ...listener,
5914
6377
  outcome: text === answer ? "traversed" : "answered-without-sentinel",
5915
6378
  detail: text === answer ? void 0 : `answered with ${res.statusCode ?? "?"} and no sentinel`
@@ -5919,7 +6382,7 @@ async function askListener(listener, challenge, answer, budgetMs) {
5919
6382
  res.on("close", finish);
5920
6383
  });
5921
6384
  req.on("timeout", () => req.destroy(new Error("timed out")));
5922
- req.on("error", (err) => resolve4({ ...listener, outcome: "unreachable", detail: err.message }));
6385
+ req.on("error", (err) => resolve5({ ...listener, outcome: "unreachable", detail: err.message }));
5923
6386
  req.end();
5924
6387
  });
5925
6388
  }
@@ -6013,12 +6476,12 @@ async function probeRuntimeTraversal(options) {
6013
6476
  let onFrozen = null;
6014
6477
  const freeze = async () => {
6015
6478
  if (!child.connected) return false;
6016
- return new Promise((resolve4) => {
6479
+ return new Promise((resolve5) => {
6017
6480
  const settle2 = (confirmed2) => {
6018
6481
  clearTimeout(timer);
6019
6482
  onFrozen = null;
6020
6483
  child.removeListener("exit", onGone);
6021
- resolve4(confirmed2);
6484
+ resolve5(confirmed2);
6022
6485
  };
6023
6486
  const onGone = () => settle2(false);
6024
6487
  const timer = setTimeout(() => settle2(false), Math.min(FREEZE_MS, remaining()));
@@ -6031,13 +6494,13 @@ async function probeRuntimeTraversal(options) {
6031
6494
  }
6032
6495
  });
6033
6496
  };
6034
- await new Promise((resolve4) => {
6497
+ await new Promise((resolve5) => {
6035
6498
  let settleTimer = null;
6036
6499
  const done = () => {
6037
6500
  discovering = false;
6038
6501
  clearTimeout(runTimer);
6039
6502
  if (settleTimer) clearTimeout(settleTimer);
6040
- resolve4();
6503
+ resolve5();
6041
6504
  };
6042
6505
  const runTimer = setTimeout(done, remaining());
6043
6506
  const settle2 = () => {
@@ -6177,7 +6640,7 @@ async function runRuntimeCheck(cwd, options = {}) {
6177
6640
  settleMs: options.settleMs,
6178
6641
  platform: options.platform
6179
6642
  });
6180
- 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 } };
6181
6644
  }
6182
6645
  function runtimeExitCode(report) {
6183
6646
  if (report.outcome === "proven") return 0;
@@ -6208,15 +6671,15 @@ function formatRuntimeCheck(report) {
6208
6671
  }
6209
6672
 
6210
6673
  // src/map-command.ts
6211
- import { writeFileSync as writeFileSync9 } from "fs";
6674
+ import path15 from "path";
6212
6675
 
6213
6676
  // src/map/ts-loader.ts
6214
6677
  import { createRequire } from "module";
6215
6678
  import { pathToFileURL } from "url";
6216
- import { join as join16 } from "path";
6679
+ import { join as join17 } from "path";
6217
6680
  async function loadTypeScript(cwd) {
6218
6681
  try {
6219
- const req = createRequire(pathToFileURL(join16(cwd, "package.json")));
6682
+ const req = createRequire(pathToFileURL(join17(cwd, "package.json")));
6220
6683
  const resolved = req.resolve("typescript");
6221
6684
  const mod = await import(pathToFileURL(resolved).href);
6222
6685
  return mod.default ?? mod;
@@ -6231,9 +6694,9 @@ async function loadTypeScript(cwd) {
6231
6694
  }
6232
6695
 
6233
6696
  // src/map/extract.ts
6234
- import { readFileSync as readFileSync15, realpathSync as realpathSync5 } from "fs";
6697
+ import { readFileSync as readFileSync16, realpathSync as realpathSync6 } from "fs";
6235
6698
  import { createHash as createHash5 } from "crypto";
6236
- import { relative as relative5 } from "path";
6699
+ import { relative as relative6 } from "path";
6237
6700
 
6238
6701
  // src/map/bindings.ts
6239
6702
  import { builtinModules } from "module";
@@ -6461,14 +6924,14 @@ function directSinks(node, ts, bindings, ctx) {
6461
6924
  const root = base ? rootIdentifier(base, ts) : void 0;
6462
6925
  if (!root) return {};
6463
6926
  const spec = bindings.resolve(root);
6464
- const relative7 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
6927
+ const relative8 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
6465
6928
  const pkg = npmPackageOf(spec);
6466
6929
  if (pkg) return { pkg, root, spec };
6467
- if (relative7 && ctx && spec) {
6930
+ if (relative8 && ctx && spec) {
6468
6931
  const viaModule = ctx.graph.importedBinding(ctx.file, spec, bindings.exportNameOf(root) ?? root);
6469
6932
  if (viaModule) return { pkg: viaModule.package, root, spec };
6470
6933
  }
6471
- return { local: bindings.locals.has(root), root, spec, relative: relative7 };
6934
+ return { local: bindings.locals.has(root), root, spec, relative: relative8 };
6472
6935
  };
6473
6936
  const attributionOf = (b, pkg) => b.pkg ? "import" : pkg ? "inferred" : void 0;
6474
6937
  const dbApi = (pkg, attribution) => {
@@ -6645,8 +7108,8 @@ function dedupeSinks(sinks, owner) {
6645
7108
  }
6646
7109
 
6647
7110
  // src/map/module-graph.ts
6648
- import { readFileSync as readFileSync13, realpathSync as realpathSync4, statSync as statSync6 } from "fs";
6649
- 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";
6650
7113
  var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
6651
7114
  function createModuleGraph(ts, opts) {
6652
7115
  const cache = /* @__PURE__ */ new Map();
@@ -6654,7 +7117,7 @@ function createModuleGraph(ts, opts) {
6654
7117
  if (cache.has(file)) return cache.get(file) ?? null;
6655
7118
  let entry = null;
6656
7119
  try {
6657
- const text = readFileSync13(file, "utf8");
7120
+ const text = readFileSync14(file, "utf8");
6658
7121
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
6659
7122
  const bindings = buildModuleBindings(sf, ts);
6660
7123
  entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
@@ -6670,7 +7133,7 @@ function createModuleGraph(ts, opts) {
6670
7133
  if (!opts.followOutside) {
6671
7134
  let real = target;
6672
7135
  try {
6673
- real = realpathSync4(target);
7136
+ real = realpathSync5(target);
6674
7137
  } catch {
6675
7138
  }
6676
7139
  if (!isInside(real, opts.boundary)) return void 0;
@@ -6688,7 +7151,7 @@ function createModuleGraph(ts, opts) {
6688
7151
  for (const callee of mod.calleesOf.get(exportName) ?? []) {
6689
7152
  for (const s of mod.fnSinks.get(callee) ?? []) collected.push(s);
6690
7153
  }
6691
- const rel = relative4(opts.cwd, target);
7154
+ const rel = relative5(opts.cwd, target);
6692
7155
  return collected.map((s) => ({ ...s, file: rel }));
6693
7156
  },
6694
7157
  // A different question about the same module: not "what sinks are in there" but "what does this
@@ -6714,7 +7177,7 @@ function createModuleGraph(ts, opts) {
6714
7177
  }
6715
7178
  function resolveRelativeModule(fromFile, spec) {
6716
7179
  if (!spec.startsWith(".")) return void 0;
6717
- const dir = dirname7(fromFile);
7180
+ const dir = dirname8(fromFile);
6718
7181
  const base = resolvePath(dir, spec);
6719
7182
  const candidates = [];
6720
7183
  const jsLike = /\.(js|jsx|mjs|cjs)$/.exec(base);
@@ -6724,10 +7187,10 @@ function resolveRelativeModule(fromFile, spec) {
6724
7187
  }
6725
7188
  candidates.push(base);
6726
7189
  for (const e of RESOLVE_EXTS) candidates.push(base + e);
6727
- 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));
6728
7191
  for (const c of candidates) {
6729
7192
  try {
6730
- if (statSync6(c).isFile()) return c;
7193
+ if (statSync7(c).isFile()) return c;
6731
7194
  } catch {
6732
7195
  }
6733
7196
  }
@@ -6753,8 +7216,8 @@ function collectCallees(sf, ts) {
6753
7216
  }
6754
7217
 
6755
7218
  // src/map/coordinates.ts
6756
- function runtimeCoordinate(source, path17) {
6757
- if (/\[\d*\]/.test(path17)) {
7219
+ function runtimeCoordinate(source, path18) {
7220
+ if (/\[\d*\]/.test(path18)) {
6758
7221
  return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
6759
7222
  }
6760
7223
  switch (source) {
@@ -6763,15 +7226,15 @@ function runtimeCoordinate(source, path17) {
6763
7226
  case "multipart":
6764
7227
  case "body":
6765
7228
  case "server-fn-data":
6766
- return { runtimeParameter: `post.${path17}` };
7229
+ return { runtimeParameter: `post.${path18}` };
6767
7230
  case "query":
6768
- return { runtimeParameter: `get.${path17}` };
7231
+ return { runtimeParameter: `get.${path18}` };
6769
7232
  case "cookie":
6770
- return { runtimeParameter: `cookie.${path17}` };
7233
+ return { runtimeParameter: `cookie.${path18}` };
6771
7234
  case "file":
6772
- return { runtimeParameter: `files.${path17}` };
7235
+ return { runtimeParameter: `files.${path18}` };
6773
7236
  case "header":
6774
- return { runtimeParameter: `server.HTTP_${path17.toUpperCase().replace(/-/g, "_")}` };
7237
+ return { runtimeParameter: `server.HTTP_${path18.toUpperCase().replace(/-/g, "_")}` };
6775
7238
  case "route-param":
6776
7239
  return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
6777
7240
  default:
@@ -6803,8 +7266,8 @@ function addressSpaceOf(source) {
6803
7266
  function isProvenFlow(confidence) {
6804
7267
  return confidence === "exact-local" || confidence === "transformed-local";
6805
7268
  }
6806
- function inputIdOf(source, path17) {
6807
- return `${addressSpaceOf(source)}:${path17}`;
7269
+ function inputIdOf(source, path18) {
7270
+ return `${addressSpaceOf(source)}:${path18}`;
6808
7271
  }
6809
7272
  function withCoordinates(fields, source) {
6810
7273
  return fields.map((f) => {
@@ -6996,6 +7459,7 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
6996
7459
  }
6997
7460
 
6998
7461
  // src/map/flows.ts
7462
+ var MAX_DEPENDENCY_INPUT_FLOWS_PER_ENDPOINT = 100;
6999
7463
  function spaceOfKey(key) {
7000
7464
  if (key === "query") return "get";
7001
7465
  if (key === "params") return "route-param";
@@ -7006,16 +7470,23 @@ function spaceOfKey(key) {
7006
7470
  if (key === "files") return "files";
7007
7471
  return void 0;
7008
7472
  }
7009
- function linkedFlows(body, params, inputs, sinks, ts) {
7010
- const { flows, limitations } = linkFlows(body, params, inputs, sinks, ts);
7011
- 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
+ };
7012
7481
  }
7013
- function linkFlows(bodyNode, params, inputs, sinks, ts) {
7014
- 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
+ }
7015
7486
  const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
7016
7487
  const rootPath = /* @__PURE__ */ new Map();
7017
- const addRoot = (name, path17, space) => {
7018
- 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 });
7019
7490
  };
7020
7491
  for (const p of params ?? []) {
7021
7492
  if (!p?.name) continue;
@@ -7146,7 +7617,57 @@ function linkFlows(bodyNode, params, inputs, sinks, ts) {
7146
7617
  }
7147
7618
  for (const l of sinkLimits) allLimits.push(l);
7148
7619
  }
7149
- 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 };
7150
7671
  }
7151
7672
  function dedupeLimitations(list) {
7152
7673
  const seen = /* @__PURE__ */ new Set();
@@ -7160,8 +7681,8 @@ function dedupeLimitations(list) {
7160
7681
  function join22(base, seg) {
7161
7682
  return base ? `${base}.${seg}` : seg;
7162
7683
  }
7163
- function normalizePath(path17) {
7164
- return path17.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
7684
+ function normalizePath(path18) {
7685
+ return path18.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
7165
7686
  }
7166
7687
  function fluentChainCalls(call, ts) {
7167
7688
  let root = call;
@@ -7192,7 +7713,7 @@ function fluentChainCalls(call, ts) {
7192
7713
  collect(root);
7193
7714
  return out;
7194
7715
  }
7195
- function taintedReadPaths(node, ts, rootPath) {
7716
+ function taintedReadPaths(node, ts, rootPath, includeDeferredBodies = true) {
7196
7717
  const out = [];
7197
7718
  const seen = /* @__PURE__ */ new Set();
7198
7719
  const add = (r) => {
@@ -7204,6 +7725,7 @@ function taintedReadPaths(node, ts, rootPath) {
7204
7725
  };
7205
7726
  const visit = (n) => {
7206
7727
  if (!n) return;
7728
+ if (!includeDeferredBodies && (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n))) return;
7207
7729
  if (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n)) {
7208
7730
  const read2 = pathFromTainted(n, ts, rootPath);
7209
7731
  if (read2 !== void 0) {
@@ -7286,6 +7808,120 @@ function pathFromTainted(node, ts, rootPath) {
7286
7808
  return { path: normalizePath([base.path, ...segs].filter(Boolean).join(".")), space };
7287
7809
  }
7288
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
+
7289
7925
  // src/map/entries.ts
7290
7926
  var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
7291
7927
  function extractFromFile(sf, ts, localSinks, bindings, ctx) {
@@ -7309,7 +7945,7 @@ function extractFromFile(sf, ts, localSinks, bindings, ctx) {
7309
7945
  ...spanOf(decl),
7310
7946
  inputs,
7311
7947
  sinks,
7312
- ...linkedFlows(handlerBody, handlerFn?.parameters, inputs, sinks, ts)
7948
+ ...linkedFlows(handlerBody, handlerFn?.parameters, inputs, sinks, ts, handlerInvocations(handlerBody, ts, bindings, ctx))
7313
7949
  };
7314
7950
  if (validatorCall && inputs.length === 0) ep.inputsResolved = false;
7315
7951
  out.push(ep);
@@ -7384,6 +8020,10 @@ function hasUseServerDirective(fn, ts) {
7384
8020
  const first = fn.body?.statements?.[0];
7385
8021
  return Boolean(first && ts.isExpressionStatement(first) && ts.isStringLiteralLike(first.expression) && first.expression.text === "use server");
7386
8022
  }
8023
+ function handlerInvocations(body, ts, bindings, ctx) {
8024
+ const sf = body?.getSourceFile?.();
8025
+ return sf ? collectInvocations(sf, ts, bindings, ctx, body).invocations : [];
8026
+ }
7387
8027
  function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, ctx, extra = {}) {
7388
8028
  const entryKind = kindLabel === "route-registration" || kindLabel === "server-action" || kindLabel === "edge-function" ? kindLabel : "route-handler";
7389
8029
  const payloadStyle = kindLabel === "server-action";
@@ -7402,14 +8042,14 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
7402
8042
  end: extra.end,
7403
8043
  inputs,
7404
8044
  sinks,
7405
- ...linkedFlows(body, params, inputs, sinks, ts)
8045
+ ...linkedFlows(body, params, inputs, sinks, ts, handlerInvocations(body, ts, bindings, ctx))
7406
8046
  };
7407
8047
  }
7408
8048
 
7409
8049
  // src/map/imports.ts
7410
- import { readFileSync as readFileSync14 } from "fs";
7411
- import { join as join18 } from "path";
7412
- var MAX_SITES = 5;
8050
+ import { readFileSync as readFileSync15 } from "fs";
8051
+ import { join as join19 } from "path";
8052
+ var MAX_SITES2 = 5;
7413
8053
  var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
7414
8054
  function isNpmPackageName(pkg) {
7415
8055
  return pkg.startsWith("node:") || NPM_NAME.test(pkg);
@@ -7419,7 +8059,7 @@ function readPathAliases(cwd) {
7419
8059
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
7420
8060
  let paths;
7421
8061
  try {
7422
- const raw = readFileSync14(join18(cwd, name), "utf8");
8062
+ const raw = readFileSync15(join19(cwd, name), "utf8");
7423
8063
  const parsed = JSON.parse(stripJsonComments(raw));
7424
8064
  paths = parsed.compilerOptions?.paths;
7425
8065
  } catch {
@@ -7623,7 +8263,7 @@ function createImportInventory(aliases = []) {
7623
8263
  for (const n of edge.names) acc.names.add(n);
7624
8264
  if (!parsed) acc.namesComplete = false;
7625
8265
  acc.siteCount++;
7626
- 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 });
7627
8267
  }
7628
8268
  },
7629
8269
  list() {
@@ -7659,127 +8299,15 @@ function lineAt(starts, pos) {
7659
8299
  return lo + 1;
7660
8300
  }
7661
8301
 
7662
- // src/map/invocations.ts
7663
- var MAX_SITES2 = 3;
7664
- function collectInvocations(sf, ts, bindings, ctx) {
7665
- const found = [];
7666
- const counts = { total: 0, dependency: 0, local: 0, ambiguous: 0 };
7667
- const isLocalName = (name, node) => bindings.locals.has(name) || isShadowedByEnclosingBinding(node, name, ts);
7668
- const lineOf2 = (node) => {
7669
- try {
7670
- return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
7671
- } catch {
7672
- return void 0;
7673
- }
7674
- };
7675
- const traceRoot = (root) => {
7676
- const specifier = bindings.resolve(root);
7677
- if (specifier === void 0) return null;
7678
- const direct = npmPackageOf(specifier);
7679
- if (direct !== void 0) {
7680
- const resolution = bindings.originOf(root) ?? "direct";
7681
- return { pkg: direct, specifier, resolution, named: resolution === "direct" };
7682
- }
7683
- const exportName = bindings.exportNameOf(root) ?? root;
7684
- const viaModule = ctx.graph.importedBinding(ctx.file, specifier, exportName);
7685
- if (viaModule !== void 0) {
7686
- return {
7687
- pkg: viaModule.package,
7688
- specifier,
7689
- resolution: "reexport",
7690
- named: viaModule.origin === "direct",
7691
- apiName: viaModule.name
7692
- };
7693
- }
7694
- return null;
7695
- };
7696
- const push = (traced, api, receiver, kind, node) => {
7697
- const span = spanOf(node);
7698
- found.push({
7699
- package: traced.pkg,
7700
- specifiers: [traced.specifier],
7701
- api,
7702
- ...receiver !== void 0 ? { receiver } : {},
7703
- symbol: receiver !== void 0 ? `${receiver}.${api}` : api,
7704
- kind,
7705
- // Only ever `import`: a global has no package to correlate against, and an inferred package is not
7706
- // evidence about the value being called.
7707
- attribution: "import",
7708
- resolution: traced.resolution,
7709
- callCount: 1,
7710
- sites: [{ file: ctx.owner, line: lineOf2(node), start: span.start, end: span.end }]
7711
- });
7712
- };
7713
- const visit = (node) => {
7714
- if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
7715
- const callee = node.expression;
7716
- counts.total++;
7717
- if (ts.isIdentifier(callee)) {
7718
- const traced = traceRoot(callee.text);
7719
- if (traced?.named) {
7720
- const api = traced.apiName ?? bindings.exportNameOf(callee.text) ?? callee.text;
7721
- push(traced, api, void 0, ts.isNewExpression(node) ? "construct" : "call", node);
7722
- counts.dependency++;
7723
- } else if (traced) {
7724
- counts.ambiguous++;
7725
- } else if (isLocalName(callee.text, node)) {
7726
- counts.local++;
7727
- } else {
7728
- counts.ambiguous++;
7729
- }
7730
- } else if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
7731
- const root = rootIdentifier(callee.expression, ts);
7732
- const traced = root !== void 0 ? traceRoot(root) : null;
7733
- if (traced && root !== void 0) {
7734
- counts.dependency++;
7735
- const receiver = traced.resolution === "direct" && bindings.isPackageMember(root) ? bindings.exportNameOf(root) ?? root : void 0;
7736
- push(traced, callee.name.text, receiver, "member", node);
7737
- } else if (root !== void 0 && ts.isIdentifier(callee.expression) && isLocalName(root, node)) {
7738
- counts.local++;
7739
- } else {
7740
- counts.ambiguous++;
7741
- }
7742
- } else {
7743
- counts.ambiguous++;
7744
- }
7745
- }
7746
- ts.forEachChild(node, visit);
7747
- };
7748
- visit(sf);
7749
- return { invocations: found, counts };
7750
- }
7751
- function createInvocationInventory() {
7752
- const byKey = /* @__PURE__ */ new Map();
7753
- return {
7754
- add(invocations) {
7755
- for (const invocation of invocations) {
7756
- const key = `${invocation.package}|${invocation.symbol}|${invocation.kind}|${invocation.resolution}`;
7757
- const existing = byKey.get(key);
7758
- if (existing === void 0) {
7759
- byKey.set(key, { ...invocation });
7760
- continue;
7761
- }
7762
- existing.callCount += invocation.callCount;
7763
- for (const specifier of invocation.specifiers) {
7764
- if (!existing.specifiers.includes(specifier)) existing.specifiers.push(specifier);
7765
- }
7766
- if (existing.sites.length < MAX_SITES2) existing.sites.push(...invocation.sites.slice(0, MAX_SITES2 - existing.sites.length));
7767
- }
7768
- },
7769
- list() {
7770
- return [...byKey.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, invocation]) => ({ ...invocation, specifiers: [...invocation.specifiers].sort() }));
7771
- }
7772
- };
7773
- }
7774
-
7775
8302
  // src/map/extract.ts
8303
+ var MAX_DEPENDENCY_INPUT_FLOWS_PER_MAP = 500;
7776
8304
  async function extractInputMap(cwd, ts, options = {}) {
7777
8305
  const notes = [];
7778
8306
  const endpoints = [];
7779
8307
  const failed = [];
7780
8308
  let boundary = cwd;
7781
8309
  try {
7782
- boundary = realpathSync5(cwd);
8310
+ boundary = realpathSync6(cwd);
7783
8311
  } catch {
7784
8312
  }
7785
8313
  const graph = createModuleGraph(ts, { cwd, boundary, followOutside: options.followSymlinks });
@@ -7787,6 +8315,7 @@ async function extractInputMap(cwd, ts, options = {}) {
7787
8315
  const files = collectSources(cwd, boundary, { followOutside: options.followSymlinks }, [], /* @__PURE__ */ new Set(), stats);
7788
8316
  const imports = createImportInventory(readPathAliases(cwd));
7789
8317
  const invocations = createInvocationInventory();
8318
+ let dependencyInputFlowCount = 0;
7790
8319
  let parsed = 0;
7791
8320
  let preFiltered = 0;
7792
8321
  let importScanFailures = 0;
@@ -7798,8 +8327,8 @@ async function extractInputMap(cwd, ts, options = {}) {
7798
8327
  const hopTargets = /* @__PURE__ */ new Set();
7799
8328
  for (const file of files) {
7800
8329
  try {
7801
- const text = readFileSync15(file, "utf8");
7802
- const relFile = relative5(cwd, file);
8330
+ const text = readFileSync16(file, "utf8");
8331
+ const relFile = relative6(cwd, file);
7803
8332
  sourceBytes += text.length;
7804
8333
  if (!hasEntrySignal(text)) {
7805
8334
  preFiltered++;
@@ -7830,6 +8359,13 @@ async function extractInputMap(cwd, ts, options = {}) {
7830
8359
  calls.ambiguous += called.counts.ambiguous;
7831
8360
  const localSinks = collectLocalSinks(sf, ts, bindings, ctx);
7832
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;
7833
8369
  if (ep.route === void 0 && ep.entryKind === "edge-function") {
7834
8370
  const fn = functionNameFromPath(relFile);
7835
8371
  if (fn) ep.route = "/" + fn;
@@ -7845,7 +8381,7 @@ async function extractInputMap(cwd, ts, options = {}) {
7845
8381
  }
7846
8382
  } catch (e) {
7847
8383
  if (typeof process !== "undefined" && process.env?.PS_MAP_DEBUG) console.error("[patchstack] map error", file, e);
7848
- failed.push(relative5(cwd, file));
8384
+ failed.push(relative6(cwd, file));
7849
8385
  }
7850
8386
  }
7851
8387
  let hopParsed = 0;
@@ -7853,10 +8389,10 @@ async function extractInputMap(cwd, ts, options = {}) {
7853
8389
  for (const target of hopTargets) {
7854
8390
  if (entryFiles.has(target)) continue;
7855
8391
  try {
7856
- const text = readFileSync15(target, "utf8");
8392
+ const text = readFileSync16(target, "utf8");
7857
8393
  const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
7858
8394
  const bindings = buildModuleBindings(sf, ts);
7859
- 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 });
7860
8396
  invocations.add(called.invocations);
7861
8397
  calls.total += called.counts.total;
7862
8398
  calls.dependency += called.counts.dependency;
@@ -7870,6 +8406,9 @@ async function extractInputMap(cwd, ts, options = {}) {
7870
8406
  }
7871
8407
  notes.push("Static analysis is best-effort \u2014 this is the DETECTED surface, not a proof of completeness.");
7872
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
+ }
7873
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.");
7874
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.');
7875
8414
  if (!options.followSymlinks) notes.push("Symlinks leaving the project directory were not followed (use --follow-symlinks to include them).");
@@ -7878,6 +8417,10 @@ async function extractInputMap(cwd, ts, options = {}) {
7878
8417
  notes.push(`${failed.length} file(s) could not be analyzed and were skipped (fail-open): ${sample}${failed.length > 5 ? ", \u2026" : ""}.`);
7879
8418
  }
7880
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
+ }
7881
8424
  if (unresolved > 0) {
7882
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).`);
7883
8426
  }
@@ -7888,7 +8431,7 @@ async function extractInputMap(cwd, ts, options = {}) {
7888
8431
  if (endpoints.length === 0) notes.push("No recognized server-side entry points found under the analyzed roots.");
7889
8432
  const importList = imports.list();
7890
8433
  const unmodelled = importList.filter((d) => d.recognizedSinkKinds.length === 0).length;
7891
- const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative5(cwd, d));
8434
+ const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative6(cwd, d));
7892
8435
  const importCoverageGaps = {
7893
8436
  unreadableFiles: failed.length,
7894
8437
  unscannableFiles: importScanFailures,
@@ -8002,8 +8545,8 @@ function getStringFlag(flags, name) {
8002
8545
  }
8003
8546
 
8004
8547
  // src/build-stamp.ts
8005
- import { chmodSync, lstatSync as lstatSync3, readFileSync as readFileSync16, readdirSync as readdirSync6, realpathSync as realpathSync6, renameSync, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
8006
- 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";
8007
8550
  var RULES_FILE_NAMES = /* @__PURE__ */ new Set(["rules.json", "patchstack.rules.json"]);
8008
8551
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "tmp", "vendor"]);
8009
8552
  var MAX_DEPTH2 = 6;
@@ -8021,11 +8564,11 @@ function guardImportsRulesFile(dir, rulesName) {
8021
8564
  }
8022
8565
  for (const name of siblings) {
8023
8566
  if (!GUARD_SOURCE.test(name)) continue;
8024
- const path17 = join19(dir, name);
8567
+ const path18 = join20(dir, name);
8025
8568
  try {
8026
- const entry = lstatSync3(path17);
8569
+ const entry = lstatSync4(path18);
8027
8570
  if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_GUARD_SOURCE_BYTES) continue;
8028
- const source = stripComments(readFileSync16(path17, "utf8"));
8571
+ const source = stripComments(readFileSync17(path18, "utf8"));
8029
8572
  if (source.includes("@patchstack/connect/protect") && importsRules.test(source)) return true;
8030
8573
  } catch {
8031
8574
  }
@@ -8042,14 +8585,14 @@ function findRulesFile(cwd) {
8042
8585
  const unreadable = [];
8043
8586
  let root;
8044
8587
  try {
8045
- root = realpathSync6(cwd);
8588
+ root = realpathSync7(cwd);
8046
8589
  } catch {
8047
8590
  return { kind: "none" };
8048
8591
  }
8049
- const contained = (path17) => {
8592
+ const contained = (path18) => {
8050
8593
  try {
8051
- const real = realpathSync6(path17);
8052
- return real === root || real.startsWith(root + sep2);
8594
+ const real = realpathSync7(path18);
8595
+ return real === root || real.startsWith(root + sep3);
8053
8596
  } catch {
8054
8597
  return false;
8055
8598
  }
@@ -8064,32 +8607,32 @@ function findRulesFile(cwd) {
8064
8607
  }
8065
8608
  for (const name of entries) {
8066
8609
  if (name.startsWith(".")) continue;
8067
- const path17 = join19(dir, name);
8610
+ const path18 = join20(dir, name);
8068
8611
  let entry;
8069
8612
  try {
8070
- entry = lstatSync3(path17);
8613
+ entry = lstatSync4(path18);
8071
8614
  } catch {
8072
8615
  continue;
8073
8616
  }
8074
8617
  if (entry.isSymbolicLink()) continue;
8075
8618
  if (entry.isDirectory()) {
8076
- if (!SKIP_DIRECTORIES.has(name)) walk2(path17, depth + 1);
8619
+ if (!SKIP_DIRECTORIES.has(name)) walk2(path18, depth + 1);
8077
8620
  continue;
8078
8621
  }
8079
8622
  if (!entry.isFile() || !RULES_FILE_NAMES.has(name)) continue;
8080
- if (!contained(path17)) continue;
8623
+ if (!contained(path18)) continue;
8081
8624
  if (!guardImportsRulesFile(dir, name)) continue;
8082
8625
  try {
8083
- if (isRuleBundle(JSON.parse(readFileSync16(path17, "utf8")))) found.push(path17);
8626
+ if (isRuleBundle(JSON.parse(readFileSync17(path18, "utf8")))) found.push(path18);
8084
8627
  } catch {
8085
- unreadable.push(path17);
8628
+ unreadable.push(path18);
8086
8629
  }
8087
8630
  }
8088
8631
  };
8089
8632
  walk2(cwd, 0);
8090
- 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)) };
8091
8634
  if (found.length === 1) return { kind: "one", path: found[0] };
8092
- 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)) };
8093
8636
  return { kind: "none" };
8094
8637
  }
8095
8638
  function detectIndent(text) {
@@ -8110,10 +8653,10 @@ function applyBuildStamp(cwd, id) {
8110
8653
  reason: `more than one candidate guard rules file (${location.paths.join(", ")}) \u2014 none was stamped`
8111
8654
  };
8112
8655
  }
8113
- const file = relative6(cwd, location.path);
8656
+ const file = relative7(cwd, location.path);
8114
8657
  let text;
8115
8658
  try {
8116
- text = readFileSync16(location.path, "utf8");
8659
+ text = readFileSync17(location.path, "utf8");
8117
8660
  } catch (err) {
8118
8661
  return { kind: "skipped", reason: `${file} could not be read (${err.message})` };
8119
8662
  }
@@ -8133,17 +8676,9 @@ function applyBuildStamp(cwd, id) {
8133
8676
  if (Object.keys(namespace).length === 0) delete bundle[BUILD_STAMP_KEY];
8134
8677
  else bundle[BUILD_STAMP_KEY] = namespace;
8135
8678
  const serialised = JSON.stringify(bundle, null, detectIndent(text)) + (text.endsWith("\n") ? "\n" : "");
8136
- const temporary = `${location.path}.patchstack-${process.pid}.tmp`;
8137
8679
  try {
8138
- const mode = lstatSync3(location.path).mode & 4095;
8139
- writeFileSync8(temporary, serialised, { encoding: "utf8", mode });
8140
- chmodSync(temporary, mode);
8141
- renameSync(temporary, location.path);
8680
+ writeProjectFileSync(cwd, location.path, serialised, { encoding: "utf8" });
8142
8681
  } catch (err) {
8143
- try {
8144
- unlinkSync(temporary);
8145
- } catch {
8146
- }
8147
8682
  return { kind: "skipped", reason: `${file} could not be written (${err.message})` };
8148
8683
  }
8149
8684
  return id === null ? { kind: "cleared", file } : { kind: "stamped", file, id };
@@ -8282,7 +8817,7 @@ async function runMap(flags) {
8282
8817
  const json = JSON.stringify(map, null, 2);
8283
8818
  const out = getStringFlag(flags, "out");
8284
8819
  if (out) {
8285
- writeFileSync9(out, json);
8820
+ atomicWriteFileSync(path15.resolve(out), json, { encoding: "utf8" });
8286
8821
  console.error(`patchstack: wrote ${out}`);
8287
8822
  } else if (flags.get("upload") !== true) {
8288
8823
  console.log(json);
@@ -8331,8 +8866,8 @@ async function runMap(flags) {
8331
8866
  }
8332
8867
 
8333
8868
  // src/setup.ts
8334
- import { readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
8335
- import path15 from "path";
8869
+ import { readFileSync as readFileSync18 } from "fs";
8870
+ import path16 from "path";
8336
8871
  var SCAN_COMMAND = "patchstack-connect scan";
8337
8872
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
8338
8873
  function setupProtection(cwd) {
@@ -8357,8 +8892,8 @@ function prependHook(existing, command) {
8357
8892
  return [command, ...remaining].join(" && ");
8358
8893
  }
8359
8894
  function wireBuildScripts(cwd, packageManager) {
8360
- const target = path15.join(cwd, "package.json");
8361
- const raw = readFileSync17(target, "utf8");
8895
+ const target = path16.join(cwd, "package.json");
8896
+ const raw = readFileSync18(target, "utf8");
8362
8897
  const pkg = JSON.parse(raw);
8363
8898
  const scripts = pkg.scripts ?? {};
8364
8899
  const build = scripts.build;
@@ -8404,7 +8939,7 @@ function wireBuildScripts(cwd, packageManager) {
8404
8939
  const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
8405
8940
  const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
8406
8941
  const trailingNewline = raw.endsWith("\n") ? "\n" : "";
8407
- writeFileSync10(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
8942
+ writeProjectFileSync(cwd, target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, { encoding: "utf8" });
8408
8943
  if (build === void 0 || build.trim().length === 0) {
8409
8944
  return {
8410
8945
  changed: true,
@@ -9241,7 +9776,7 @@ async function runGuide(args) {
9241
9776
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
9242
9777
  console.log("");
9243
9778
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
9244
- console.log(readFileSync18(guidePath, "utf8"));
9779
+ console.log(readFileSync19(guidePath, "utf8"));
9245
9780
  return 0;
9246
9781
  }
9247
9782
  async function runSetup(args) {
@@ -9416,7 +9951,7 @@ function describeStack(stack) {
9416
9951
  }
9417
9952
  function readBuildScript(cwd) {
9418
9953
  try {
9419
- const pkg = JSON.parse(readFileSync18(path16.join(cwd, "package.json"), "utf8"));
9954
+ const pkg = JSON.parse(readFileSync19(path17.join(cwd, "package.json"), "utf8"));
9420
9955
  return pkg.scripts?.build;
9421
9956
  } catch {
9422
9957
  return void 0;
@@ -9496,7 +10031,7 @@ async function runMarkBuild(args) {
9496
10031
  let marked = 0;
9497
10032
  let widgetTouched = 0;
9498
10033
  for (const file of files) {
9499
- const before = readFileSync18(file, "utf8");
10034
+ const before = readFileSync19(file, "utf8");
9500
10035
  let after = injectMarker(before, snippet);
9501
10036
  if (widgetUuid !== null) {
9502
10037
  const ensured = ensureWidgetInHtml(after, widgetUuid);
@@ -9506,7 +10041,7 @@ async function runMarkBuild(args) {
9506
10041
  after = ensured.html;
9507
10042
  }
9508
10043
  if (after !== before) {
9509
- writeFileSync11(file, after);
10044
+ writeProjectFileSync(cwd, file, after, { encoding: "utf8" });
9510
10045
  marked += 1;
9511
10046
  }
9512
10047
  }