@patchstack/connect 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
4
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
5
5
  import { createRequire as createRequire2 } from "module";
6
6
 
7
7
  // src/parsers/index.ts
@@ -1210,8 +1210,8 @@ function computeManifestChecksum(packages) {
1210
1210
  var TOKEN_SKEW_MS = 6e4;
1211
1211
  function buildTokenUrl(manifestEndpoint) {
1212
1212
  const url = new URL(manifestEndpoint);
1213
- const path13 = url.pathname.replace(/\/$/, "");
1214
- url.pathname = path13.endsWith("/manifest") ? `${path13.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
1213
+ const path14 = url.pathname.replace(/\/$/, "");
1214
+ url.pathname = path14.endsWith("/manifest") ? `${path14.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
1215
1215
  url.search = "";
1216
1216
  url.hash = "";
1217
1217
  return url.toString();
@@ -1288,6 +1288,27 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
1288
1288
  return first.response;
1289
1289
  }
1290
1290
 
1291
+ // src/build-id.ts
1292
+ var BUILD_ID = /^[0-9a-f]{64}$/i;
1293
+ function canonicalBuildId(value) {
1294
+ if (typeof value !== "string") return null;
1295
+ const trimmed = value.trim();
1296
+ return BUILD_ID.test(trimmed) ? trimmed.toLowerCase() : null;
1297
+ }
1298
+ var BUILD_STAMP_KEY = "_patchstack";
1299
+ function readBuildStamp(bundle) {
1300
+ if (bundle === null || typeof bundle !== "object") return null;
1301
+ const namespace = bundle[BUILD_STAMP_KEY];
1302
+ if (namespace === null || typeof namespace !== "object") return null;
1303
+ return canonicalBuildId(namespace.build_id);
1304
+ }
1305
+ function hasRawBuildStamp(bundle) {
1306
+ if (bundle === null || typeof bundle !== "object") return false;
1307
+ const namespace = bundle[BUILD_STAMP_KEY];
1308
+ if (namespace === null || typeof namespace !== "object") return false;
1309
+ return "build_id" in namespace;
1310
+ }
1311
+
1291
1312
  // src/client.ts
1292
1313
  var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
1293
1314
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -1295,16 +1316,16 @@ var CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
1295
1316
  function claimTokenHeader(config) {
1296
1317
  return typeof config.claimToken === "string" && config.claimToken !== "" ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
1297
1318
  }
1298
- function claimOutcomeLines(claim2, config) {
1319
+ function claimOutcomeLines(claim3, config) {
1299
1320
  if (typeof config.claimToken !== "string" || config.claimToken === "") return [];
1300
- if (claim2?.state === "claimed" || claim2?.state === "owned-by-you") {
1301
- const dashboard = typeof claim2.dashboard_url === "string" && claim2.dashboard_url !== "" ? [`Dashboard: ${claim2.dashboard_url}`] : [];
1321
+ if (claim3?.state === "claimed" || claim3?.state === "owned-by-you") {
1322
+ const dashboard = typeof claim3.dashboard_url === "string" && claim3.dashboard_url !== "" ? [`Dashboard: ${claim3.dashboard_url}`] : [];
1302
1323
  return [
1303
- claim2.state === "claimed" ? "Connected to your Patchstack account." : "This site is already connected to your Patchstack account.",
1324
+ claim3.state === "claimed" ? "Connected to your Patchstack account." : "This site is already connected to your Patchstack account.",
1304
1325
  ...dashboard
1305
1326
  ];
1306
1327
  }
1307
- const why = claim2 === void 0 ? "Patchstack did not act on the claim token" : claim2.state === "owned-by-other" ? "this site belongs to a different Patchstack account" : claim2.reason === "expired" ? "the claim token has expired" : "Patchstack did not recognise the claim token";
1328
+ const why = claim3 === void 0 ? "Patchstack did not act on the claim token" : claim3.state === "owned-by-other" ? "this site belongs to a different Patchstack account" : claim3.reason === "expired" ? "the claim token has expired" : "Patchstack did not recognise the claim token";
1308
1329
  return [
1309
1330
  `Not connected to your account: ${why}.`,
1310
1331
  "Open the dashboard link below to connect it, or copy a fresh prompt from the dashboard."
@@ -1329,8 +1350,8 @@ function authFailureMessage(status, config) {
1329
1350
  }
1330
1351
  function buildRulesUrl(manifestEndpoint, siteUuid) {
1331
1352
  const url = new URL(manifestEndpoint);
1332
- const path13 = url.pathname.replace(/\/$/, "");
1333
- url.pathname = path13.endsWith("/manifest") ? `${path13.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
1353
+ const path14 = url.pathname.replace(/\/$/, "");
1354
+ url.pathname = path14.endsWith("/manifest") ? `${path14.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
1334
1355
  url.search = "";
1335
1356
  url.hash = "";
1336
1357
  return url.toString();
@@ -1345,26 +1366,27 @@ function buildSettingsUrl(endpoint, siteUuid) {
1345
1366
  }
1346
1367
  function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
1347
1368
  const url = new URL(manifestEndpoint);
1348
- const path13 = url.pathname.replace(/\/$/, "");
1349
- url.pathname = path13.endsWith("/manifest") ? `${path13.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1369
+ const path14 = url.pathname.replace(/\/$/, "");
1370
+ url.pathname = path14.endsWith("/manifest") ? `${path14.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1350
1371
  url.search = "";
1351
1372
  url.hash = "";
1352
1373
  return url.toString();
1353
1374
  }
1354
1375
  function buildInputMapUrl(manifestEndpoint, siteUuid) {
1355
1376
  const url = new URL(manifestEndpoint);
1356
- const path13 = url.pathname.replace(/\/$/, "");
1357
- url.pathname = path13.endsWith("/manifest") ? `${path13.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1377
+ const path14 = url.pathname.replace(/\/$/, "");
1378
+ url.pathname = path14.endsWith("/manifest") ? `${path14.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1358
1379
  url.search = "";
1359
1380
  url.hash = "";
1360
1381
  return url.toString();
1361
1382
  }
1362
- async function postInputMap(config, map) {
1383
+ async function postInputMap(config, map, buildId) {
1363
1384
  if (config.siteUuid === null) {
1364
1385
  return { result: "skipped", message: "No site UUID configured \u2014 run `patchstack-connect scan` first." };
1365
1386
  }
1366
1387
  const url = buildInputMapUrl(config.endpoint, config.siteUuid);
1367
1388
  try {
1389
+ const canonical = canonicalBuildId(buildId);
1368
1390
  const response = await pulseFetch(config, url, {
1369
1391
  method: "POST",
1370
1392
  headers: {
@@ -1372,7 +1394,12 @@ async function postInputMap(config, map) {
1372
1394
  "Content-Type": "application/json",
1373
1395
  "User-Agent": "@patchstack/connect"
1374
1396
  },
1375
- body: JSON.stringify(map),
1397
+ // `build_id` is additive to the v3 document: a reader that ignores it behaves exactly as before.
1398
+ // It is the digest of the policy map carrying these coordinates, so the server can serve a rule pinned
1399
+ // to one of them as enforceable only to a guard carrying that same map. Omitted entirely when this
1400
+ // run could not bind the map into a pre-bundle guard — an absent field is a question the server
1401
+ // answers by withholding enforceability, which is the safe direction.
1402
+ body: JSON.stringify(canonical === null ? map : { ...map, build_id: canonical }),
1376
1403
  signal: AbortSignal.timeout(config.timeoutMs)
1377
1404
  });
1378
1405
  if (response.status === 404) {
@@ -1690,7 +1717,7 @@ async function waitForDemoRule(manifestEndpoint, siteUuid, scenario, options = {
1690
1717
  const pollIntervalMs = options.pollIntervalMs ?? 1e3;
1691
1718
  const requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
1692
1719
  const fetchFn = options.fetchFn ?? fetch;
1693
- const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
1720
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve4) => setTimeout(resolve4, ms)));
1694
1721
  const now = options.now ?? Date.now;
1695
1722
  const started = now();
1696
1723
  const rulesUrl = buildRulesUrl(manifestEndpoint, siteUuid);
@@ -3011,6 +3038,112 @@ import { join as join10, dirname as dirname5 } from "path";
3011
3038
  import { execFileSync } from "child_process";
3012
3039
  import { existsSync as existsSync7 } from "fs";
3013
3040
  import { dirname as dirname4, join as join9, resolve } from "path";
3041
+
3042
+ // src/protect/install/node-flags.ts
3043
+ var flagName = (token) => token.split("=")[0];
3044
+ var NO_OPERAND_FLAGS = /* @__PURE__ */ new Set([
3045
+ "--disallow-code-generation-from-strings",
3046
+ "--enable-source-maps",
3047
+ "--experimental-import-meta-resolve",
3048
+ "--experimental-json-modules",
3049
+ "--experimental-vm-modules",
3050
+ "--frozen-intrinsics",
3051
+ "--no-deprecation",
3052
+ "--no-warnings",
3053
+ "--pending-deprecation",
3054
+ "--preserve-symlinks",
3055
+ "--preserve-symlinks-main",
3056
+ "--throw-deprecation",
3057
+ "--trace-deprecation",
3058
+ "--trace-exit",
3059
+ "--trace-uncaught",
3060
+ "--trace-warnings",
3061
+ "--use-strict",
3062
+ "--zero-fill-buffers"
3063
+ ]);
3064
+ var VALUED_FLAGS = /* @__PURE__ */ new Set([
3065
+ "--dns-result-order",
3066
+ "--max-http-header-size",
3067
+ "--max-old-space-size",
3068
+ "--max-semi-space-size",
3069
+ "--stack-size",
3070
+ "--title",
3071
+ "--unhandled-rejections"
3072
+ ]);
3073
+ var CODE_LOADING_FLAGS = /* @__PURE__ */ new Set([
3074
+ "--require",
3075
+ "-r",
3076
+ "--import",
3077
+ "--loader",
3078
+ "--experimental-loader",
3079
+ "--eval",
3080
+ "-e",
3081
+ "--print",
3082
+ "-p",
3083
+ "--inspect",
3084
+ "--inspect-brk",
3085
+ "--inspect-wait",
3086
+ "--inspect-port",
3087
+ "--debug",
3088
+ "--debug-brk"
3089
+ ]);
3090
+ var TAKES_NEXT_TOKEN = /* @__PURE__ */ new Set(["--require", "-r", "--import", "--loader", "--experimental-loader", "--eval", "-e", "--print", "-p", "--inspect-port"]);
3091
+ function tokenizeNodeOptions(value) {
3092
+ const tokens = [];
3093
+ let current = "";
3094
+ let quoted = false;
3095
+ let started = false;
3096
+ for (const ch of value) {
3097
+ if (ch === '"') {
3098
+ quoted = !quoted;
3099
+ started = true;
3100
+ continue;
3101
+ }
3102
+ if (!quoted && /\s/.test(ch)) {
3103
+ if (started) tokens.push(current);
3104
+ current = "";
3105
+ started = false;
3106
+ continue;
3107
+ }
3108
+ current += ch;
3109
+ started = true;
3110
+ }
3111
+ if (quoted) return null;
3112
+ if (started) tokens.push(current);
3113
+ return tokens;
3114
+ }
3115
+ function classifyNodeOptions(value) {
3116
+ const tokens = tokenizeNodeOptions(value);
3117
+ if (tokens === null) return { kind: "refused", why: "NODE_OPTIONS has an unbalanced double quote, so this cannot read it the way Node will" };
3118
+ for (const token of tokens) {
3119
+ const name = flagName(token);
3120
+ if (CODE_LOADING_FLAGS.has(name)) {
3121
+ return { kind: "refused", why: `NODE_OPTIONS carries ${name}, which runs code or opens a port before the listener reporter is in place` };
3122
+ }
3123
+ if (NO_OPERAND_FLAGS.has(token)) continue;
3124
+ if (token.includes("=") && VALUED_FLAGS.has(name)) continue;
3125
+ return { kind: "refused", why: `NODE_OPTIONS carries ${name}, which this does not recognise well enough to say what it does` };
3126
+ }
3127
+ return { kind: "safe" };
3128
+ }
3129
+ function withoutCodeLoading(value) {
3130
+ if (value === void 0 || value.trim() === "") return "";
3131
+ const tokens = tokenizeNodeOptions(value);
3132
+ if (tokens === null) return "";
3133
+ const kept = [];
3134
+ for (let i = 0; i < tokens.length; i++) {
3135
+ const token = tokens[i];
3136
+ const name = flagName(token);
3137
+ if (CODE_LOADING_FLAGS.has(name)) {
3138
+ if (!token.includes("=") && TAKES_NEXT_TOKEN.has(name)) i++;
3139
+ continue;
3140
+ }
3141
+ if (NO_OPERAND_FLAGS.has(token) || token.includes("=") && VALUED_FLAGS.has(name)) kept.push(token);
3142
+ }
3143
+ return kept.map((token) => /\s/.test(token) ? `"${token}"` : token).join(" ");
3144
+ }
3145
+
3146
+ // src/protect/install/source-scope.ts
3014
3147
  var IMPORT_LINE = /^\s*(?:import\b|export\s+(?:\*|\{)|(?:const|let|var)\s+[^=]+=\s*require\()/;
3015
3148
  function lastTopLevelImportLine(source) {
3016
3149
  const lines = source.split("\n");
@@ -3089,7 +3222,10 @@ function advanceDepth(line, inBlockComment, add) {
3089
3222
  function parses(filePath) {
3090
3223
  if (!/\.(?:js|cjs|mjs)$/.test(filePath)) return null;
3091
3224
  try {
3092
- execFileSync(process.execPath, ["--check", filePath], { stdio: "pipe" });
3225
+ execFileSync(process.execPath, ["--check", filePath], {
3226
+ stdio: "pipe",
3227
+ env: { ...process.env, NODE_OPTIONS: withoutCodeLoading(process.env.NODE_OPTIONS) }
3228
+ });
3093
3229
  return true;
3094
3230
  } catch (error) {
3095
3231
  return isSyntaxError(error) ? false : null;
@@ -4183,9 +4319,9 @@ async function collectGuideState(cwd) {
4183
4319
  endpointOverride,
4184
4320
  hasBuildScript: Boolean(pkg?.scripts?.build?.trim()),
4185
4321
  installScanWired: (pkg?.scripts?.postinstall ?? "").includes("patchstack-connect scan"),
4186
- // bun run doesn't execute npm-style pre/post scripts, so chaining inside
4187
- // the build script itself also counts as wired (and is what we suggest on bun).
4188
- prebuildWired: (pkg?.scripts?.prebuild ?? "").includes("patchstack-connect scan") || (pkg?.scripts?.build ?? "").includes("patchstack-connect scan"),
4322
+ // The scan has to run first: a later prebuild command may upload and stamp the map that the bundle
4323
+ // must retain. Bun skips npm-style pre/post scripts, so its build chain follows the same order.
4324
+ prebuildWired: /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.prebuild ?? "") || /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.build ?? ""),
4189
4325
  postbuildWired: (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build") || (pkg?.scripts?.build ?? "").includes("patchstack-connect mark-build"),
4190
4326
  widgetInstalled: widget.found,
4191
4327
  widgetTokenMatches: widget.uuidMatches,
@@ -4358,12 +4494,15 @@ function renderGuideChecklist(state, useColor) {
4358
4494
  lines.push(detail(`${check.label}${check.hint ? ` \u2014 ${check.hint}` : ""}`));
4359
4495
  }
4360
4496
  lines.push(detail("Verify \u2192 npx @patchstack/connect protect --check"));
4497
+ lines.push(detail("Prove a request reaches the guard (starts your app) \u2192 npx @patchstack/connect protect --check --runtime"));
4361
4498
  }
4362
4499
  lines.push("");
4363
4500
  if (state.claimUrl !== null) {
4364
4501
  lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Dashboard link (open to view reports):")}`);
4365
4502
  lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
4366
4503
  lines.push(detail("Open this link in a browser. The CLI never opens it."));
4504
+ lines.push(detail("Or from this terminal \u2192 npx @patchstack/connect claim"));
4505
+ lines.push(detail(" (prints a link to sign in with, then attaches the site to that account)"));
4367
4506
  if (state.endpointOverride !== null) {
4368
4507
  lines.push(detail("(this URL inherits the endpoint override above)"));
4369
4508
  }
@@ -4406,7 +4545,12 @@ function renderGuideChecklist(state, useColor) {
4406
4545
  }
4407
4546
 
4408
4547
  // src/login.ts
4409
- import { mkdtempSync, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync7 } from "fs";
4548
+ import { mkdtempSync } from "fs";
4549
+ import { tmpdir as tmpdir2 } from "os";
4550
+ import path12 from "path";
4551
+
4552
+ // src/device-flow.ts
4553
+ import { readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync7 } from "fs";
4410
4554
  import { tmpdir } from "os";
4411
4555
  import path11 from "path";
4412
4556
  import { createHash as createHash2 } from "crypto";
@@ -4418,108 +4562,126 @@ function baseFrom(manifestEndpoint) {
4418
4562
  url.hash = "";
4419
4563
  return url.toString().replace(/\/$/, "");
4420
4564
  }
4421
- function pendingPath(siteUuid) {
4565
+ function pendingPath(siteUuid, intent) {
4422
4566
  const key = createHash2("sha256").update(siteUuid).digest("hex").slice(0, 16);
4423
- return path11.join(tmpdir(), `patchstack-login-${key}.json`);
4567
+ return path11.join(tmpdir(), `patchstack-${intent}-${key}.json`);
4424
4568
  }
4425
- function savePendingLogin(siteUuid, pending) {
4426
- writeFileSync7(pendingPath(siteUuid), JSON.stringify(pending), { encoding: "utf8", mode: 384 });
4569
+ function savePending(siteUuid, intent, pending) {
4570
+ writeFileSync7(pendingPath(siteUuid, intent), JSON.stringify(pending), {
4571
+ encoding: "utf8",
4572
+ mode: 384
4573
+ });
4427
4574
  }
4428
- function readPendingLogin(siteUuid) {
4575
+ function readPending(siteUuid, intent) {
4429
4576
  try {
4430
- return JSON.parse(readFileSync7(pendingPath(siteUuid), "utf8"));
4577
+ return JSON.parse(readFileSync7(pendingPath(siteUuid, intent), "utf8"));
4431
4578
  } catch {
4432
4579
  return null;
4433
4580
  }
4434
4581
  }
4435
- function clearPendingLogin(siteUuid) {
4582
+ function clearPending(siteUuid, intent) {
4436
4583
  try {
4437
- rmSync(pendingPath(siteUuid));
4584
+ rmSync(pendingPath(siteUuid, intent));
4438
4585
  } catch {
4439
4586
  }
4440
4587
  }
4441
- async function startLogin(config, deps = {}) {
4588
+ async function requestDeviceCode(config, intent, conflictMessage, deps = {}) {
4442
4589
  const fetchImpl = deps.fetchImpl ?? fetch;
4443
4590
  const now = deps.now ?? (() => Date.now());
4444
4591
  if (config.siteUuid === null) {
4445
- return { status: "failed", message: "No site UUID configured \u2014 run `patchstack-connect scan` first." };
4446
- }
4447
- const base = baseFrom(config.endpoint);
4448
- const started = await fetchImpl(`${base}/device/code`, {
4449
- method: "POST",
4450
- headers: { "Content-Type": "application/json", Accept: "application/json" },
4451
- body: JSON.stringify({ site_uuid: config.siteUuid })
4452
- });
4453
- if (started.status === 409) {
4454
4592
  return {
4455
- status: "unclaimed",
4456
- message: "This site has not been claimed yet, so there is no owner to approve the request. Claim it in the dashboard, or delete .patchstackrc.json to provision a new site."
4593
+ status: "failed",
4594
+ message: "No site UUID configured \u2014 run `patchstack-connect scan` first."
4457
4595
  };
4458
4596
  }
4597
+ const base = baseFrom(config.endpoint);
4598
+ const started = await pulseFetch(
4599
+ config,
4600
+ `${base}/device/code`,
4601
+ {
4602
+ method: "POST",
4603
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
4604
+ body: JSON.stringify({ site_uuid: config.siteUuid, intent })
4605
+ },
4606
+ fetchImpl
4607
+ );
4608
+ if (started.status === 409) return { status: "conflict", message: conflictMessage };
4459
4609
  if (started.status === 404) {
4460
4610
  return { status: "not-found", message: "Patchstack does not recognise this site UUID." };
4461
4611
  }
4462
4612
  if (!started.ok) {
4463
- return { status: "failed", message: `Could not start the login (HTTP ${started.status}).` };
4613
+ return { status: "failed", message: `Could not start the request (HTTP ${started.status}).` };
4464
4614
  }
4465
4615
  const body = await started.json();
4466
4616
  const pending = {
4467
4617
  deviceCode: body.device_code,
4468
4618
  userCode: body.user_code,
4469
- // Points at the API, which redirects to the dashboard SPA — the CLI only
4470
- // knows the API origin, and the approval page lives on the app.
4619
+ // Points at the API, which redirects to the dashboard SPA — the CLI only knows the API origin,
4620
+ // and the approval page lives on the app.
4471
4621
  verificationUri: `${base}/device?code=${encodeURIComponent(body.user_code)}`,
4472
4622
  expiresAt: now() + body.expires_in * 1e3,
4473
4623
  intervalMs: Math.max(1, body.interval) * 1e3
4474
4624
  };
4475
- savePendingLogin(config.siteUuid, pending);
4625
+ savePending(config.siteUuid, intent, pending);
4476
4626
  return { status: "started", pending };
4477
4627
  }
4478
- async function redeemIfApproved(config, pending, deps = {}) {
4628
+ async function pollDeviceToken(config, intent, pending, deps = {}) {
4479
4629
  const fetchImpl = deps.fetchImpl ?? fetch;
4480
4630
  const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
4481
4631
  method: "POST",
4482
4632
  headers: { "Content-Type": "application/json", Accept: "application/json" },
4483
4633
  body: JSON.stringify({ device_code: pending.deviceCode })
4484
4634
  });
4485
- if (polled.status === 428) return "pending";
4635
+ if (polled.status === 428) return { state: "pending" };
4486
4636
  if (!polled.ok) {
4487
- if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
4488
- return "expired";
4637
+ if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
4638
+ return { state: "expired" };
4489
4639
  }
4490
- const { api_key: apiKey } = await polled.json();
4491
- if (typeof apiKey !== "string" || apiKey.length === 0) return "expired";
4492
- await persistApiKey(process.cwd(), apiKey);
4493
- if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
4494
- return "approved";
4640
+ const body = await polled.json();
4641
+ if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
4642
+ return { state: "approved", body };
4495
4643
  }
4496
- async function waitForApproval(config, pending, deps = {}) {
4497
- const fetchImpl = deps.fetchImpl ?? fetch;
4644
+ async function pollUntilApproved(config, intent, pending, deps = {}) {
4498
4645
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4499
4646
  const now = deps.now ?? (() => Date.now());
4500
4647
  const deadline = Math.min(pending.expiresAt, deps.until ?? pending.expiresAt);
4501
- const expired = { status: "expired", message: "The login request expired. Run the command again." };
4502
4648
  while (now() < deadline) {
4503
4649
  await sleep(pending.intervalMs);
4504
- const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
4505
- method: "POST",
4506
- headers: { "Content-Type": "application/json", Accept: "application/json" },
4507
- body: JSON.stringify({ device_code: pending.deviceCode })
4508
- });
4509
- if (polled.status === 428) continue;
4510
- if (!polled.ok) {
4511
- if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
4512
- return expired;
4513
- }
4514
- const { api_key: apiKey } = await polled.json();
4515
- if (typeof apiKey !== "string" || apiKey.length === 0) {
4516
- return { status: "failed", message: "Patchstack approved the request but returned no credential." };
4517
- }
4518
- await persistApiKey(process.cwd(), apiKey);
4519
- if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
4520
- return { status: "approved", userCode: pending.userCode, verificationUri: pending.verificationUri };
4650
+ const outcome = await pollDeviceToken(config, intent, pending, deps);
4651
+ if (outcome.state !== "pending") return outcome;
4521
4652
  }
4522
- return expired;
4653
+ return { state: "expired" };
4654
+ }
4655
+
4656
+ // src/login.ts
4657
+ var UNCLAIMED = "This site has not been claimed yet, so there is no owner to approve the request. Claim it with `patchstack-connect claim`, or delete .patchstackrc.json to provision a new site.";
4658
+ function readPendingLogin(siteUuid) {
4659
+ return readPending(siteUuid, "login");
4660
+ }
4661
+ async function startLogin(config, deps = {}) {
4662
+ const started = await requestDeviceCode(config, "login", UNCLAIMED, deps);
4663
+ if (started.status === "conflict") return { status: "unclaimed", message: started.message };
4664
+ return started;
4665
+ }
4666
+ async function redeemIfApproved(config, pending, deps = {}) {
4667
+ const polled = await pollDeviceToken(config, "login", pending, deps);
4668
+ if (polled.state !== "approved") return polled.state;
4669
+ const apiKey = polled.body?.api_key;
4670
+ if (typeof apiKey !== "string" || apiKey.length === 0) return "expired";
4671
+ await persistApiKey(process.cwd(), apiKey);
4672
+ return "approved";
4673
+ }
4674
+ async function waitForApproval(config, pending, deps = {}) {
4675
+ const polled = await pollUntilApproved(config, "login", pending, deps);
4676
+ if (polled.state === "expired") {
4677
+ return { status: "expired", message: "The login request expired. Run the command again." };
4678
+ }
4679
+ const apiKey = polled.body?.api_key;
4680
+ if (typeof apiKey !== "string" || apiKey.length === 0) {
4681
+ return { status: "failed", message: "Patchstack approved the request but returned no credential." };
4682
+ }
4683
+ await persistApiKey(process.cwd(), apiKey);
4684
+ return { status: "approved", userCode: pending.userCode, verificationUri: pending.verificationUri };
4523
4685
  }
4524
4686
  async function login(config, onPrompt, deps = {}) {
4525
4687
  const started = await startLogin(config, deps);
@@ -4530,16 +4692,546 @@ async function login(config, onPrompt, deps = {}) {
4530
4692
  return waitForApproval(config, started.pending, deps);
4531
4693
  }
4532
4694
 
4695
+ // src/claim.ts
4696
+ var ALREADY_CLAIMED = "This site is already attached to a Patchstack account. Nothing to claim \u2014 sign in at the dashboard to see it, or use `patchstack-connect login` if you need its credential back.";
4697
+ function readPendingClaim(siteUuid) {
4698
+ return readPending(siteUuid, "claim");
4699
+ }
4700
+ async function startClaim(config, deps = {}) {
4701
+ const started = await requestDeviceCode(config, "claim", ALREADY_CLAIMED, deps);
4702
+ if (started.status === "conflict") return { status: "already-claimed", message: started.message };
4703
+ return started;
4704
+ }
4705
+ async function settle(body) {
4706
+ const account = typeof body?.account === "string" ? body.account : void 0;
4707
+ const dashboardUrl = typeof body?.dashboard_url === "string" ? body.dashboard_url : void 0;
4708
+ const apiKey = body?.api_key;
4709
+ let credentialSaved = false;
4710
+ if (typeof apiKey === "string" && apiKey.length > 0) {
4711
+ await persistApiKey(process.cwd(), apiKey);
4712
+ credentialSaved = true;
4713
+ }
4714
+ return { status: "claimed", account, dashboardUrl, credentialSaved };
4715
+ }
4716
+ async function redeemClaimIfApproved(config, pending, deps = {}) {
4717
+ const polled = await pollDeviceToken(config, "claim", pending, deps);
4718
+ if (polled.state === "pending") return "pending";
4719
+ if (polled.state === "expired") {
4720
+ return { status: "expired", message: "The claim request expired. Run the command again." };
4721
+ }
4722
+ return settle(polled.body);
4723
+ }
4724
+ async function waitForClaim(config, pending, deps = {}) {
4725
+ const polled = await pollUntilApproved(config, "claim", pending, deps);
4726
+ if (polled.state === "expired") {
4727
+ return { status: "expired", message: "The claim request expired. Run the command again." };
4728
+ }
4729
+ return settle(polled.body);
4730
+ }
4731
+ async function claim2(config, onPrompt, deps = {}) {
4732
+ const started = await startClaim(config, deps);
4733
+ if (started.status !== "started" || started.pending === void 0) {
4734
+ return {
4735
+ status: started.status === "started" ? "failed" : started.status,
4736
+ message: started.message
4737
+ };
4738
+ }
4739
+ onPrompt(started.pending.userCode, started.pending.verificationUri);
4740
+ return waitForClaim(config, started.pending, deps);
4741
+ }
4742
+
4743
+ // src/protect/install/runtime/check.ts
4744
+ import { relative as relative2 } from "path";
4745
+
4746
+ // src/protect/install/runtime/entry.ts
4747
+ import { existsSync as existsSync13, readFileSync as readFileSync8, realpathSync, statSync as statSync3 } from "fs";
4748
+ import { join as join13, resolve as resolve3, sep } from "path";
4749
+ var CONVENTIONAL = [
4750
+ "server.js",
4751
+ "server.mjs",
4752
+ "server.cjs",
4753
+ "app.js",
4754
+ "app.mjs",
4755
+ "app.cjs",
4756
+ "index.js",
4757
+ "index.mjs",
4758
+ "index.cjs",
4759
+ "src/server.js",
4760
+ "src/server.mjs",
4761
+ "src/server.cjs",
4762
+ "src/index.js",
4763
+ "src/index.mjs",
4764
+ "src/index.cjs",
4765
+ "dist/server.js",
4766
+ "dist/index.js",
4767
+ "dist/main.js",
4768
+ "build/index.js",
4769
+ "build/server.js"
4770
+ ];
4771
+ var LOADABLE = /\.(?:js|mjs|cjs)$/;
4772
+ function readPackage2(cwd) {
4773
+ try {
4774
+ const parsed = JSON.parse(readFileSync8(join13(cwd, "package.json"), "utf8"));
4775
+ return parsed !== null && typeof parsed === "object" ? parsed : null;
4776
+ } catch {
4777
+ return null;
4778
+ }
4779
+ }
4780
+ function isFile(path14) {
4781
+ try {
4782
+ return statSync3(path14).isFile();
4783
+ } catch {
4784
+ return false;
4785
+ }
4786
+ }
4787
+ function loadable(cwd, candidate) {
4788
+ const written = resolve3(cwd, candidate);
4789
+ if (!LOADABLE.test(written) || !isFile(written)) return null;
4790
+ let root;
4791
+ let path14;
4792
+ try {
4793
+ root = realpathSync(cwd);
4794
+ path14 = realpathSync(written);
4795
+ } catch {
4796
+ return null;
4797
+ }
4798
+ if (path14 !== root && !path14.startsWith(root + sep)) return null;
4799
+ return written;
4800
+ }
4801
+ function passable(token) {
4802
+ if (CODE_LOADING_FLAGS.has(flagName(token))) return false;
4803
+ if (NO_OPERAND_FLAGS.has(token)) return true;
4804
+ return token.includes("=") && VALUED_FLAGS.has(flagName(token));
4805
+ }
4806
+ function fromScript(cwd, script) {
4807
+ if (/[&|;><()#$`'"*?\[\]{}\\~\r\n]/.test(script)) return null;
4808
+ const parts = script.trim().split(/\s+/);
4809
+ if (parts[0] !== "node") return null;
4810
+ const nodeArgs = [];
4811
+ let rest = parts.slice(1);
4812
+ while (rest.length > 0 && rest[0].startsWith("-")) {
4813
+ const token = rest[0];
4814
+ if (!passable(token)) return null;
4815
+ nodeArgs.push(token);
4816
+ rest = rest.slice(1);
4817
+ }
4818
+ if (rest.length === 0) return null;
4819
+ const file = loadable(cwd, rest[0]);
4820
+ return file === null ? null : { file, nodeArgs, appArgs: rest.slice(1) };
4821
+ }
4822
+ function resolveEntry(cwd) {
4823
+ const pkg = readPackage2(cwd);
4824
+ const scripts = pkg?.scripts ?? null;
4825
+ const declared = ["start", "serve"].map((name) => ({ name, script: scripts?.[name] })).find((entry) => typeof entry.script === "string" && entry.script.trim() !== "");
4826
+ if (declared !== void 0) {
4827
+ const resolved = fromScript(cwd, declared.script);
4828
+ return resolved === null ? {
4829
+ kind: "unavailable",
4830
+ reason: `the "${declared.name}" script is outside the bounded \`node <file>\` form this check can launch without the project's own toolchain`
4831
+ } : { kind: "entry", ...resolved, from: `the "${declared.name}" script` };
4832
+ }
4833
+ const main2 = pkg?.main;
4834
+ if (typeof main2 === "string") {
4835
+ const file = loadable(cwd, main2);
4836
+ if (file !== null) return { kind: "entry", file, nodeArgs: [], appArgs: [], from: 'the package "main" field' };
4837
+ }
4838
+ for (const candidate of CONVENTIONAL) {
4839
+ const file = loadable(cwd, candidate);
4840
+ if (file !== null) return { kind: "entry", file, nodeArgs: [], appArgs: [], from: candidate };
4841
+ }
4842
+ return {
4843
+ kind: "unavailable",
4844
+ reason: existsSync13(join13(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"
4845
+ };
4846
+ }
4847
+
4848
+ // src/protect/install/runtime/probe.ts
4849
+ import { spawn } from "child_process";
4850
+ import { createHash as createHash3, randomBytes } from "crypto";
4851
+ import { existsSync as existsSync14 } from "fs";
4852
+ import { request as httpRequest } from "http";
4853
+ import { request as httpsRequest, Agent as HttpsAgent } from "https";
4854
+ import { fileURLToPath as fileURLToPath2 } from "url";
4855
+ var CHALLENGE_BYTES = 32;
4856
+ var VERIFY_HEADER_NAME = "x-patchstack-verify";
4857
+ var CHALLENGE_ENV = "PATCHSTACK_VERIFY_CHALLENGE";
4858
+ var SETTLE_MS = 1500;
4859
+ var RUN_MS = 2e4;
4860
+ var PROBE_MS = 5e3;
4861
+ var FREEZE_MS = 500;
4862
+ var OUTPUT_LIMIT = 4096;
4863
+ var REDACTION_OVERLAP = 256;
4864
+ var ANSWER_LIMIT = 8192;
4865
+ var LOOPBACK = ["127.0.0.1", "::1"];
4866
+ var ENDING_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
4867
+ function expectedAnswerFor(challenge) {
4868
+ return createHash3("sha256").update(`patchstack-verify:${challenge}`).digest("hex");
4869
+ }
4870
+ function preloadPath() {
4871
+ const candidates = [
4872
+ new URL("./protect/runtime/report-listeners.cjs", import.meta.url),
4873
+ // beside the built CLI
4874
+ new URL("./report-listeners.cjs", import.meta.url)
4875
+ // beside this file, in the repository
4876
+ ];
4877
+ for (const candidate of candidates) {
4878
+ const path14 = fileURLToPath2(candidate);
4879
+ if (existsSync14(path14)) return path14;
4880
+ }
4881
+ return null;
4882
+ }
4883
+ function propagatedNodeOptions(preload, existing) {
4884
+ if (preload.includes('"')) {
4885
+ return {
4886
+ kind: "unavailable",
4887
+ reason: "the path to the listener reporter contains a double quote, which NODE_OPTIONS cannot carry \u2014 so a worker thread could not inherit the listener containment"
4888
+ };
4889
+ }
4890
+ const ours = `--require "${preload}"`;
4891
+ if (existing === void 0 || existing.trim() === "") return { kind: "options", value: ours };
4892
+ const verdict = classifyNodeOptions(existing);
4893
+ return verdict.kind === "refused" ? { kind: "unavailable", reason: verdict.why } : { kind: "options", value: `${ours} ${existing}` };
4894
+ }
4895
+ function keptOutput(chunks, challenge, answer) {
4896
+ const text = chunks.join("").split(challenge).join("<challenge>").split(answer).join("<answer>");
4897
+ return text.slice(0, OUTPUT_LIMIT);
4898
+ }
4899
+ async function askListener(listener, challenge, answer, budgetMs) {
4900
+ if (budgetMs <= 0) {
4901
+ return { ...listener, outcome: "unreachable", detail: "the run ran out of time before it could be asked" };
4902
+ }
4903
+ const send = listener.scheme === "https" ? httpsRequest : httpRequest;
4904
+ const options = {
4905
+ host: listener.host,
4906
+ port: listener.port,
4907
+ path: "/",
4908
+ method: "GET",
4909
+ headers: { [VERIFY_HEADER_NAME]: challenge },
4910
+ timeout: budgetMs
4911
+ };
4912
+ if (listener.scheme === "https") options.agent = new HttpsAgent({ rejectUnauthorized: false });
4913
+ return new Promise((resolve4) => {
4914
+ const req = send(options, (res) => {
4915
+ const body = [];
4916
+ let read2 = 0;
4917
+ res.on("data", (chunk) => {
4918
+ read2 += chunk.length;
4919
+ if (read2 <= ANSWER_LIMIT) {
4920
+ body.push(chunk);
4921
+ return;
4922
+ }
4923
+ res.destroy();
4924
+ });
4925
+ const finish = () => {
4926
+ const text = Buffer.concat(body).toString("utf8").trim();
4927
+ resolve4({
4928
+ ...listener,
4929
+ outcome: text === answer ? "traversed" : "answered-without-sentinel",
4930
+ detail: text === answer ? void 0 : `answered with ${res.statusCode ?? "?"} and no sentinel`
4931
+ });
4932
+ };
4933
+ res.on("end", finish);
4934
+ res.on("close", finish);
4935
+ });
4936
+ req.on("timeout", () => req.destroy(new Error("timed out")));
4937
+ req.on("error", (err) => resolve4({ ...listener, outcome: "unreachable", detail: err.message }));
4938
+ req.end();
4939
+ });
4940
+ }
4941
+ function verdictOf(listeners) {
4942
+ if (listeners.length === 0 || listeners.some((l) => l.outcome === "unreachable")) return "unavailable";
4943
+ if (listeners.some((l) => l.outcome === "answered-without-sentinel")) return "not-traversed";
4944
+ return "proven";
4945
+ }
4946
+ function terminator(child) {
4947
+ let done = false;
4948
+ return () => {
4949
+ if (done) return;
4950
+ done = true;
4951
+ try {
4952
+ if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
4953
+ } catch {
4954
+ try {
4955
+ child.kill("SIGKILL");
4956
+ } catch {
4957
+ }
4958
+ }
4959
+ };
4960
+ }
4961
+ async function probeRuntimeTraversal(options) {
4962
+ const platform = options.platform ?? process.platform;
4963
+ const challenge = randomBytes(CHALLENGE_BYTES).toString("hex");
4964
+ const answer = expectedAnswerFor(challenge);
4965
+ const nothing = (reason) => ({ outcome: "unavailable", reason, listeners: [], output: "" });
4966
+ if (platform === "win32") return nothing("process-tree cleanup is not implemented on Windows");
4967
+ const preload = options.preload ?? preloadPath();
4968
+ if (preload === null) return nothing("the listener reporter is missing from this installation");
4969
+ const deadline = Date.now() + (options.timeoutMs ?? RUN_MS);
4970
+ const remaining = () => Math.max(0, deadline - Date.now());
4971
+ const propagation = propagatedNodeOptions(preload, process.env.NODE_OPTIONS);
4972
+ if (propagation.kind === "unavailable") return nothing(propagation.reason);
4973
+ let detachMessages = () => {
4974
+ };
4975
+ const child = spawn(process.execPath, [...options.nodeArgs ?? [], "--require", preload, options.entry, ...options.appArgs ?? []], {
4976
+ cwd: options.cwd,
4977
+ detached: true,
4978
+ // its own process group, so cleanup has one stable target
4979
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
4980
+ // The app's own environment, plus the challenge and the propagated reporter. `NODE_ENV` is
4981
+ // deliberately left alone: choosing one here would change which code path the app takes, and the
4982
+ // point is to verify the app as it is.
4983
+ env: {
4984
+ ...process.env,
4985
+ [CHALLENGE_ENV]: challenge,
4986
+ // Replaced rather than added to: what may be inherited was decided above, and the reporter is
4987
+ // first in the value that goes out.
4988
+ NODE_OPTIONS: propagation.value
4989
+ }
4990
+ });
4991
+ const terminate = terminator(child);
4992
+ const onSignal = (signal) => {
4993
+ terminate();
4994
+ detachHandlers();
4995
+ if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
4996
+ };
4997
+ const onExit = () => terminate();
4998
+ const detachHandlers = () => {
4999
+ for (const signal of ENDING_SIGNALS) process.removeListener(signal, onSignal);
5000
+ process.removeListener("exit", onExit);
5001
+ };
5002
+ for (const signal of ENDING_SIGNALS) process.prependListener(signal, onSignal);
5003
+ process.on("exit", onExit);
5004
+ try {
5005
+ const chunks = [];
5006
+ let kept = 0;
5007
+ const drain = (stream) => {
5008
+ stream?.setEncoding("utf8");
5009
+ stream?.on("data", (chunk) => {
5010
+ if (kept < OUTPUT_LIMIT + REDACTION_OVERLAP) {
5011
+ chunks.push(chunk);
5012
+ kept += chunk.length;
5013
+ }
5014
+ });
5015
+ };
5016
+ drain(child.stdout);
5017
+ drain(child.stderr);
5018
+ const observed = [];
5019
+ const unsupported = [];
5020
+ const created = [];
5021
+ const workers = [];
5022
+ const late = [];
5023
+ const escaped = [];
5024
+ let loaded = false;
5025
+ let startFailure = null;
5026
+ let exitHow = null;
5027
+ let discovering = true;
5028
+ let onFrozen = null;
5029
+ const freeze = async () => {
5030
+ if (!child.connected) return false;
5031
+ return new Promise((resolve4) => {
5032
+ const settle2 = (confirmed2) => {
5033
+ clearTimeout(timer);
5034
+ onFrozen = null;
5035
+ child.removeListener("exit", onGone);
5036
+ resolve4(confirmed2);
5037
+ };
5038
+ const onGone = () => settle2(false);
5039
+ const timer = setTimeout(() => settle2(false), Math.min(FREEZE_MS, remaining()));
5040
+ onFrozen = () => settle2(true);
5041
+ child.once("exit", onGone);
5042
+ try {
5043
+ child.send({ patchstackVerify: "freeze" });
5044
+ } catch {
5045
+ settle2(false);
5046
+ }
5047
+ });
5048
+ };
5049
+ await new Promise((resolve4) => {
5050
+ let settleTimer = null;
5051
+ const done = () => {
5052
+ discovering = false;
5053
+ clearTimeout(runTimer);
5054
+ if (settleTimer) clearTimeout(settleTimer);
5055
+ resolve4();
5056
+ };
5057
+ const runTimer = setTimeout(done, remaining());
5058
+ const settle2 = () => {
5059
+ if (settleTimer) clearTimeout(settleTimer);
5060
+ settleTimer = setTimeout(done, Math.min(options.settleMs ?? SETTLE_MS, remaining()));
5061
+ };
5062
+ const onMessage = (message) => {
5063
+ const note = message;
5064
+ if (note?.patchstackVerify === "frozen") {
5065
+ onFrozen?.();
5066
+ return;
5067
+ }
5068
+ if (note?.patchstackVerify === "process-created" && typeof note.escape === "string") escaped.push(`${note.what ?? "a process"} ${note.escape}`);
5069
+ if (note?.patchstackVerify === "worker-created" && typeof note.escape === "string") escaped.push(`a worker thread that ${note.escape}`);
5070
+ if (!discovering) {
5071
+ const kind = note?.patchstackVerify;
5072
+ if (kind === "listener") late.push(`an ${note.scheme === "https" ? "HTTPS" : "HTTP"} listener on port ${Number(note.port)}`);
5073
+ else if (kind === "unsupported-listener") late.push(`a listener this cannot probe (${note.why ?? "an unsupported transport"})`);
5074
+ else if (kind === "process-created") late.push(`another process (${note.what ?? "a process"} via ${note.via ?? "child_process"})`);
5075
+ else if (kind === "worker-created") late.push(`a worker thread (${note.what ?? "a worker"})`);
5076
+ return;
5077
+ }
5078
+ if (note?.patchstackVerify === "listener" && (note.scheme === "http" || note.scheme === "https")) {
5079
+ observed.push({ scheme: note.scheme, host: String(note.host), port: Number(note.port) });
5080
+ settle2();
5081
+ }
5082
+ if (note?.patchstackVerify === "unsupported-listener") {
5083
+ unsupported.push(note.why ?? "an unsupported transport");
5084
+ done();
5085
+ }
5086
+ if (note?.patchstackVerify === "process-created") {
5087
+ created.push(
5088
+ `${note.what ?? "a process"} (via ${note.via ?? "child_process"})${note.node === true ? "" : ", which the listener reporter cannot reach"}`
5089
+ );
5090
+ done();
5091
+ }
5092
+ if (note?.patchstackVerify === "worker-created") {
5093
+ workers.push(note.what ?? "a worker");
5094
+ done();
5095
+ }
5096
+ if (note?.patchstackVerify === "ready") loaded = true;
5097
+ };
5098
+ child.on("message", onMessage);
5099
+ detachMessages = () => child.removeListener("message", onMessage);
5100
+ child.on("error", (err) => {
5101
+ startFailure = `the app could not be started (${err.message})`;
5102
+ done();
5103
+ });
5104
+ child.on("exit", (code, signal) => {
5105
+ exitHow = signal ?? `code ${code}`;
5106
+ done();
5107
+ });
5108
+ });
5109
+ const offLoopback = observed.filter((l) => !LOOPBACK.includes(l.host));
5110
+ const contained = unsupported.length === 0 && created.length === 0 && workers.length === 0 && offLoopback.length === 0;
5111
+ const listeners = contained ? await Promise.all(observed.map((l) => askListener(l, challenge, answer, Math.min(PROBE_MS, remaining())))) : [];
5112
+ const confirmed = await freeze();
5113
+ const output = keptOutput(chunks, challenge, answer);
5114
+ const pid = child.pid;
5115
+ const exitNote = startFailure ?? (exitHow === null ? null : observed.length === 0 ? `the app exited before it listened (${exitHow})` : `the app exited while it was being probed (${exitHow})`);
5116
+ if (escaped.length > 0) {
5117
+ return {
5118
+ outcome: "unavailable",
5119
+ reason: `the app tried to start something outside this run's one-process scope, and it was refused: ${escaped.join("; ")}`,
5120
+ listeners: [],
5121
+ output,
5122
+ pid
5123
+ };
5124
+ }
5125
+ if (unsupported.length > 0) {
5126
+ return { outcome: "unavailable", reason: `a listener this cannot probe: ${unsupported.join("; ")}`, listeners: [], output, pid };
5127
+ }
5128
+ if (created.length > 0) {
5129
+ return {
5130
+ outcome: "unavailable",
5131
+ reason: `the app attempted to start another process, so this run cannot answer for the whole app: ${created.join("; ")}`,
5132
+ listeners: [],
5133
+ output,
5134
+ pid
5135
+ };
5136
+ }
5137
+ if (workers.length > 0) {
5138
+ return {
5139
+ outcome: "unavailable",
5140
+ reason: `the app started a worker thread, whose listeners cannot report back to this run: ${workers.join("; ")}`,
5141
+ listeners: [],
5142
+ output,
5143
+ pid
5144
+ };
5145
+ }
5146
+ if (offLoopback.length > 0) {
5147
+ return {
5148
+ outcome: "unavailable",
5149
+ reason: `a listener bound an address this verification must not open: ${offLoopback.map((l) => l.host).join("; ")}`,
5150
+ listeners: [],
5151
+ output,
5152
+ pid
5153
+ };
5154
+ }
5155
+ if (late.length > 0) {
5156
+ return {
5157
+ outcome: "unavailable",
5158
+ reason: `the app was still opening things after discovery had ended, so this run cannot answer for the whole app: ${late.join("; ")}`,
5159
+ listeners: [],
5160
+ output,
5161
+ pid
5162
+ };
5163
+ }
5164
+ if (observed.length === 0) {
5165
+ if (exitNote !== null) return { outcome: "unavailable", reason: exitNote, listeners: [], output, pid };
5166
+ if (!loaded) {
5167
+ return { outcome: "unavailable", reason: "the listener reporter did not load in the app process", listeners: [], output, pid };
5168
+ }
5169
+ return { outcome: "unavailable", reason: "no HTTP listener was observed while starting up", listeners: [], output, pid };
5170
+ }
5171
+ const unconfirmed = !confirmed && loaded && exitHow === null;
5172
+ const outcome = unconfirmed && verdictOf(listeners) === "proven" ? "unavailable" : verdictOf(listeners);
5173
+ const reason = outcome === "unavailable" ? exitNote ?? (unconfirmed ? "the app never confirmed it had stopped opening listeners, so one it opened last cannot be ruled out" : listeners.find((l) => l.outcome === "unreachable")?.detail ?? "a listener could not be reached") : void 0;
5174
+ return { outcome, reason, listeners, output, pid };
5175
+ } finally {
5176
+ terminate();
5177
+ detachHandlers();
5178
+ detachMessages();
5179
+ }
5180
+ }
5181
+
5182
+ // src/protect/install/runtime/check.ts
5183
+ async function runRuntimeCheck(cwd, options = {}) {
5184
+ const entry = resolveEntry(cwd);
5185
+ if (entry.kind === "unavailable") return { outcome: "unavailable", reason: entry.reason, listeners: [], output: "" };
5186
+ const result = await probeRuntimeTraversal({
5187
+ cwd,
5188
+ entry: entry.file,
5189
+ nodeArgs: entry.nodeArgs,
5190
+ appArgs: entry.appArgs,
5191
+ timeoutMs: options.timeoutMs,
5192
+ settleMs: options.settleMs,
5193
+ platform: options.platform
5194
+ });
5195
+ return { ...result, entry: { file: relative2(cwd, entry.file).replace(/\\/g, "/"), from: entry.from } };
5196
+ }
5197
+ function runtimeExitCode(report) {
5198
+ if (report.outcome === "proven") return 0;
5199
+ return report.outcome === "not-traversed" ? 1 : 2;
5200
+ }
5201
+ function formatRuntimeCheck(report) {
5202
+ const lines = ["runtime traversal (--runtime):"];
5203
+ if (report.entry) lines.push(` entry: ${report.entry.file} (from ${report.entry.from})`);
5204
+ for (const listener of report.listeners) {
5205
+ const mark = listener.outcome === "traversed" ? "\u2713" : listener.outcome === "answered-without-sentinel" ? "\u2717" : "?";
5206
+ const note = listener.outcome === "traversed" ? "runtime traversal reached the scaffolded guard seam" : listener.outcome === "answered-without-sentinel" ? "the listener answered and the guard seam did not" : listener.detail ?? "could not be reached";
5207
+ const authority = listener.host.includes(":") ? `[${listener.host}]` : listener.host;
5208
+ lines.push(` ${mark} ${listener.scheme}://${authority}:${listener.port} \u2014 ${note}`);
5209
+ }
5210
+ if (report.outcome === "proven") {
5211
+ lines.push("runtime traversal reached the scaffolded guard seam \u2713");
5212
+ } else if (report.outcome === "not-traversed") {
5213
+ lines.push("runtime traversal did NOT reach the scaffolded guard seam \u2717");
5214
+ } else {
5215
+ lines.push(`? runtime traversal could not be established \u2014 ${report.reason ?? "no reason given"}`);
5216
+ lines.push(" This is not a failure. The structural checks above still stand on their own.");
5217
+ }
5218
+ if (report.outcome !== "proven" && report.output.trim() !== "") {
5219
+ lines.push(" the app printed:");
5220
+ for (const line of report.output.trim().split("\n").slice(0, 20)) lines.push(` ${line}`);
5221
+ }
5222
+ return lines;
5223
+ }
5224
+
4533
5225
  // src/map-command.ts
4534
- import { writeFileSync as writeFileSync8 } from "fs";
5226
+ import { writeFileSync as writeFileSync9 } from "fs";
4535
5227
 
4536
5228
  // src/map/ts-loader.ts
4537
5229
  import { createRequire } from "module";
4538
5230
  import { pathToFileURL } from "url";
4539
- import { join as join13 } from "path";
5231
+ import { join as join14 } from "path";
4540
5232
  async function loadTypeScript(cwd) {
4541
5233
  try {
4542
- const req = createRequire(pathToFileURL(join13(cwd, "package.json")));
5234
+ const req = createRequire(pathToFileURL(join14(cwd, "package.json")));
4543
5235
  const resolved = req.resolve("typescript");
4544
5236
  const mod = await import(pathToFileURL(resolved).href);
4545
5237
  return mod.default ?? mod;
@@ -4554,9 +5246,9 @@ async function loadTypeScript(cwd) {
4554
5246
  }
4555
5247
 
4556
5248
  // src/map/extract.ts
4557
- import { readFileSync as readFileSync12, realpathSync as realpathSync3 } from "fs";
4558
- import { createHash as createHash4 } from "crypto";
4559
- import { relative as relative4 } from "path";
5249
+ import { readFileSync as readFileSync13, realpathSync as realpathSync4 } from "fs";
5250
+ import { createHash as createHash5 } from "crypto";
5251
+ import { relative as relative5 } from "path";
4560
5252
 
4561
5253
  // src/map/ast.ts
4562
5254
  function rootIdentifier(node, ts) {
@@ -4810,8 +5502,8 @@ function npmPackageOf(spec) {
4810
5502
  }
4811
5503
 
4812
5504
  // src/map/sources.ts
4813
- import { readFileSync as readFileSync8, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
4814
- import { join as join14, relative as relative2, isAbsolute } from "path";
5505
+ import { readFileSync as readFileSync9, readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
5506
+ import { join as join15, relative as relative3, isAbsolute } from "path";
4815
5507
 
4816
5508
  // src/map/routes.ts
4817
5509
  var ROUTE_REGISTER_NAMES = ["get", "post", "put", "patch", "delete", "options", "all", "head", "use"];
@@ -4895,7 +5587,7 @@ function hasEntrySignal(text) {
4895
5587
  }
4896
5588
  function detectFramework(cwd) {
4897
5589
  try {
4898
- const pkg = JSON.parse(readFileSync8(join14(cwd, "package.json"), "utf8"));
5590
+ const pkg = JSON.parse(readFileSync9(join15(cwd, "package.json"), "utf8"));
4899
5591
  const d = { ...pkg.dependencies, ...pkg.devDependencies };
4900
5592
  if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
4901
5593
  if (d["next"]) return "next";
@@ -4907,11 +5599,11 @@ function detectFramework(cwd) {
4907
5599
  } catch {
4908
5600
  }
4909
5601
  try {
4910
- if (statSync3(join14(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
5602
+ if (statSync4(join15(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
4911
5603
  } catch {
4912
5604
  }
4913
5605
  try {
4914
- if (statSync3(join14(cwd, "functions")).isDirectory()) return "deno-functions";
5606
+ if (statSync4(join15(cwd, "functions")).isDirectory()) return "deno-functions";
4915
5607
  } catch {
4916
5608
  }
4917
5609
  return "unknown";
@@ -4953,7 +5645,7 @@ function probeForSource(dir, budget) {
4953
5645
  for (const e of entries) {
4954
5646
  if (budget.left-- <= 0) return "unknown";
4955
5647
  if (e.isDirectory()) {
4956
- if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join14(dir, e.name));
5648
+ if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join15(dir, e.name));
4957
5649
  } else if (isSourceFile(e.name)) return true;
4958
5650
  else if (e.isSymbolicLink()) return "unknown";
4959
5651
  }
@@ -4966,7 +5658,7 @@ function probeForSource(dir, budget) {
4966
5658
  function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ new Set(), stats = { discovered: 0, unwalked: 0, skippedWithSource: [], skippedUnknown: 0 }, probeBudget = { left: PROBE_BUDGET }) {
4967
5659
  let key;
4968
5660
  try {
4969
- key = realpathSync(dir);
5661
+ key = realpathSync2(dir);
4970
5662
  } catch {
4971
5663
  stats.unwalked++;
4972
5664
  return out;
@@ -4990,19 +5682,19 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
4990
5682
  const probeable = !SKIP_IS_DEFINITIONAL.has(e.name);
4991
5683
  if (probeable && !e.isDirectory() && e.isSymbolicLink()) stats.skippedUnknown++;
4992
5684
  else if (probeable && e.isDirectory()) {
4993
- const held = probeForSource(join14(dir, e.name), probeBudget);
4994
- if (held === true) stats.skippedWithSource.push(join14(dir, e.name));
5685
+ const held = probeForSource(join15(dir, e.name), probeBudget);
5686
+ if (held === true) stats.skippedWithSource.push(join15(dir, e.name));
4995
5687
  else if (held === "unknown") stats.skippedUnknown++;
4996
5688
  }
4997
5689
  continue;
4998
5690
  }
4999
- const full = join14(dir, e.name);
5691
+ const full = join15(dir, e.name);
5000
5692
  if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
5001
5693
  else if (e.isSymbolicLink()) {
5002
5694
  let st, real;
5003
5695
  try {
5004
- st = statSync3(full);
5005
- real = realpathSync(full);
5696
+ st = statSync4(full);
5697
+ real = realpathSync2(full);
5006
5698
  } catch {
5007
5699
  stats.unwalked++;
5008
5700
  continue;
@@ -5025,7 +5717,7 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
5025
5717
  }
5026
5718
  function isInside(candidate, boundary) {
5027
5719
  if (candidate === boundary) return true;
5028
- const rel = relative2(boundary, candidate);
5720
+ const rel = relative3(boundary, candidate);
5029
5721
  return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
5030
5722
  }
5031
5723
  var DEPLOYMENT_SHAPES = [
@@ -5052,13 +5744,13 @@ var DEPLOYMENT_SHAPES = [
5052
5744
  function detectDeploymentShapes(cwd, opts = {}) {
5053
5745
  let boundary = opts.boundary ?? cwd;
5054
5746
  try {
5055
- boundary = realpathSync(boundary);
5747
+ boundary = realpathSync2(boundary);
5056
5748
  } catch {
5057
5749
  }
5058
- const inProject = (path13) => {
5750
+ const inProject = (path14) => {
5059
5751
  if (opts.followOutside) return true;
5060
5752
  try {
5061
- return isInside(realpathSync(path13), boundary);
5753
+ return isInside(realpathSync2(path14), boundary);
5062
5754
  } catch {
5063
5755
  return false;
5064
5756
  }
@@ -5066,9 +5758,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
5066
5758
  const found = [];
5067
5759
  for (const candidate of DEPLOYMENT_SHAPES) {
5068
5760
  for (const file of candidate.files ?? []) {
5069
- const full = join14(cwd, file);
5761
+ const full = join15(cwd, file);
5070
5762
  try {
5071
- if (statSync3(full).isFile() && inProject(full)) {
5763
+ if (statSync4(full).isFile() && inProject(full)) {
5072
5764
  found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
5073
5765
  break;
5074
5766
  }
@@ -5076,9 +5768,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
5076
5768
  }
5077
5769
  }
5078
5770
  for (const dir of candidate.dirs ?? []) {
5079
- const full = join14(cwd, dir);
5771
+ const full = join15(cwd, dir);
5080
5772
  try {
5081
- if (!statSync3(full).isDirectory() || !inProject(full)) continue;
5773
+ if (!statSync4(full).isDirectory() || !inProject(full)) continue;
5082
5774
  } catch {
5083
5775
  continue;
5084
5776
  }
@@ -5095,7 +5787,7 @@ function holdsFunctionEntry(dir) {
5095
5787
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
5096
5788
  if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
5097
5789
  try {
5098
- for (const nested of readdirSync5(join14(dir, entry.name), { withFileTypes: true })) {
5790
+ for (const nested of readdirSync5(join15(dir, entry.name), { withFileTypes: true })) {
5099
5791
  if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
5100
5792
  }
5101
5793
  } catch {
@@ -5111,7 +5803,7 @@ function holdsSourceFile(dir) {
5111
5803
  if (entry.isFile() && isSourceFile(entry.name)) return true;
5112
5804
  if (entry.isDirectory()) {
5113
5805
  try {
5114
- for (const nested of readdirSync5(join14(dir, entry.name), { withFileTypes: true })) {
5806
+ for (const nested of readdirSync5(join15(dir, entry.name), { withFileTypes: true })) {
5115
5807
  if (nested.isFile() && isSourceFile(nested.name)) return true;
5116
5808
  }
5117
5809
  } catch {
@@ -5124,8 +5816,8 @@ function holdsSourceFile(dir) {
5124
5816
  }
5125
5817
 
5126
5818
  // src/map/surface.ts
5127
- import { readFileSync as readFileSync9 } from "fs";
5128
- import { join as join15 } from "path";
5819
+ import { readFileSync as readFileSync10 } from "fs";
5820
+ import { join as join16 } from "path";
5129
5821
  var STATIC_GENERATORS = [
5130
5822
  { dep: "vite", label: "vite" },
5131
5823
  { dep: "react-scripts", label: "create-react-app" },
@@ -5163,7 +5855,7 @@ var SHAPE_SIGNAL = {
5163
5855
  };
5164
5856
  function readManifest(cwd) {
5165
5857
  try {
5166
- const parsed = JSON.parse(readFileSync9(join15(cwd, "package.json"), "utf8"));
5858
+ const parsed = JSON.parse(readFileSync10(join16(cwd, "package.json"), "utf8"));
5167
5859
  return typeof parsed === "object" && parsed !== null ? parsed : null;
5168
5860
  } catch {
5169
5861
  return null;
@@ -5176,7 +5868,7 @@ function nextExportsStatically(cwd, manifest, ts) {
5176
5868
  for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
5177
5869
  let text;
5178
5870
  try {
5179
- text = readFileSync9(join15(cwd, file), "utf8");
5871
+ text = readFileSync10(join16(cwd, file), "utf8");
5180
5872
  } catch {
5181
5873
  continue;
5182
5874
  }
@@ -5304,7 +5996,7 @@ function surfaceNote(surface) {
5304
5996
  }
5305
5997
 
5306
5998
  // src/map/sinks.ts
5307
- import { createHash as createHash3 } from "crypto";
5999
+ import { createHash as createHash4 } from "crypto";
5308
6000
  var DB_OPS = /* @__PURE__ */ new Set(["insert", "update", "delete", "select", "upsert", "rpc"]);
5309
6001
  var PRISMA_OPS = /* @__PURE__ */ new Set(["create", "createMany", "update", "updateMany", "delete", "deleteMany", "upsert", "findFirst", "findUnique", "findMany", "count", "aggregate"]);
5310
6002
  var FS_CALLS = /^(readFile|writeFile|readFileSync|writeFileSync|appendFile|createReadStream|createWriteStream|unlink|rm|rmSync|mkdir|readdir|stat|open)$/;
@@ -5403,14 +6095,14 @@ function directSinks(node, ts, bindings, ctx) {
5403
6095
  const root = base ? rootIdentifier(base, ts) : void 0;
5404
6096
  if (!root) return {};
5405
6097
  const spec = bindings.resolve(root);
5406
- const relative5 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
6098
+ const relative7 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
5407
6099
  const pkg = npmPackageOf(spec);
5408
6100
  if (pkg) return { pkg, root, spec };
5409
- if (relative5 && ctx && spec) {
6101
+ if (relative7 && ctx && spec) {
5410
6102
  const viaModule = ctx.graph.importedBinding(ctx.file, spec, bindings.exportNameOf(root) ?? root);
5411
6103
  if (viaModule) return { pkg: viaModule.package, root, spec };
5412
6104
  }
5413
- return { local: bindings.locals.has(root), root, spec, relative: relative5 };
6105
+ return { local: bindings.locals.has(root), root, spec, relative: relative7 };
5414
6106
  };
5415
6107
  const attributionOf = (b, pkg) => b.pkg ? "import" : pkg ? "inferred" : void 0;
5416
6108
  const dbApi = (pkg, attribution) => {
@@ -5571,7 +6263,7 @@ function argumentRoleOf(sinkKind, method, index, total = 0) {
5571
6263
  return table?.[index] ?? "unknown";
5572
6264
  }
5573
6265
  function sinkId(s, owner) {
5574
- return createHash3("sha256").update([s.kind, s.provider, s.package, s.table, s.op, s.file ?? owner, s.start, s.end].join("|")).digest("hex").slice(0, 12);
6266
+ return createHash4("sha256").update([s.kind, s.provider, s.package, s.table, s.op, s.file ?? owner, s.start, s.end].join("|")).digest("hex").slice(0, 12);
5575
6267
  }
5576
6268
  function dedupeSinks(sinks, owner) {
5577
6269
  const seen = /* @__PURE__ */ new Set();
@@ -5587,8 +6279,8 @@ function dedupeSinks(sinks, owner) {
5587
6279
  }
5588
6280
 
5589
6281
  // src/map/module-graph.ts
5590
- import { readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
5591
- import { dirname as dirname7, join as join16, relative as relative3, resolve as resolvePath } from "path";
6282
+ import { readFileSync as readFileSync11, realpathSync as realpathSync3, statSync as statSync5 } from "fs";
6283
+ import { dirname as dirname7, join as join17, relative as relative4, resolve as resolvePath } from "path";
5592
6284
  var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
5593
6285
  function createModuleGraph(ts, opts) {
5594
6286
  const cache = /* @__PURE__ */ new Map();
@@ -5596,7 +6288,7 @@ function createModuleGraph(ts, opts) {
5596
6288
  if (cache.has(file)) return cache.get(file) ?? null;
5597
6289
  let entry = null;
5598
6290
  try {
5599
- const text = readFileSync10(file, "utf8");
6291
+ const text = readFileSync11(file, "utf8");
5600
6292
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
5601
6293
  const bindings = buildModuleBindings(sf, ts);
5602
6294
  entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
@@ -5612,7 +6304,7 @@ function createModuleGraph(ts, opts) {
5612
6304
  if (!opts.followOutside) {
5613
6305
  let real = target;
5614
6306
  try {
5615
- real = realpathSync2(target);
6307
+ real = realpathSync3(target);
5616
6308
  } catch {
5617
6309
  }
5618
6310
  if (!isInside(real, opts.boundary)) return void 0;
@@ -5630,7 +6322,7 @@ function createModuleGraph(ts, opts) {
5630
6322
  for (const callee of mod.calleesOf.get(exportName) ?? []) {
5631
6323
  for (const s of mod.fnSinks.get(callee) ?? []) collected.push(s);
5632
6324
  }
5633
- const rel = relative3(opts.cwd, target);
6325
+ const rel = relative4(opts.cwd, target);
5634
6326
  return collected.map((s) => ({ ...s, file: rel }));
5635
6327
  },
5636
6328
  // A different question about the same module: not "what sinks are in there" but "what does this
@@ -5666,10 +6358,10 @@ function resolveRelativeModule(fromFile, spec) {
5666
6358
  }
5667
6359
  candidates.push(base);
5668
6360
  for (const e of RESOLVE_EXTS) candidates.push(base + e);
5669
- for (const e of RESOLVE_EXTS) candidates.push(join16(base, "index" + e));
6361
+ for (const e of RESOLVE_EXTS) candidates.push(join17(base, "index" + e));
5670
6362
  for (const c of candidates) {
5671
6363
  try {
5672
- if (statSync4(c).isFile()) return c;
6364
+ if (statSync5(c).isFile()) return c;
5673
6365
  } catch {
5674
6366
  }
5675
6367
  }
@@ -5695,8 +6387,8 @@ function collectCallees(sf, ts) {
5695
6387
  }
5696
6388
 
5697
6389
  // src/map/coordinates.ts
5698
- function runtimeCoordinate(source, path13) {
5699
- if (/\[\d*\]/.test(path13)) {
6390
+ function runtimeCoordinate(source, path14) {
6391
+ if (/\[\d*\]/.test(path14)) {
5700
6392
  return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
5701
6393
  }
5702
6394
  switch (source) {
@@ -5705,15 +6397,15 @@ function runtimeCoordinate(source, path13) {
5705
6397
  case "multipart":
5706
6398
  case "body":
5707
6399
  case "server-fn-data":
5708
- return { runtimeParameter: `post.${path13}` };
6400
+ return { runtimeParameter: `post.${path14}` };
5709
6401
  case "query":
5710
- return { runtimeParameter: `get.${path13}` };
6402
+ return { runtimeParameter: `get.${path14}` };
5711
6403
  case "cookie":
5712
- return { runtimeParameter: `cookie.${path13}` };
6404
+ return { runtimeParameter: `cookie.${path14}` };
5713
6405
  case "file":
5714
- return { runtimeParameter: `files.${path13}` };
6406
+ return { runtimeParameter: `files.${path14}` };
5715
6407
  case "header":
5716
- return { runtimeParameter: `server.HTTP_${path13.toUpperCase().replace(/-/g, "_")}` };
6408
+ return { runtimeParameter: `server.HTTP_${path14.toUpperCase().replace(/-/g, "_")}` };
5717
6409
  case "route-param":
5718
6410
  return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
5719
6411
  default:
@@ -5745,8 +6437,8 @@ function addressSpaceOf(source) {
5745
6437
  function isProvenFlow(confidence) {
5746
6438
  return confidence === "exact-local" || confidence === "transformed-local";
5747
6439
  }
5748
- function inputIdOf(source, path13) {
5749
- return `${addressSpaceOf(source)}:${path13}`;
6440
+ function inputIdOf(source, path14) {
6441
+ return `${addressSpaceOf(source)}:${path14}`;
5750
6442
  }
5751
6443
  function withCoordinates(fields, source) {
5752
6444
  return fields.map((f) => {
@@ -5956,8 +6648,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts) {
5956
6648
  if (!bodyNode || sinks.length === 0 || inputs.length === 0) return { flows: [], limitations: [] };
5957
6649
  const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
5958
6650
  const rootPath = /* @__PURE__ */ new Map();
5959
- const addRoot = (name, path13, space) => {
5960
- if (!rootPath.has(name)) rootPath.set(name, { path: path13, space });
6651
+ const addRoot = (name, path14, space) => {
6652
+ if (!rootPath.has(name)) rootPath.set(name, { path: path14, space });
5961
6653
  };
5962
6654
  for (const p of params ?? []) {
5963
6655
  if (!p?.name) continue;
@@ -6102,8 +6794,8 @@ function dedupeLimitations(list) {
6102
6794
  function join22(base, seg) {
6103
6795
  return base ? `${base}.${seg}` : seg;
6104
6796
  }
6105
- function normalizePath(path13) {
6106
- return path13.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
6797
+ function normalizePath(path14) {
6798
+ return path14.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
6107
6799
  }
6108
6800
  function fluentChainCalls(call, ts) {
6109
6801
  let root = call;
@@ -6349,8 +7041,8 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
6349
7041
  }
6350
7042
 
6351
7043
  // src/map/imports.ts
6352
- import { readFileSync as readFileSync11 } from "fs";
6353
- import { join as join17 } from "path";
7044
+ import { readFileSync as readFileSync12 } from "fs";
7045
+ import { join as join18 } from "path";
6354
7046
  var MAX_SITES = 5;
6355
7047
  var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
6356
7048
  function isNpmPackageName(pkg) {
@@ -6361,7 +7053,7 @@ function readPathAliases(cwd) {
6361
7053
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
6362
7054
  let paths;
6363
7055
  try {
6364
- const raw = readFileSync11(join17(cwd, name), "utf8");
7056
+ const raw = readFileSync12(join18(cwd, name), "utf8");
6365
7057
  const parsed = JSON.parse(stripJsonComments(raw));
6366
7058
  paths = parsed.compilerOptions?.paths;
6367
7059
  } catch {
@@ -6721,7 +7413,7 @@ async function extractInputMap(cwd, ts, options = {}) {
6721
7413
  const failed = [];
6722
7414
  let boundary = cwd;
6723
7415
  try {
6724
- boundary = realpathSync3(cwd);
7416
+ boundary = realpathSync4(cwd);
6725
7417
  } catch {
6726
7418
  }
6727
7419
  const graph = createModuleGraph(ts, { cwd, boundary, followOutside: options.followSymlinks });
@@ -6740,8 +7432,8 @@ async function extractInputMap(cwd, ts, options = {}) {
6740
7432
  const hopTargets = /* @__PURE__ */ new Set();
6741
7433
  for (const file of files) {
6742
7434
  try {
6743
- const text = readFileSync12(file, "utf8");
6744
- const relFile = relative4(cwd, file);
7435
+ const text = readFileSync13(file, "utf8");
7436
+ const relFile = relative5(cwd, file);
6745
7437
  sourceBytes += text.length;
6746
7438
  if (!hasEntrySignal(text)) {
6747
7439
  preFiltered++;
@@ -6753,7 +7445,7 @@ async function extractInputMap(cwd, ts, options = {}) {
6753
7445
  }
6754
7446
  parsed++;
6755
7447
  entryFiles.add(file);
6756
- const fingerprint = createHash4("sha256").update(text).digest("hex").slice(0, 16);
7448
+ const fingerprint = createHash5("sha256").update(text).digest("hex").slice(0, 16);
6757
7449
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
6758
7450
  const bindings = buildModuleBindings(sf, ts);
6759
7451
  for (const specifier of bindings.imports) {
@@ -6787,7 +7479,7 @@ async function extractInputMap(cwd, ts, options = {}) {
6787
7479
  }
6788
7480
  } catch (e) {
6789
7481
  if (typeof process !== "undefined" && process.env?.PS_MAP_DEBUG) console.error("[patchstack] map error", file, e);
6790
- failed.push(relative4(cwd, file));
7482
+ failed.push(relative5(cwd, file));
6791
7483
  }
6792
7484
  }
6793
7485
  let hopParsed = 0;
@@ -6795,10 +7487,10 @@ async function extractInputMap(cwd, ts, options = {}) {
6795
7487
  for (const target of hopTargets) {
6796
7488
  if (entryFiles.has(target)) continue;
6797
7489
  try {
6798
- const text = readFileSync12(target, "utf8");
7490
+ const text = readFileSync13(target, "utf8");
6799
7491
  const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
6800
7492
  const bindings = buildModuleBindings(sf, ts);
6801
- const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative4(cwd, target), graph });
7493
+ const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative5(cwd, target), graph });
6802
7494
  invocations.add(called.invocations);
6803
7495
  calls.total += called.counts.total;
6804
7496
  calls.dependency += called.counts.dependency;
@@ -6830,7 +7522,7 @@ async function extractInputMap(cwd, ts, options = {}) {
6830
7522
  if (endpoints.length === 0) notes.push("No recognized server-side entry points found under the analyzed roots.");
6831
7523
  const importList = imports.list();
6832
7524
  const unmodelled = importList.filter((d) => d.recognizedSinkKinds.length === 0).length;
6833
- const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative4(cwd, d));
7525
+ const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative5(cwd, d));
6834
7526
  const importCoverageGaps = {
6835
7527
  unreadableFiles: failed.length,
6836
7528
  unscannableFiles: importScanFailures,
@@ -6943,6 +7635,243 @@ function getStringFlag(flags, name) {
6943
7635
  return typeof value === "string" ? value : void 0;
6944
7636
  }
6945
7637
 
7638
+ // src/build-stamp.ts
7639
+ import { chmodSync, lstatSync as lstatSync3, readFileSync as readFileSync14, readdirSync as readdirSync6, realpathSync as realpathSync5, renameSync, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
7640
+ import { join as join19, relative as relative6, sep as sep2 } from "path";
7641
+ var RULES_FILE_NAMES = /* @__PURE__ */ new Set(["rules.json", "patchstack.rules.json"]);
7642
+ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "tmp", "vendor"]);
7643
+ var MAX_DEPTH2 = 6;
7644
+ var GUARD_SOURCE = /\.(?:[cm]?[jt]sx?)$/;
7645
+ var MAX_GUARD_SOURCE_BYTES = 1024 * 1024;
7646
+ var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7647
+ function guardImportsRulesFile(dir, rulesName) {
7648
+ const quotedRules = `["']\\./${escapeRegExp(rulesName)}["']`;
7649
+ const importsRules = new RegExp(`(?:\\bfrom\\s*|\\brequire\\s*\\(\\s*|\\bnew\\s+URL\\s*\\(\\s*)${quotedRules}`);
7650
+ let siblings;
7651
+ try {
7652
+ siblings = readdirSync6(dir);
7653
+ } catch {
7654
+ return false;
7655
+ }
7656
+ for (const name of siblings) {
7657
+ if (!GUARD_SOURCE.test(name)) continue;
7658
+ const path14 = join19(dir, name);
7659
+ try {
7660
+ const entry = lstatSync3(path14);
7661
+ if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_GUARD_SOURCE_BYTES) continue;
7662
+ const source = stripComments(readFileSync14(path14, "utf8"));
7663
+ if (source.includes("@patchstack/connect/protect") && importsRules.test(source)) return true;
7664
+ } catch {
7665
+ }
7666
+ }
7667
+ return false;
7668
+ }
7669
+ function isRuleBundle(value) {
7670
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
7671
+ const record = value;
7672
+ return Array.isArray(record.firewall) && Array.isArray(record.whitelists) && record.whitelist_keys !== null && typeof record.whitelist_keys === "object";
7673
+ }
7674
+ function findRulesFile(cwd) {
7675
+ const found = [];
7676
+ const unreadable = [];
7677
+ let root;
7678
+ try {
7679
+ root = realpathSync5(cwd);
7680
+ } catch {
7681
+ return { kind: "none" };
7682
+ }
7683
+ const contained = (path14) => {
7684
+ try {
7685
+ const real = realpathSync5(path14);
7686
+ return real === root || real.startsWith(root + sep2);
7687
+ } catch {
7688
+ return false;
7689
+ }
7690
+ };
7691
+ const walk2 = (dir, depth) => {
7692
+ if (depth > MAX_DEPTH2 || found.length > 8) return;
7693
+ let entries;
7694
+ try {
7695
+ entries = readdirSync6(dir);
7696
+ } catch {
7697
+ return;
7698
+ }
7699
+ for (const name of entries) {
7700
+ if (name.startsWith(".")) continue;
7701
+ const path14 = join19(dir, name);
7702
+ let entry;
7703
+ try {
7704
+ entry = lstatSync3(path14);
7705
+ } catch {
7706
+ continue;
7707
+ }
7708
+ if (entry.isSymbolicLink()) continue;
7709
+ if (entry.isDirectory()) {
7710
+ if (!SKIP_DIRECTORIES.has(name)) walk2(path14, depth + 1);
7711
+ continue;
7712
+ }
7713
+ if (!entry.isFile() || !RULES_FILE_NAMES.has(name)) continue;
7714
+ if (!contained(path14)) continue;
7715
+ if (!guardImportsRulesFile(dir, name)) continue;
7716
+ try {
7717
+ if (isRuleBundle(JSON.parse(readFileSync14(path14, "utf8")))) found.push(path14);
7718
+ } catch {
7719
+ unreadable.push(path14);
7720
+ }
7721
+ }
7722
+ };
7723
+ walk2(cwd, 0);
7724
+ if (found.length > 1) return { kind: "ambiguous", paths: found.map((p) => relative6(cwd, p)) };
7725
+ if (found.length === 1) return { kind: "one", path: found[0] };
7726
+ if (unreadable.length > 0) return { kind: "unreadable", paths: unreadable.map((p) => relative6(cwd, p)) };
7727
+ return { kind: "none" };
7728
+ }
7729
+ function detectIndent(text) {
7730
+ const match = /\n([ \t]+)"/.exec(text);
7731
+ if (match === null) return 0;
7732
+ const whitespace = match[1];
7733
+ return whitespace.includes(" ") ? " " : whitespace.length;
7734
+ }
7735
+ function applyBuildStamp(cwd, id) {
7736
+ const location = findRulesFile(cwd);
7737
+ if (location.kind === "none") return { kind: "skipped", reason: "no guard rules file in this project" };
7738
+ if (location.kind === "unreadable") {
7739
+ return { kind: "skipped", reason: `${location.paths.join(", ")} is not valid JSON \u2014 left untouched` };
7740
+ }
7741
+ if (location.kind === "ambiguous") {
7742
+ return {
7743
+ kind: "skipped",
7744
+ reason: `more than one candidate guard rules file (${location.paths.join(", ")}) \u2014 none was stamped`
7745
+ };
7746
+ }
7747
+ const file = relative6(cwd, location.path);
7748
+ let text;
7749
+ try {
7750
+ text = readFileSync14(location.path, "utf8");
7751
+ } catch (err) {
7752
+ return { kind: "skipped", reason: `${file} could not be read (${err.message})` };
7753
+ }
7754
+ let parsed;
7755
+ try {
7756
+ parsed = JSON.parse(text);
7757
+ } catch {
7758
+ return { kind: "skipped", reason: `${file} is not valid JSON \u2014 left untouched` };
7759
+ }
7760
+ if (!isRuleBundle(parsed)) return { kind: "skipped", reason: `${file} is not a guard rules bundle` };
7761
+ const bundle = parsed;
7762
+ const current = readBuildStamp(bundle);
7763
+ if (id === null ? !hasRawBuildStamp(bundle) : current === id) return { kind: "unchanged", file };
7764
+ const namespace = { ...bundle[BUILD_STAMP_KEY] ?? {} };
7765
+ if (id === null) delete namespace.build_id;
7766
+ else namespace.build_id = id;
7767
+ if (Object.keys(namespace).length === 0) delete bundle[BUILD_STAMP_KEY];
7768
+ else bundle[BUILD_STAMP_KEY] = namespace;
7769
+ const serialised = JSON.stringify(bundle, null, detectIndent(text)) + (text.endsWith("\n") ? "\n" : "");
7770
+ const temporary = `${location.path}.patchstack-${process.pid}.tmp`;
7771
+ try {
7772
+ const mode = lstatSync3(location.path).mode & 4095;
7773
+ writeFileSync8(temporary, serialised, { encoding: "utf8", mode });
7774
+ chmodSync(temporary, mode);
7775
+ renameSync(temporary, location.path);
7776
+ } catch (err) {
7777
+ try {
7778
+ unlinkSync(temporary);
7779
+ } catch {
7780
+ }
7781
+ return { kind: "skipped", reason: `${file} could not be written (${err.message})` };
7782
+ }
7783
+ return id === null ? { kind: "cleared", file } : { kind: "stamped", file, id };
7784
+ }
7785
+
7786
+ // src/build-hook.ts
7787
+ var INSTALL_AND_BUILD_EVENTS = /* @__PURE__ */ new Set([
7788
+ "preinstall",
7789
+ "install",
7790
+ "postinstall",
7791
+ "prepare",
7792
+ "prebuild",
7793
+ "build",
7794
+ "postbuild"
7795
+ ]);
7796
+ function isInstallOrBuildHook(env = process.env) {
7797
+ const event = env.npm_lifecycle_event;
7798
+ return typeof event === "string" && INSTALL_AND_BUILD_EVENTS.has(event);
7799
+ }
7800
+ function isPreBundleBuildHook(env = process.env) {
7801
+ const event = env.npm_lifecycle_event;
7802
+ if (event === "prebuild") return true;
7803
+ if (event !== "build") return false;
7804
+ return /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(env.npm_lifecycle_script ?? "");
7805
+ }
7806
+ function undeliveredReportLines(err, config, cwd) {
7807
+ const lines = [`patchstack: manifest not reported \u2014 ${err.message}`];
7808
+ if (err.code === "UNAUTHORIZED") {
7809
+ const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
7810
+ if (hasCredential) {
7811
+ lines.push(
7812
+ "patchstack: the credential this environment holds was rejected. If `login` rotated it, set the new value as PATCHSTACK_API_KEY here."
7813
+ );
7814
+ } else {
7815
+ lines.push(
7816
+ `patchstack: PATCHSTACK_API_KEY is not set, and neither ${SECRET_CONFIG_FILENAME} nor .patchstackrc.json in ${cwd} holds a credential.`,
7817
+ `patchstack: ${SECRET_CONFIG_FILENAME} is git-ignored, so a clean checkout never has it. Set PATCHSTACK_API_KEY in the platform's environment so builds can report.`
7818
+ );
7819
+ }
7820
+ }
7821
+ lines.push(
7822
+ "patchstack: continuing the build. Patchstack keeps the last manifest it accepted for this site until a scan that can report."
7823
+ );
7824
+ return lines;
7825
+ }
7826
+
7827
+ // src/input-map-id.ts
7828
+ import { createHash as createHash6 } from "crypto";
7829
+ var NonCanonicalInputMap = class extends Error {
7830
+ };
7831
+ function canonicalString(value) {
7832
+ let out = '"';
7833
+ for (const character of value) {
7834
+ const code = character.codePointAt(0);
7835
+ if (character === '"') out += '\\"';
7836
+ else if (character === "\\") out += "\\\\";
7837
+ else if (code < 32) out += `\\u${code.toString(16).padStart(4, "0")}`;
7838
+ else out += character;
7839
+ }
7840
+ return `${out}"`;
7841
+ }
7842
+ function canonicalText(value) {
7843
+ if (value === null) return "null";
7844
+ if (value === true) return "true";
7845
+ if (value === false) return "false";
7846
+ if (typeof value === "string") return canonicalString(value);
7847
+ if (typeof value === "number") {
7848
+ if (!Number.isSafeInteger(value)) {
7849
+ throw new NonCanonicalInputMap("the input map contains a number outside the safe-integer form");
7850
+ }
7851
+ return String(value === 0 ? 0 : value);
7852
+ }
7853
+ if (Array.isArray(value)) return `[${value.map(canonicalText).join(",")}]`;
7854
+ if (value !== null && typeof value === "object") {
7855
+ const record = value;
7856
+ const keys = Object.keys(record).sort();
7857
+ return `{${keys.map((key) => `${canonicalString(key)}:${canonicalText(record[key])}`).join(",")}}`;
7858
+ }
7859
+ throw new NonCanonicalInputMap(`the input map contains a value with no JSON form (${typeof value})`);
7860
+ }
7861
+ function inputMapBuildId(map) {
7862
+ let material = map;
7863
+ if (map !== null && typeof map === "object" && !Array.isArray(map)) {
7864
+ const record = map;
7865
+ const { build_id: _buildId, ...mapDocument } = record;
7866
+ const coverage = record.coverage;
7867
+ if (coverage !== null && typeof coverage === "object" && !Array.isArray(coverage)) {
7868
+ const { analysisMs: _analysisMs, rssBytes: _rssBytes, peakRssBytes: _peakRssBytes, ...stableCoverage } = coverage;
7869
+ material = { ...mapDocument, coverage: stableCoverage };
7870
+ } else material = mapDocument;
7871
+ }
7872
+ return createHash6("sha256").update("patchstack-input-map-v1\0", "utf8").update(canonicalText(material), "utf8").digest("hex");
7873
+ }
7874
+
6946
7875
  // src/map-command.ts
6947
7876
  async function runMap(flags) {
6948
7877
  const cwd = getStringFlag(flags, "dir") ?? process.cwd();
@@ -6987,7 +7916,7 @@ async function runMap(flags) {
6987
7916
  const json = JSON.stringify(map, null, 2);
6988
7917
  const out = getStringFlag(flags, "out");
6989
7918
  if (out) {
6990
- writeFileSync8(out, json);
7919
+ writeFileSync9(out, json);
6991
7920
  console.error(`patchstack: wrote ${out}`);
6992
7921
  } else if (flags.get("upload") !== true) {
6993
7922
  console.log(json);
@@ -7003,7 +7932,25 @@ async function runMap(flags) {
7003
7932
  cliSiteUuid: getStringFlag(flags, "site-uuid"),
7004
7933
  cliEndpoint: getStringFlag(flags, "endpoint")
7005
7934
  });
7006
- const outcome = await postInputMap(config, map);
7935
+ let buildId = null;
7936
+ if (isPreBundleBuildHook()) {
7937
+ const candidate = inputMapBuildId(map);
7938
+ const stamp = applyBuildStamp(cwd, candidate);
7939
+ if (stamp.kind === "stamped" || stamp.kind === "unchanged") {
7940
+ buildId = candidate;
7941
+ console.error(`patchstack: bound this map to ${stamp.file} (${candidate.slice(0, 12)}).`);
7942
+ } else {
7943
+ const reason = stamp.kind === "skipped" ? stamp.reason : "the rules file did not retain the map identity";
7944
+ console.error(
7945
+ `patchstack: could not bind this map to the runtime guard \u2014 ${reason}. Rules generated from these coordinates will detect only, not block.`
7946
+ );
7947
+ }
7948
+ } else {
7949
+ console.error(
7950
+ "patchstack: no runtime binding recorded \u2014 run `map --upload` in a prebuild hook before the bundler, so rules generated from these coordinates can be tied to the runtime guard. Until then they detect only, not block."
7951
+ );
7952
+ }
7953
+ const outcome = await postInputMap(config, map, buildId);
7007
7954
  if (outcome.result === "stored") {
7008
7955
  console.error(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
7009
7956
  } else if (outcome.result === "unchanged") {
@@ -7018,8 +7965,8 @@ async function runMap(flags) {
7018
7965
  }
7019
7966
 
7020
7967
  // src/setup.ts
7021
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
7022
- import path12 from "path";
7968
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
7969
+ import path13 from "path";
7023
7970
  var SCAN_COMMAND = "patchstack-connect scan";
7024
7971
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
7025
7972
  function setupProtection(cwd) {
@@ -7036,9 +7983,16 @@ function appendHook(existing, command) {
7036
7983
  }
7037
7984
  return `${existing} && ${command}`;
7038
7985
  }
7986
+ function prependHook(existing, command) {
7987
+ if (existing === void 0 || existing.trim().length === 0) return command;
7988
+ const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7989
+ if (new RegExp(`^\\s*${escaped}\\s*(?:;|\\|\\|)`).test(existing)) return existing;
7990
+ const remaining = existing.split(/\s*&&\s*/).filter((part) => part.trim() !== command && part.trim().length > 0);
7991
+ return [command, ...remaining].join(" && ");
7992
+ }
7039
7993
  function wireBuildScripts(cwd, packageManager) {
7040
- const target = path12.join(cwd, "package.json");
7041
- const raw = readFileSync13(target, "utf8");
7994
+ const target = path13.join(cwd, "package.json");
7995
+ const raw = readFileSync15(target, "utf8");
7042
7996
  const pkg = JSON.parse(raw);
7043
7997
  const scripts = pkg.scripts ?? {};
7044
7998
  const build = scripts.build;
@@ -7053,10 +8007,7 @@ function wireBuildScripts(cwd, packageManager) {
7053
8007
  }
7054
8008
  scripts.postinstall = postinstall;
7055
8009
  } else if (packageManager === "bun") {
7056
- let nextBuild = build;
7057
- if (!nextBuild.includes(SCAN_COMMAND)) {
7058
- nextBuild = `${SCAN_COMMAND} && ${nextBuild}`;
7059
- }
8010
+ let nextBuild = prependHook(build, SCAN_COMMAND);
7060
8011
  if (!nextBuild.includes(MARK_BUILD_COMMAND)) {
7061
8012
  nextBuild = `${nextBuild} && ${MARK_BUILD_COMMAND}`;
7062
8013
  }
@@ -7070,7 +8021,7 @@ function wireBuildScripts(cwd, packageManager) {
7070
8021
  scripts.postinstall = postinstall;
7071
8022
  scripts.build = nextBuild;
7072
8023
  } else {
7073
- const prebuild = appendHook(scripts.prebuild, SCAN_COMMAND);
8024
+ const prebuild = prependHook(scripts.prebuild, SCAN_COMMAND);
7074
8025
  const postbuild = appendHook(scripts.postbuild, MARK_BUILD_COMMAND);
7075
8026
  if (prebuild === scripts.prebuild && postbuild === scripts.postbuild && postinstall === scripts.postinstall) {
7076
8027
  return {
@@ -7087,7 +8038,7 @@ function wireBuildScripts(cwd, packageManager) {
7087
8038
  const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
7088
8039
  const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
7089
8040
  const trailingNewline = raw.endsWith("\n") ? "\n" : "";
7090
- writeFileSync9(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
8041
+ writeFileSync10(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
7091
8042
  if (build === void 0 || build.trim().length === 0) {
7092
8043
  return {
7093
8044
  changed: true,
@@ -7106,41 +8057,6 @@ function wireBuildScripts(cwd, packageManager) {
7106
8057
  };
7107
8058
  }
7108
8059
 
7109
- // src/build-hook.ts
7110
- var INSTALL_AND_BUILD_EVENTS = /* @__PURE__ */ new Set([
7111
- "preinstall",
7112
- "install",
7113
- "postinstall",
7114
- "prepare",
7115
- "prebuild",
7116
- "build",
7117
- "postbuild"
7118
- ]);
7119
- function isInstallOrBuildHook(env = process.env) {
7120
- const event = env.npm_lifecycle_event;
7121
- return typeof event === "string" && INSTALL_AND_BUILD_EVENTS.has(event);
7122
- }
7123
- function undeliveredReportLines(err, config, cwd) {
7124
- const lines = [`patchstack: manifest not reported \u2014 ${err.message}`];
7125
- if (err.code === "UNAUTHORIZED") {
7126
- const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
7127
- if (hasCredential) {
7128
- lines.push(
7129
- "patchstack: the credential this environment holds was rejected. If `login` rotated it, set the new value as PATCHSTACK_API_KEY here."
7130
- );
7131
- } else {
7132
- lines.push(
7133
- `patchstack: PATCHSTACK_API_KEY is not set, and neither ${SECRET_CONFIG_FILENAME} nor .patchstackrc.json in ${cwd} holds a credential.`,
7134
- `patchstack: ${SECRET_CONFIG_FILENAME} is git-ignored, so a clean checkout never has it. Set PATCHSTACK_API_KEY in the platform's environment so builds can report.`
7135
- );
7136
- }
7137
- }
7138
- lines.push(
7139
- "patchstack: continuing the build. Patchstack keeps the last manifest it accepted for this site until a scan that can report."
7140
- );
7141
- return lines;
7142
- }
7143
-
7144
8060
  // src/cli.ts
7145
8061
  var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
7146
8062
 
@@ -7168,7 +8084,8 @@ Usage:
7168
8084
  with how the link was established). Best-effort static
7169
8085
  analysis \u2014 reports the DETECTED surface, with
7170
8086
  coverage counters. Prints JSON (--out writes a
7171
- file; --follow-symlinks leaves the project dir).
8087
+ file; a pre-bundle --upload also stamps the existing
8088
+ guard rules file; --follow-symlinks leaves the project dir).
7172
8089
  Uses the app's own TypeScript
7173
8090
  patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
7174
8091
  with an existing site UUID
@@ -7192,6 +8109,15 @@ Usage:
7192
8109
  demonstrations, not production).
7193
8110
  --check verifies the guard is wired (exit 1
7194
8111
  if not) \u2014 for the wire-then-verify loop.
8112
+ It reads the app's source and never runs it.
8113
+ --check --runtime additionally STARTS THE APP
8114
+ on a loopback port and sends it one request,
8115
+ to establish that a request reaches the
8116
+ guard. Opt-in, and the only mode that runs
8117
+ the app. Exit 0 traversed, 1 a listener
8118
+ answered instead of the guard, 2 could not
8119
+ be established (e.g. a TypeScript or
8120
+ framework entry this must not start).
7195
8121
  patchstack-connect demo node-serialize [--url URL] Run the production-backed node-serialize
7196
8122
  walkthrough: verify the vulnerable package,
7197
8123
  scan it, wait for live rule 18843, install +
@@ -7204,6 +8130,20 @@ Usage:
7204
8130
  what's missing, with tailored commands), then
7205
8131
  print the full setup guide. --full prints the
7206
8132
  guide even when setup is complete
8133
+ patchstack-connect claim [--wait] Attach this site to a Patchstack account from
8134
+ the terminal, so its reports become visible.
8135
+ Prints a link for the user to open and sign in
8136
+ (or sign up) \u2014 whoever approves becomes the
8137
+ site's owner. In a terminal it then waits. When
8138
+ the output is piped or captured \u2014 an assistant
8139
+ running it \u2014 it prints the link and EXITS, so
8140
+ the link is visible immediately. Run it AGAIN
8141
+ once the user confirms: it resumes the same
8142
+ request rather than starting a new one. --wait
8143
+ blocks instead of returning. Does not rotate
8144
+ the site's credential. The same claim can be
8145
+ done by opening the dashboard link that scan
8146
+ and status print. Not usable in CI
7207
8147
  patchstack-connect login [--wait] Recover this site's credential when
7208
8148
  .patchstackrc.local.json has been lost. Needs the
7209
8149
  site UUID, which lives in the committed
@@ -7321,6 +8261,107 @@ async function runInit(args) {
7321
8261
  console.log("Next: run `npx @patchstack/connect scan` to send your first manifest.");
7322
8262
  return 0;
7323
8263
  }
8264
+ async function runClaim(args) {
8265
+ if (process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false") {
8266
+ console.error("`claim` is interactive and cannot run in CI. Claim the site from a developer machine.");
8267
+ return 1;
8268
+ }
8269
+ const config = await resolveConfig({
8270
+ cwd: process.cwd(),
8271
+ cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
8272
+ cliEndpoint: getStringFlag(args.flags, "endpoint")
8273
+ });
8274
+ const siteUuid = config.siteUuid;
8275
+ if (siteUuid === null) {
8276
+ console.error("\n No site to claim yet \u2014 run `npx @patchstack/connect scan` first.\n");
8277
+ return 1;
8278
+ }
8279
+ const settled = async (result2) => {
8280
+ console.log(
8281
+ `
8282
+ \u2713 Site claimed${result2.account !== void 0 ? ` by ${result2.account}` : ""}.`
8283
+ );
8284
+ console.log(` ${result2.dashboardUrl ?? buildClaimUrl(config.endpoint, siteUuid)}`);
8285
+ if (result2.credentialSaved === true) {
8286
+ const ignore = await secretFileIgnored(process.cwd());
8287
+ console.log(` A credential for this site was issued and saved to ${SECRET_CONFIG_FILENAME}.`);
8288
+ console.log(
8289
+ ignore.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${ignore.reason ?? "unknown reason"}. Add it to .gitignore yourself before committing.`
8290
+ );
8291
+ }
8292
+ console.log("");
8293
+ return 0;
8294
+ };
8295
+ const prompt = (userCode, verificationUri) => {
8296
+ console.log(`
8297
+ Your code: ${userCode}`);
8298
+ console.log(` Claim at: ${verificationUri}
8299
+ `);
8300
+ console.log(" Open that link and sign in to Patchstack \u2014 or create an account \u2014 to attach");
8301
+ console.log(" this site to it. Whoever approves becomes the site's owner.\n");
8302
+ };
8303
+ if (args.flags.has("wait")) {
8304
+ const pending = readPendingClaim(siteUuid);
8305
+ if (pending === null) {
8306
+ console.error("\n No claim is waiting for approval. Run `patchstack-connect claim` first.\n");
8307
+ return 1;
8308
+ }
8309
+ const resumed = await waitForClaim(config, pending);
8310
+ if (resumed.status === "claimed") return await settled(resumed);
8311
+ console.error(`
8312
+ ${resumed.message ?? "Claim failed."}
8313
+ `);
8314
+ return 1;
8315
+ }
8316
+ if (process.stdout.isTTY !== true) {
8317
+ const existing = readPendingClaim(siteUuid);
8318
+ if (existing !== null && Date.now() < existing.expiresAt) {
8319
+ const outcome = await redeemClaimIfApproved(config, existing);
8320
+ if (outcome !== "pending" && outcome.status === "claimed") return await settled(outcome);
8321
+ if (outcome === "pending") {
8322
+ const secondsLeft = Math.round((existing.expiresAt - Date.now()) / 1e3);
8323
+ console.log(`
8324
+ Still waiting for the site to be claimed with code ${existing.userCode}.`);
8325
+ console.log(` Claim at: ${existing.verificationUri}`);
8326
+ console.log(` (valid for another ${secondsLeft}s \u2014 run this again once the user confirms)
8327
+ `);
8328
+ return 0;
8329
+ }
8330
+ }
8331
+ const started = await startClaim(config);
8332
+ if (started.status !== "started" || started.pending === void 0) {
8333
+ if (started.status === "already-claimed") {
8334
+ console.log(`
8335
+ ${started.message ?? "This site is already claimed."}
8336
+ `);
8337
+ return 0;
8338
+ }
8339
+ console.error(`
8340
+ ${started.message ?? "Claim failed."}
8341
+ `);
8342
+ return 1;
8343
+ }
8344
+ prompt(started.pending.userCode, started.pending.verificationUri);
8345
+ console.log(" Give that link to the user. When they confirm they have claimed the site, run");
8346
+ console.log(" this same command again (or `claim --wait` to block until they do).\n");
8347
+ return 0;
8348
+ }
8349
+ const result = await claim2(config, (userCode, verificationUri) => {
8350
+ prompt(userCode, verificationUri);
8351
+ console.log(" Waiting (the code expires in 10 minutes)\u2026");
8352
+ });
8353
+ if (result.status === "claimed") return await settled(result);
8354
+ if (result.status === "already-claimed") {
8355
+ console.log(`
8356
+ ${result.message ?? "This site is already claimed."}
8357
+ `);
8358
+ return 0;
8359
+ }
8360
+ console.error(`
8361
+ ${result.message ?? "Claim failed."}
8362
+ `);
8363
+ return 1;
8364
+ }
7324
8365
  async function runLogin(args) {
7325
8366
  if (process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false") {
7326
8367
  console.error("`login` is interactive and cannot run in CI. Set PATCHSTACK_PULSE_AUTH instead.");
@@ -7466,6 +8507,10 @@ async function runScan(args, options = {}) {
7466
8507
  }
7467
8508
  return 0;
7468
8509
  }
8510
+ if (isPreBundleBuildHook()) {
8511
+ const cleared = applyBuildStamp(process.cwd(), null);
8512
+ if (cleared.kind === "cleared") console.log(`Removed the previous map binding from ${cleared.file}.`);
8513
+ }
7469
8514
  if (config.widget) {
7470
8515
  reportSourceMarker(detectStack(payload.packages).framework);
7471
8516
  }
@@ -7523,6 +8568,9 @@ async function runScan(args, options = {}) {
7523
8568
  if (config.endpoint !== DEFAULT_ENDPOINT) {
7524
8569
  console.log(" (this URL inherits the endpoint override above)");
7525
8570
  }
8571
+ console.log("");
8572
+ console.log("Or attach it to your account from this terminal:");
8573
+ console.log(" npx @patchstack/connect claim");
7526
8574
  }
7527
8575
  if (options.showRemainingSetup !== false) {
7528
8576
  try {
@@ -7610,6 +8658,11 @@ function reportSourceMarker(framework) {
7610
8658
  }
7611
8659
  }
7612
8660
  async function runProtectCommand(args) {
8661
+ const runtime = args.flags.get("runtime") === true;
8662
+ if (runtime && args.flags.get("check") !== true) {
8663
+ console.error("patchstack protect: --runtime only applies to --check. Run `protect --check --runtime`.");
8664
+ return 1;
8665
+ }
7613
8666
  if (args.flags.get("check") === true) {
7614
8667
  const report = runVerify(process.cwd());
7615
8668
  console.log(`patchstack protect --check (${report.stack}):`);
@@ -7628,7 +8681,12 @@ async function runProtectCommand(args) {
7628
8681
  if (report.checks.some((c) => c.unverifiable)) {
7629
8682
  console.log("One or more checks could not be answered from here \u2014 see the `?` lines above.");
7630
8683
  }
7631
- return report.wired ? 0 : 1;
8684
+ if (!report.wired) return 1;
8685
+ if (!runtime) return 0;
8686
+ console.log("");
8687
+ const runtimeReport = await runRuntimeCheck(process.cwd());
8688
+ for (const line2 of formatRuntimeCheck(runtimeReport)) console.log(line2);
8689
+ return runtimeExitCode(runtimeReport);
7632
8690
  }
7633
8691
  const demo = args.flags.get("demo") === true;
7634
8692
  try {
@@ -7751,7 +8809,7 @@ async function runGuide(args) {
7751
8809
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
7752
8810
  console.log("");
7753
8811
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
7754
- console.log(readFileSync14(guidePath, "utf8"));
8812
+ console.log(readFileSync16(guidePath, "utf8"));
7755
8813
  return 0;
7756
8814
  }
7757
8815
  async function runSetup(args) {
@@ -7825,6 +8883,7 @@ async function runStatus(args) {
7825
8883
  console.log(`Environment: ${config.environment}`);
7826
8884
  if (config.siteUuid !== null) {
7827
8885
  console.log(`Dashboard URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
8886
+ console.log(" Not attached to an account yet? Run `npx @patchstack/connect claim`.");
7828
8887
  switch (await fetchSiteStatus(config)) {
7829
8888
  case "active":
7830
8889
  console.log("Site status: active on Patchstack");
@@ -7944,7 +9003,7 @@ async function runMarkBuild(args) {
7944
9003
  let marked = 0;
7945
9004
  let widgetTouched = 0;
7946
9005
  for (const file of files) {
7947
- const before = readFileSync14(file, "utf8");
9006
+ const before = readFileSync16(file, "utf8");
7948
9007
  let after = injectMarker(before, snippet);
7949
9008
  if (widgetUuid !== null) {
7950
9009
  const ensured = ensureWidgetInHtml(after, widgetUuid);
@@ -7954,7 +9013,7 @@ async function runMarkBuild(args) {
7954
9013
  after = ensured.html;
7955
9014
  }
7956
9015
  if (after !== before) {
7957
- writeFileSync10(file, after);
9016
+ writeFileSync11(file, after);
7958
9017
  marked += 1;
7959
9018
  }
7960
9019
  }
@@ -8006,6 +9065,8 @@ async function main() {
8006
9065
  return runSetup(args);
8007
9066
  case "map":
8008
9067
  return runMap(args.flags);
9068
+ case "claim":
9069
+ return runClaim(args);
8009
9070
  case "login":
8010
9071
  return runLogin(args);
8011
9072
  default: