@patchstack/connect 0.5.1 → 0.5.2

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 readFileSync15, writeFileSync as writeFileSync10 } 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();
@@ -1295,16 +1295,16 @@ var CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
1295
1295
  function claimTokenHeader(config) {
1296
1296
  return typeof config.claimToken === "string" && config.claimToken !== "" ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
1297
1297
  }
1298
- function claimOutcomeLines(claim2, config) {
1298
+ function claimOutcomeLines(claim3, config) {
1299
1299
  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}`] : [];
1300
+ if (claim3?.state === "claimed" || claim3?.state === "owned-by-you") {
1301
+ const dashboard = typeof claim3.dashboard_url === "string" && claim3.dashboard_url !== "" ? [`Dashboard: ${claim3.dashboard_url}`] : [];
1302
1302
  return [
1303
- claim2.state === "claimed" ? "Connected to your Patchstack account." : "This site is already connected to your Patchstack account.",
1303
+ claim3.state === "claimed" ? "Connected to your Patchstack account." : "This site is already connected to your Patchstack account.",
1304
1304
  ...dashboard
1305
1305
  ];
1306
1306
  }
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";
1307
+ 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
1308
  return [
1309
1309
  `Not connected to your account: ${why}.`,
1310
1310
  "Open the dashboard link below to connect it, or copy a fresh prompt from the dashboard."
@@ -1329,8 +1329,8 @@ function authFailureMessage(status, config) {
1329
1329
  }
1330
1330
  function buildRulesUrl(manifestEndpoint, siteUuid) {
1331
1331
  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)}`;
1332
+ const path14 = url.pathname.replace(/\/$/, "");
1333
+ url.pathname = path14.endsWith("/manifest") ? `${path14.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
1334
1334
  url.search = "";
1335
1335
  url.hash = "";
1336
1336
  return url.toString();
@@ -1345,16 +1345,16 @@ function buildSettingsUrl(endpoint, siteUuid) {
1345
1345
  }
1346
1346
  function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
1347
1347
  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)}`;
1348
+ const path14 = url.pathname.replace(/\/$/, "");
1349
+ url.pathname = path14.endsWith("/manifest") ? `${path14.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1350
1350
  url.search = "";
1351
1351
  url.hash = "";
1352
1352
  return url.toString();
1353
1353
  }
1354
1354
  function buildInputMapUrl(manifestEndpoint, siteUuid) {
1355
1355
  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)}`;
1356
+ const path14 = url.pathname.replace(/\/$/, "");
1357
+ url.pathname = path14.endsWith("/manifest") ? `${path14.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1358
1358
  url.search = "";
1359
1359
  url.hash = "";
1360
1360
  return url.toString();
@@ -1690,7 +1690,7 @@ async function waitForDemoRule(manifestEndpoint, siteUuid, scenario, options = {
1690
1690
  const pollIntervalMs = options.pollIntervalMs ?? 1e3;
1691
1691
  const requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
1692
1692
  const fetchFn = options.fetchFn ?? fetch;
1693
- const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
1693
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve4) => setTimeout(resolve4, ms)));
1694
1694
  const now = options.now ?? Date.now;
1695
1695
  const started = now();
1696
1696
  const rulesUrl = buildRulesUrl(manifestEndpoint, siteUuid);
@@ -3011,6 +3011,112 @@ import { join as join10, dirname as dirname5 } from "path";
3011
3011
  import { execFileSync } from "child_process";
3012
3012
  import { existsSync as existsSync7 } from "fs";
3013
3013
  import { dirname as dirname4, join as join9, resolve } from "path";
3014
+
3015
+ // src/protect/install/node-flags.ts
3016
+ var flagName = (token) => token.split("=")[0];
3017
+ var NO_OPERAND_FLAGS = /* @__PURE__ */ new Set([
3018
+ "--disallow-code-generation-from-strings",
3019
+ "--enable-source-maps",
3020
+ "--experimental-import-meta-resolve",
3021
+ "--experimental-json-modules",
3022
+ "--experimental-vm-modules",
3023
+ "--frozen-intrinsics",
3024
+ "--no-deprecation",
3025
+ "--no-warnings",
3026
+ "--pending-deprecation",
3027
+ "--preserve-symlinks",
3028
+ "--preserve-symlinks-main",
3029
+ "--throw-deprecation",
3030
+ "--trace-deprecation",
3031
+ "--trace-exit",
3032
+ "--trace-uncaught",
3033
+ "--trace-warnings",
3034
+ "--use-strict",
3035
+ "--zero-fill-buffers"
3036
+ ]);
3037
+ var VALUED_FLAGS = /* @__PURE__ */ new Set([
3038
+ "--dns-result-order",
3039
+ "--max-http-header-size",
3040
+ "--max-old-space-size",
3041
+ "--max-semi-space-size",
3042
+ "--stack-size",
3043
+ "--title",
3044
+ "--unhandled-rejections"
3045
+ ]);
3046
+ var CODE_LOADING_FLAGS = /* @__PURE__ */ new Set([
3047
+ "--require",
3048
+ "-r",
3049
+ "--import",
3050
+ "--loader",
3051
+ "--experimental-loader",
3052
+ "--eval",
3053
+ "-e",
3054
+ "--print",
3055
+ "-p",
3056
+ "--inspect",
3057
+ "--inspect-brk",
3058
+ "--inspect-wait",
3059
+ "--inspect-port",
3060
+ "--debug",
3061
+ "--debug-brk"
3062
+ ]);
3063
+ var TAKES_NEXT_TOKEN = /* @__PURE__ */ new Set(["--require", "-r", "--import", "--loader", "--experimental-loader", "--eval", "-e", "--print", "-p", "--inspect-port"]);
3064
+ function tokenizeNodeOptions(value) {
3065
+ const tokens = [];
3066
+ let current = "";
3067
+ let quoted = false;
3068
+ let started = false;
3069
+ for (const ch of value) {
3070
+ if (ch === '"') {
3071
+ quoted = !quoted;
3072
+ started = true;
3073
+ continue;
3074
+ }
3075
+ if (!quoted && /\s/.test(ch)) {
3076
+ if (started) tokens.push(current);
3077
+ current = "";
3078
+ started = false;
3079
+ continue;
3080
+ }
3081
+ current += ch;
3082
+ started = true;
3083
+ }
3084
+ if (quoted) return null;
3085
+ if (started) tokens.push(current);
3086
+ return tokens;
3087
+ }
3088
+ function classifyNodeOptions(value) {
3089
+ const tokens = tokenizeNodeOptions(value);
3090
+ if (tokens === null) return { kind: "refused", why: "NODE_OPTIONS has an unbalanced double quote, so this cannot read it the way Node will" };
3091
+ for (const token of tokens) {
3092
+ const name = flagName(token);
3093
+ if (CODE_LOADING_FLAGS.has(name)) {
3094
+ return { kind: "refused", why: `NODE_OPTIONS carries ${name}, which runs code or opens a port before the listener reporter is in place` };
3095
+ }
3096
+ if (NO_OPERAND_FLAGS.has(token)) continue;
3097
+ if (token.includes("=") && VALUED_FLAGS.has(name)) continue;
3098
+ return { kind: "refused", why: `NODE_OPTIONS carries ${name}, which this does not recognise well enough to say what it does` };
3099
+ }
3100
+ return { kind: "safe" };
3101
+ }
3102
+ function withoutCodeLoading(value) {
3103
+ if (value === void 0 || value.trim() === "") return "";
3104
+ const tokens = tokenizeNodeOptions(value);
3105
+ if (tokens === null) return "";
3106
+ const kept = [];
3107
+ for (let i = 0; i < tokens.length; i++) {
3108
+ const token = tokens[i];
3109
+ const name = flagName(token);
3110
+ if (CODE_LOADING_FLAGS.has(name)) {
3111
+ if (!token.includes("=") && TAKES_NEXT_TOKEN.has(name)) i++;
3112
+ continue;
3113
+ }
3114
+ if (NO_OPERAND_FLAGS.has(token) || token.includes("=") && VALUED_FLAGS.has(name)) kept.push(token);
3115
+ }
3116
+ return kept.map((token) => /\s/.test(token) ? `"${token}"` : token).join(" ");
3117
+ }
3118
+
3119
+ // src/protect/install/source-scope.ts
3014
3120
  var IMPORT_LINE = /^\s*(?:import\b|export\s+(?:\*|\{)|(?:const|let|var)\s+[^=]+=\s*require\()/;
3015
3121
  function lastTopLevelImportLine(source) {
3016
3122
  const lines = source.split("\n");
@@ -3089,7 +3195,10 @@ function advanceDepth(line, inBlockComment, add) {
3089
3195
  function parses(filePath) {
3090
3196
  if (!/\.(?:js|cjs|mjs)$/.test(filePath)) return null;
3091
3197
  try {
3092
- execFileSync(process.execPath, ["--check", filePath], { stdio: "pipe" });
3198
+ execFileSync(process.execPath, ["--check", filePath], {
3199
+ stdio: "pipe",
3200
+ env: { ...process.env, NODE_OPTIONS: withoutCodeLoading(process.env.NODE_OPTIONS) }
3201
+ });
3093
3202
  return true;
3094
3203
  } catch (error) {
3095
3204
  return isSyntaxError(error) ? false : null;
@@ -4358,12 +4467,15 @@ function renderGuideChecklist(state, useColor) {
4358
4467
  lines.push(detail(`${check.label}${check.hint ? ` \u2014 ${check.hint}` : ""}`));
4359
4468
  }
4360
4469
  lines.push(detail("Verify \u2192 npx @patchstack/connect protect --check"));
4470
+ lines.push(detail("Prove a request reaches the guard (starts your app) \u2192 npx @patchstack/connect protect --check --runtime"));
4361
4471
  }
4362
4472
  lines.push("");
4363
4473
  if (state.claimUrl !== null) {
4364
4474
  lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Dashboard link (open to view reports):")}`);
4365
4475
  lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
4366
4476
  lines.push(detail("Open this link in a browser. The CLI never opens it."));
4477
+ lines.push(detail("Or from this terminal \u2192 npx @patchstack/connect claim"));
4478
+ lines.push(detail(" (prints a link to sign in with, then attaches the site to that account)"));
4367
4479
  if (state.endpointOverride !== null) {
4368
4480
  lines.push(detail("(this URL inherits the endpoint override above)"));
4369
4481
  }
@@ -4406,7 +4518,12 @@ function renderGuideChecklist(state, useColor) {
4406
4518
  }
4407
4519
 
4408
4520
  // src/login.ts
4409
- import { mkdtempSync, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync7 } from "fs";
4521
+ import { mkdtempSync } from "fs";
4522
+ import { tmpdir as tmpdir2 } from "os";
4523
+ import path12 from "path";
4524
+
4525
+ // src/device-flow.ts
4526
+ import { readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync7 } from "fs";
4410
4527
  import { tmpdir } from "os";
4411
4528
  import path11 from "path";
4412
4529
  import { createHash as createHash2 } from "crypto";
@@ -4418,108 +4535,126 @@ function baseFrom(manifestEndpoint) {
4418
4535
  url.hash = "";
4419
4536
  return url.toString().replace(/\/$/, "");
4420
4537
  }
4421
- function pendingPath(siteUuid) {
4538
+ function pendingPath(siteUuid, intent) {
4422
4539
  const key = createHash2("sha256").update(siteUuid).digest("hex").slice(0, 16);
4423
- return path11.join(tmpdir(), `patchstack-login-${key}.json`);
4540
+ return path11.join(tmpdir(), `patchstack-${intent}-${key}.json`);
4424
4541
  }
4425
- function savePendingLogin(siteUuid, pending) {
4426
- writeFileSync7(pendingPath(siteUuid), JSON.stringify(pending), { encoding: "utf8", mode: 384 });
4542
+ function savePending(siteUuid, intent, pending) {
4543
+ writeFileSync7(pendingPath(siteUuid, intent), JSON.stringify(pending), {
4544
+ encoding: "utf8",
4545
+ mode: 384
4546
+ });
4427
4547
  }
4428
- function readPendingLogin(siteUuid) {
4548
+ function readPending(siteUuid, intent) {
4429
4549
  try {
4430
- return JSON.parse(readFileSync7(pendingPath(siteUuid), "utf8"));
4550
+ return JSON.parse(readFileSync7(pendingPath(siteUuid, intent), "utf8"));
4431
4551
  } catch {
4432
4552
  return null;
4433
4553
  }
4434
4554
  }
4435
- function clearPendingLogin(siteUuid) {
4555
+ function clearPending(siteUuid, intent) {
4436
4556
  try {
4437
- rmSync(pendingPath(siteUuid));
4557
+ rmSync(pendingPath(siteUuid, intent));
4438
4558
  } catch {
4439
4559
  }
4440
4560
  }
4441
- async function startLogin(config, deps = {}) {
4561
+ async function requestDeviceCode(config, intent, conflictMessage, deps = {}) {
4442
4562
  const fetchImpl = deps.fetchImpl ?? fetch;
4443
4563
  const now = deps.now ?? (() => Date.now());
4444
4564
  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
4565
  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."
4566
+ status: "failed",
4567
+ message: "No site UUID configured \u2014 run `patchstack-connect scan` first."
4457
4568
  };
4458
4569
  }
4570
+ const base = baseFrom(config.endpoint);
4571
+ const started = await pulseFetch(
4572
+ config,
4573
+ `${base}/device/code`,
4574
+ {
4575
+ method: "POST",
4576
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
4577
+ body: JSON.stringify({ site_uuid: config.siteUuid, intent })
4578
+ },
4579
+ fetchImpl
4580
+ );
4581
+ if (started.status === 409) return { status: "conflict", message: conflictMessage };
4459
4582
  if (started.status === 404) {
4460
4583
  return { status: "not-found", message: "Patchstack does not recognise this site UUID." };
4461
4584
  }
4462
4585
  if (!started.ok) {
4463
- return { status: "failed", message: `Could not start the login (HTTP ${started.status}).` };
4586
+ return { status: "failed", message: `Could not start the request (HTTP ${started.status}).` };
4464
4587
  }
4465
4588
  const body = await started.json();
4466
4589
  const pending = {
4467
4590
  deviceCode: body.device_code,
4468
4591
  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.
4592
+ // Points at the API, which redirects to the dashboard SPA — the CLI only knows the API origin,
4593
+ // and the approval page lives on the app.
4471
4594
  verificationUri: `${base}/device?code=${encodeURIComponent(body.user_code)}`,
4472
4595
  expiresAt: now() + body.expires_in * 1e3,
4473
4596
  intervalMs: Math.max(1, body.interval) * 1e3
4474
4597
  };
4475
- savePendingLogin(config.siteUuid, pending);
4598
+ savePending(config.siteUuid, intent, pending);
4476
4599
  return { status: "started", pending };
4477
4600
  }
4478
- async function redeemIfApproved(config, pending, deps = {}) {
4601
+ async function pollDeviceToken(config, intent, pending, deps = {}) {
4479
4602
  const fetchImpl = deps.fetchImpl ?? fetch;
4480
4603
  const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
4481
4604
  method: "POST",
4482
4605
  headers: { "Content-Type": "application/json", Accept: "application/json" },
4483
4606
  body: JSON.stringify({ device_code: pending.deviceCode })
4484
4607
  });
4485
- if (polled.status === 428) return "pending";
4608
+ if (polled.status === 428) return { state: "pending" };
4486
4609
  if (!polled.ok) {
4487
- if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
4488
- return "expired";
4610
+ if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
4611
+ return { state: "expired" };
4489
4612
  }
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";
4613
+ const body = await polled.json();
4614
+ if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
4615
+ return { state: "approved", body };
4495
4616
  }
4496
- async function waitForApproval(config, pending, deps = {}) {
4497
- const fetchImpl = deps.fetchImpl ?? fetch;
4617
+ async function pollUntilApproved(config, intent, pending, deps = {}) {
4498
4618
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4499
4619
  const now = deps.now ?? (() => Date.now());
4500
4620
  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
4621
  while (now() < deadline) {
4503
4622
  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 };
4623
+ const outcome = await pollDeviceToken(config, intent, pending, deps);
4624
+ if (outcome.state !== "pending") return outcome;
4521
4625
  }
4522
- return expired;
4626
+ return { state: "expired" };
4627
+ }
4628
+
4629
+ // src/login.ts
4630
+ 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.";
4631
+ function readPendingLogin(siteUuid) {
4632
+ return readPending(siteUuid, "login");
4633
+ }
4634
+ async function startLogin(config, deps = {}) {
4635
+ const started = await requestDeviceCode(config, "login", UNCLAIMED, deps);
4636
+ if (started.status === "conflict") return { status: "unclaimed", message: started.message };
4637
+ return started;
4638
+ }
4639
+ async function redeemIfApproved(config, pending, deps = {}) {
4640
+ const polled = await pollDeviceToken(config, "login", pending, deps);
4641
+ if (polled.state !== "approved") return polled.state;
4642
+ const apiKey = polled.body?.api_key;
4643
+ if (typeof apiKey !== "string" || apiKey.length === 0) return "expired";
4644
+ await persistApiKey(process.cwd(), apiKey);
4645
+ return "approved";
4646
+ }
4647
+ async function waitForApproval(config, pending, deps = {}) {
4648
+ const polled = await pollUntilApproved(config, "login", pending, deps);
4649
+ if (polled.state === "expired") {
4650
+ return { status: "expired", message: "The login request expired. Run the command again." };
4651
+ }
4652
+ const apiKey = polled.body?.api_key;
4653
+ if (typeof apiKey !== "string" || apiKey.length === 0) {
4654
+ return { status: "failed", message: "Patchstack approved the request but returned no credential." };
4655
+ }
4656
+ await persistApiKey(process.cwd(), apiKey);
4657
+ return { status: "approved", userCode: pending.userCode, verificationUri: pending.verificationUri };
4523
4658
  }
4524
4659
  async function login(config, onPrompt, deps = {}) {
4525
4660
  const started = await startLogin(config, deps);
@@ -4530,16 +4665,546 @@ async function login(config, onPrompt, deps = {}) {
4530
4665
  return waitForApproval(config, started.pending, deps);
4531
4666
  }
4532
4667
 
4668
+ // src/claim.ts
4669
+ 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.";
4670
+ function readPendingClaim(siteUuid) {
4671
+ return readPending(siteUuid, "claim");
4672
+ }
4673
+ async function startClaim(config, deps = {}) {
4674
+ const started = await requestDeviceCode(config, "claim", ALREADY_CLAIMED, deps);
4675
+ if (started.status === "conflict") return { status: "already-claimed", message: started.message };
4676
+ return started;
4677
+ }
4678
+ async function settle(body) {
4679
+ const account = typeof body?.account === "string" ? body.account : void 0;
4680
+ const dashboardUrl = typeof body?.dashboard_url === "string" ? body.dashboard_url : void 0;
4681
+ const apiKey = body?.api_key;
4682
+ let credentialSaved = false;
4683
+ if (typeof apiKey === "string" && apiKey.length > 0) {
4684
+ await persistApiKey(process.cwd(), apiKey);
4685
+ credentialSaved = true;
4686
+ }
4687
+ return { status: "claimed", account, dashboardUrl, credentialSaved };
4688
+ }
4689
+ async function redeemClaimIfApproved(config, pending, deps = {}) {
4690
+ const polled = await pollDeviceToken(config, "claim", pending, deps);
4691
+ if (polled.state === "pending") return "pending";
4692
+ if (polled.state === "expired") {
4693
+ return { status: "expired", message: "The claim request expired. Run the command again." };
4694
+ }
4695
+ return settle(polled.body);
4696
+ }
4697
+ async function waitForClaim(config, pending, deps = {}) {
4698
+ const polled = await pollUntilApproved(config, "claim", pending, deps);
4699
+ if (polled.state === "expired") {
4700
+ return { status: "expired", message: "The claim request expired. Run the command again." };
4701
+ }
4702
+ return settle(polled.body);
4703
+ }
4704
+ async function claim2(config, onPrompt, deps = {}) {
4705
+ const started = await startClaim(config, deps);
4706
+ if (started.status !== "started" || started.pending === void 0) {
4707
+ return {
4708
+ status: started.status === "started" ? "failed" : started.status,
4709
+ message: started.message
4710
+ };
4711
+ }
4712
+ onPrompt(started.pending.userCode, started.pending.verificationUri);
4713
+ return waitForClaim(config, started.pending, deps);
4714
+ }
4715
+
4716
+ // src/protect/install/runtime/check.ts
4717
+ import { relative as relative2 } from "path";
4718
+
4719
+ // src/protect/install/runtime/entry.ts
4720
+ import { existsSync as existsSync13, readFileSync as readFileSync8, realpathSync, statSync as statSync3 } from "fs";
4721
+ import { join as join13, resolve as resolve3, sep } from "path";
4722
+ var CONVENTIONAL = [
4723
+ "server.js",
4724
+ "server.mjs",
4725
+ "server.cjs",
4726
+ "app.js",
4727
+ "app.mjs",
4728
+ "app.cjs",
4729
+ "index.js",
4730
+ "index.mjs",
4731
+ "index.cjs",
4732
+ "src/server.js",
4733
+ "src/server.mjs",
4734
+ "src/server.cjs",
4735
+ "src/index.js",
4736
+ "src/index.mjs",
4737
+ "src/index.cjs",
4738
+ "dist/server.js",
4739
+ "dist/index.js",
4740
+ "dist/main.js",
4741
+ "build/index.js",
4742
+ "build/server.js"
4743
+ ];
4744
+ var LOADABLE = /\.(?:js|mjs|cjs)$/;
4745
+ function readPackage2(cwd) {
4746
+ try {
4747
+ const parsed = JSON.parse(readFileSync8(join13(cwd, "package.json"), "utf8"));
4748
+ return parsed !== null && typeof parsed === "object" ? parsed : null;
4749
+ } catch {
4750
+ return null;
4751
+ }
4752
+ }
4753
+ function isFile(path14) {
4754
+ try {
4755
+ return statSync3(path14).isFile();
4756
+ } catch {
4757
+ return false;
4758
+ }
4759
+ }
4760
+ function loadable(cwd, candidate) {
4761
+ const written = resolve3(cwd, candidate);
4762
+ if (!LOADABLE.test(written) || !isFile(written)) return null;
4763
+ let root;
4764
+ let path14;
4765
+ try {
4766
+ root = realpathSync(cwd);
4767
+ path14 = realpathSync(written);
4768
+ } catch {
4769
+ return null;
4770
+ }
4771
+ if (path14 !== root && !path14.startsWith(root + sep)) return null;
4772
+ return written;
4773
+ }
4774
+ function passable(token) {
4775
+ if (CODE_LOADING_FLAGS.has(flagName(token))) return false;
4776
+ if (NO_OPERAND_FLAGS.has(token)) return true;
4777
+ return token.includes("=") && VALUED_FLAGS.has(flagName(token));
4778
+ }
4779
+ function fromScript(cwd, script) {
4780
+ if (/[&|;><()#$`'"*?\[\]{}\\~\r\n]/.test(script)) return null;
4781
+ const parts = script.trim().split(/\s+/);
4782
+ if (parts[0] !== "node") return null;
4783
+ const nodeArgs = [];
4784
+ let rest = parts.slice(1);
4785
+ while (rest.length > 0 && rest[0].startsWith("-")) {
4786
+ const token = rest[0];
4787
+ if (!passable(token)) return null;
4788
+ nodeArgs.push(token);
4789
+ rest = rest.slice(1);
4790
+ }
4791
+ if (rest.length === 0) return null;
4792
+ const file = loadable(cwd, rest[0]);
4793
+ return file === null ? null : { file, nodeArgs, appArgs: rest.slice(1) };
4794
+ }
4795
+ function resolveEntry(cwd) {
4796
+ const pkg = readPackage2(cwd);
4797
+ const scripts = pkg?.scripts ?? null;
4798
+ const declared = ["start", "serve"].map((name) => ({ name, script: scripts?.[name] })).find((entry) => typeof entry.script === "string" && entry.script.trim() !== "");
4799
+ if (declared !== void 0) {
4800
+ const resolved = fromScript(cwd, declared.script);
4801
+ return resolved === null ? {
4802
+ kind: "unavailable",
4803
+ reason: `the "${declared.name}" script is outside the bounded \`node <file>\` form this check can launch without the project's own toolchain`
4804
+ } : { kind: "entry", ...resolved, from: `the "${declared.name}" script` };
4805
+ }
4806
+ const main2 = pkg?.main;
4807
+ if (typeof main2 === "string") {
4808
+ const file = loadable(cwd, main2);
4809
+ if (file !== null) return { kind: "entry", file, nodeArgs: [], appArgs: [], from: 'the package "main" field' };
4810
+ }
4811
+ for (const candidate of CONVENTIONAL) {
4812
+ const file = loadable(cwd, candidate);
4813
+ if (file !== null) return { kind: "entry", file, nodeArgs: [], appArgs: [], from: candidate };
4814
+ }
4815
+ return {
4816
+ kind: "unavailable",
4817
+ 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"
4818
+ };
4819
+ }
4820
+
4821
+ // src/protect/install/runtime/probe.ts
4822
+ import { spawn } from "child_process";
4823
+ import { createHash as createHash3, randomBytes } from "crypto";
4824
+ import { existsSync as existsSync14 } from "fs";
4825
+ import { request as httpRequest } from "http";
4826
+ import { request as httpsRequest, Agent as HttpsAgent } from "https";
4827
+ import { fileURLToPath as fileURLToPath2 } from "url";
4828
+ var CHALLENGE_BYTES = 32;
4829
+ var VERIFY_HEADER_NAME = "x-patchstack-verify";
4830
+ var CHALLENGE_ENV = "PATCHSTACK_VERIFY_CHALLENGE";
4831
+ var SETTLE_MS = 1500;
4832
+ var RUN_MS = 2e4;
4833
+ var PROBE_MS = 5e3;
4834
+ var FREEZE_MS = 500;
4835
+ var OUTPUT_LIMIT = 4096;
4836
+ var REDACTION_OVERLAP = 256;
4837
+ var ANSWER_LIMIT = 8192;
4838
+ var LOOPBACK = ["127.0.0.1", "::1"];
4839
+ var ENDING_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
4840
+ function expectedAnswerFor(challenge) {
4841
+ return createHash3("sha256").update(`patchstack-verify:${challenge}`).digest("hex");
4842
+ }
4843
+ function preloadPath() {
4844
+ const candidates = [
4845
+ new URL("./protect/runtime/report-listeners.cjs", import.meta.url),
4846
+ // beside the built CLI
4847
+ new URL("./report-listeners.cjs", import.meta.url)
4848
+ // beside this file, in the repository
4849
+ ];
4850
+ for (const candidate of candidates) {
4851
+ const path14 = fileURLToPath2(candidate);
4852
+ if (existsSync14(path14)) return path14;
4853
+ }
4854
+ return null;
4855
+ }
4856
+ function propagatedNodeOptions(preload, existing) {
4857
+ if (preload.includes('"')) {
4858
+ return {
4859
+ kind: "unavailable",
4860
+ 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"
4861
+ };
4862
+ }
4863
+ const ours = `--require "${preload}"`;
4864
+ if (existing === void 0 || existing.trim() === "") return { kind: "options", value: ours };
4865
+ const verdict = classifyNodeOptions(existing);
4866
+ return verdict.kind === "refused" ? { kind: "unavailable", reason: verdict.why } : { kind: "options", value: `${ours} ${existing}` };
4867
+ }
4868
+ function keptOutput(chunks, challenge, answer) {
4869
+ const text = chunks.join("").split(challenge).join("<challenge>").split(answer).join("<answer>");
4870
+ return text.slice(0, OUTPUT_LIMIT);
4871
+ }
4872
+ async function askListener(listener, challenge, answer, budgetMs) {
4873
+ if (budgetMs <= 0) {
4874
+ return { ...listener, outcome: "unreachable", detail: "the run ran out of time before it could be asked" };
4875
+ }
4876
+ const send = listener.scheme === "https" ? httpsRequest : httpRequest;
4877
+ const options = {
4878
+ host: listener.host,
4879
+ port: listener.port,
4880
+ path: "/",
4881
+ method: "GET",
4882
+ headers: { [VERIFY_HEADER_NAME]: challenge },
4883
+ timeout: budgetMs
4884
+ };
4885
+ if (listener.scheme === "https") options.agent = new HttpsAgent({ rejectUnauthorized: false });
4886
+ return new Promise((resolve4) => {
4887
+ const req = send(options, (res) => {
4888
+ const body = [];
4889
+ let read2 = 0;
4890
+ res.on("data", (chunk) => {
4891
+ read2 += chunk.length;
4892
+ if (read2 <= ANSWER_LIMIT) {
4893
+ body.push(chunk);
4894
+ return;
4895
+ }
4896
+ res.destroy();
4897
+ });
4898
+ const finish = () => {
4899
+ const text = Buffer.concat(body).toString("utf8").trim();
4900
+ resolve4({
4901
+ ...listener,
4902
+ outcome: text === answer ? "traversed" : "answered-without-sentinel",
4903
+ detail: text === answer ? void 0 : `answered with ${res.statusCode ?? "?"} and no sentinel`
4904
+ });
4905
+ };
4906
+ res.on("end", finish);
4907
+ res.on("close", finish);
4908
+ });
4909
+ req.on("timeout", () => req.destroy(new Error("timed out")));
4910
+ req.on("error", (err) => resolve4({ ...listener, outcome: "unreachable", detail: err.message }));
4911
+ req.end();
4912
+ });
4913
+ }
4914
+ function verdictOf(listeners) {
4915
+ if (listeners.length === 0 || listeners.some((l) => l.outcome === "unreachable")) return "unavailable";
4916
+ if (listeners.some((l) => l.outcome === "answered-without-sentinel")) return "not-traversed";
4917
+ return "proven";
4918
+ }
4919
+ function terminator(child) {
4920
+ let done = false;
4921
+ return () => {
4922
+ if (done) return;
4923
+ done = true;
4924
+ try {
4925
+ if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
4926
+ } catch {
4927
+ try {
4928
+ child.kill("SIGKILL");
4929
+ } catch {
4930
+ }
4931
+ }
4932
+ };
4933
+ }
4934
+ async function probeRuntimeTraversal(options) {
4935
+ const platform = options.platform ?? process.platform;
4936
+ const challenge = randomBytes(CHALLENGE_BYTES).toString("hex");
4937
+ const answer = expectedAnswerFor(challenge);
4938
+ const nothing = (reason) => ({ outcome: "unavailable", reason, listeners: [], output: "" });
4939
+ if (platform === "win32") return nothing("process-tree cleanup is not implemented on Windows");
4940
+ const preload = options.preload ?? preloadPath();
4941
+ if (preload === null) return nothing("the listener reporter is missing from this installation");
4942
+ const deadline = Date.now() + (options.timeoutMs ?? RUN_MS);
4943
+ const remaining = () => Math.max(0, deadline - Date.now());
4944
+ const propagation = propagatedNodeOptions(preload, process.env.NODE_OPTIONS);
4945
+ if (propagation.kind === "unavailable") return nothing(propagation.reason);
4946
+ let detachMessages = () => {
4947
+ };
4948
+ const child = spawn(process.execPath, [...options.nodeArgs ?? [], "--require", preload, options.entry, ...options.appArgs ?? []], {
4949
+ cwd: options.cwd,
4950
+ detached: true,
4951
+ // its own process group, so cleanup has one stable target
4952
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
4953
+ // The app's own environment, plus the challenge and the propagated reporter. `NODE_ENV` is
4954
+ // deliberately left alone: choosing one here would change which code path the app takes, and the
4955
+ // point is to verify the app as it is.
4956
+ env: {
4957
+ ...process.env,
4958
+ [CHALLENGE_ENV]: challenge,
4959
+ // Replaced rather than added to: what may be inherited was decided above, and the reporter is
4960
+ // first in the value that goes out.
4961
+ NODE_OPTIONS: propagation.value
4962
+ }
4963
+ });
4964
+ const terminate = terminator(child);
4965
+ const onSignal = (signal) => {
4966
+ terminate();
4967
+ detachHandlers();
4968
+ if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
4969
+ };
4970
+ const onExit = () => terminate();
4971
+ const detachHandlers = () => {
4972
+ for (const signal of ENDING_SIGNALS) process.removeListener(signal, onSignal);
4973
+ process.removeListener("exit", onExit);
4974
+ };
4975
+ for (const signal of ENDING_SIGNALS) process.prependListener(signal, onSignal);
4976
+ process.on("exit", onExit);
4977
+ try {
4978
+ const chunks = [];
4979
+ let kept = 0;
4980
+ const drain = (stream) => {
4981
+ stream?.setEncoding("utf8");
4982
+ stream?.on("data", (chunk) => {
4983
+ if (kept < OUTPUT_LIMIT + REDACTION_OVERLAP) {
4984
+ chunks.push(chunk);
4985
+ kept += chunk.length;
4986
+ }
4987
+ });
4988
+ };
4989
+ drain(child.stdout);
4990
+ drain(child.stderr);
4991
+ const observed = [];
4992
+ const unsupported = [];
4993
+ const created = [];
4994
+ const workers = [];
4995
+ const late = [];
4996
+ const escaped = [];
4997
+ let loaded = false;
4998
+ let startFailure = null;
4999
+ let exitHow = null;
5000
+ let discovering = true;
5001
+ let onFrozen = null;
5002
+ const freeze = async () => {
5003
+ if (!child.connected) return false;
5004
+ return new Promise((resolve4) => {
5005
+ const settle2 = (confirmed2) => {
5006
+ clearTimeout(timer);
5007
+ onFrozen = null;
5008
+ child.removeListener("exit", onGone);
5009
+ resolve4(confirmed2);
5010
+ };
5011
+ const onGone = () => settle2(false);
5012
+ const timer = setTimeout(() => settle2(false), Math.min(FREEZE_MS, remaining()));
5013
+ onFrozen = () => settle2(true);
5014
+ child.once("exit", onGone);
5015
+ try {
5016
+ child.send({ patchstackVerify: "freeze" });
5017
+ } catch {
5018
+ settle2(false);
5019
+ }
5020
+ });
5021
+ };
5022
+ await new Promise((resolve4) => {
5023
+ let settleTimer = null;
5024
+ const done = () => {
5025
+ discovering = false;
5026
+ clearTimeout(runTimer);
5027
+ if (settleTimer) clearTimeout(settleTimer);
5028
+ resolve4();
5029
+ };
5030
+ const runTimer = setTimeout(done, remaining());
5031
+ const settle2 = () => {
5032
+ if (settleTimer) clearTimeout(settleTimer);
5033
+ settleTimer = setTimeout(done, Math.min(options.settleMs ?? SETTLE_MS, remaining()));
5034
+ };
5035
+ const onMessage = (message) => {
5036
+ const note = message;
5037
+ if (note?.patchstackVerify === "frozen") {
5038
+ onFrozen?.();
5039
+ return;
5040
+ }
5041
+ if (note?.patchstackVerify === "process-created" && typeof note.escape === "string") escaped.push(`${note.what ?? "a process"} ${note.escape}`);
5042
+ if (note?.patchstackVerify === "worker-created" && typeof note.escape === "string") escaped.push(`a worker thread that ${note.escape}`);
5043
+ if (!discovering) {
5044
+ const kind = note?.patchstackVerify;
5045
+ if (kind === "listener") late.push(`an ${note.scheme === "https" ? "HTTPS" : "HTTP"} listener on port ${Number(note.port)}`);
5046
+ else if (kind === "unsupported-listener") late.push(`a listener this cannot probe (${note.why ?? "an unsupported transport"})`);
5047
+ else if (kind === "process-created") late.push(`another process (${note.what ?? "a process"} via ${note.via ?? "child_process"})`);
5048
+ else if (kind === "worker-created") late.push(`a worker thread (${note.what ?? "a worker"})`);
5049
+ return;
5050
+ }
5051
+ if (note?.patchstackVerify === "listener" && (note.scheme === "http" || note.scheme === "https")) {
5052
+ observed.push({ scheme: note.scheme, host: String(note.host), port: Number(note.port) });
5053
+ settle2();
5054
+ }
5055
+ if (note?.patchstackVerify === "unsupported-listener") {
5056
+ unsupported.push(note.why ?? "an unsupported transport");
5057
+ done();
5058
+ }
5059
+ if (note?.patchstackVerify === "process-created") {
5060
+ created.push(
5061
+ `${note.what ?? "a process"} (via ${note.via ?? "child_process"})${note.node === true ? "" : ", which the listener reporter cannot reach"}`
5062
+ );
5063
+ done();
5064
+ }
5065
+ if (note?.patchstackVerify === "worker-created") {
5066
+ workers.push(note.what ?? "a worker");
5067
+ done();
5068
+ }
5069
+ if (note?.patchstackVerify === "ready") loaded = true;
5070
+ };
5071
+ child.on("message", onMessage);
5072
+ detachMessages = () => child.removeListener("message", onMessage);
5073
+ child.on("error", (err) => {
5074
+ startFailure = `the app could not be started (${err.message})`;
5075
+ done();
5076
+ });
5077
+ child.on("exit", (code, signal) => {
5078
+ exitHow = signal ?? `code ${code}`;
5079
+ done();
5080
+ });
5081
+ });
5082
+ const offLoopback = observed.filter((l) => !LOOPBACK.includes(l.host));
5083
+ const contained = unsupported.length === 0 && created.length === 0 && workers.length === 0 && offLoopback.length === 0;
5084
+ const listeners = contained ? await Promise.all(observed.map((l) => askListener(l, challenge, answer, Math.min(PROBE_MS, remaining())))) : [];
5085
+ const confirmed = await freeze();
5086
+ const output = keptOutput(chunks, challenge, answer);
5087
+ const pid = child.pid;
5088
+ 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})`);
5089
+ if (escaped.length > 0) {
5090
+ return {
5091
+ outcome: "unavailable",
5092
+ reason: `the app tried to start something outside this run's one-process scope, and it was refused: ${escaped.join("; ")}`,
5093
+ listeners: [],
5094
+ output,
5095
+ pid
5096
+ };
5097
+ }
5098
+ if (unsupported.length > 0) {
5099
+ return { outcome: "unavailable", reason: `a listener this cannot probe: ${unsupported.join("; ")}`, listeners: [], output, pid };
5100
+ }
5101
+ if (created.length > 0) {
5102
+ return {
5103
+ outcome: "unavailable",
5104
+ reason: `the app attempted to start another process, so this run cannot answer for the whole app: ${created.join("; ")}`,
5105
+ listeners: [],
5106
+ output,
5107
+ pid
5108
+ };
5109
+ }
5110
+ if (workers.length > 0) {
5111
+ return {
5112
+ outcome: "unavailable",
5113
+ reason: `the app started a worker thread, whose listeners cannot report back to this run: ${workers.join("; ")}`,
5114
+ listeners: [],
5115
+ output,
5116
+ pid
5117
+ };
5118
+ }
5119
+ if (offLoopback.length > 0) {
5120
+ return {
5121
+ outcome: "unavailable",
5122
+ reason: `a listener bound an address this verification must not open: ${offLoopback.map((l) => l.host).join("; ")}`,
5123
+ listeners: [],
5124
+ output,
5125
+ pid
5126
+ };
5127
+ }
5128
+ if (late.length > 0) {
5129
+ return {
5130
+ outcome: "unavailable",
5131
+ reason: `the app was still opening things after discovery had ended, so this run cannot answer for the whole app: ${late.join("; ")}`,
5132
+ listeners: [],
5133
+ output,
5134
+ pid
5135
+ };
5136
+ }
5137
+ if (observed.length === 0) {
5138
+ if (exitNote !== null) return { outcome: "unavailable", reason: exitNote, listeners: [], output, pid };
5139
+ if (!loaded) {
5140
+ return { outcome: "unavailable", reason: "the listener reporter did not load in the app process", listeners: [], output, pid };
5141
+ }
5142
+ return { outcome: "unavailable", reason: "no HTTP listener was observed while starting up", listeners: [], output, pid };
5143
+ }
5144
+ const unconfirmed = !confirmed && loaded && exitHow === null;
5145
+ const outcome = unconfirmed && verdictOf(listeners) === "proven" ? "unavailable" : verdictOf(listeners);
5146
+ 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;
5147
+ return { outcome, reason, listeners, output, pid };
5148
+ } finally {
5149
+ terminate();
5150
+ detachHandlers();
5151
+ detachMessages();
5152
+ }
5153
+ }
5154
+
5155
+ // src/protect/install/runtime/check.ts
5156
+ async function runRuntimeCheck(cwd, options = {}) {
5157
+ const entry = resolveEntry(cwd);
5158
+ if (entry.kind === "unavailable") return { outcome: "unavailable", reason: entry.reason, listeners: [], output: "" };
5159
+ const result = await probeRuntimeTraversal({
5160
+ cwd,
5161
+ entry: entry.file,
5162
+ nodeArgs: entry.nodeArgs,
5163
+ appArgs: entry.appArgs,
5164
+ timeoutMs: options.timeoutMs,
5165
+ settleMs: options.settleMs,
5166
+ platform: options.platform
5167
+ });
5168
+ return { ...result, entry: { file: relative2(cwd, entry.file).replace(/\\/g, "/"), from: entry.from } };
5169
+ }
5170
+ function runtimeExitCode(report) {
5171
+ if (report.outcome === "proven") return 0;
5172
+ return report.outcome === "not-traversed" ? 1 : 2;
5173
+ }
5174
+ function formatRuntimeCheck(report) {
5175
+ const lines = ["runtime traversal (--runtime):"];
5176
+ if (report.entry) lines.push(` entry: ${report.entry.file} (from ${report.entry.from})`);
5177
+ for (const listener of report.listeners) {
5178
+ const mark = listener.outcome === "traversed" ? "\u2713" : listener.outcome === "answered-without-sentinel" ? "\u2717" : "?";
5179
+ 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";
5180
+ const authority = listener.host.includes(":") ? `[${listener.host}]` : listener.host;
5181
+ lines.push(` ${mark} ${listener.scheme}://${authority}:${listener.port} \u2014 ${note}`);
5182
+ }
5183
+ if (report.outcome === "proven") {
5184
+ lines.push("runtime traversal reached the scaffolded guard seam \u2713");
5185
+ } else if (report.outcome === "not-traversed") {
5186
+ lines.push("runtime traversal did NOT reach the scaffolded guard seam \u2717");
5187
+ } else {
5188
+ lines.push(`? runtime traversal could not be established \u2014 ${report.reason ?? "no reason given"}`);
5189
+ lines.push(" This is not a failure. The structural checks above still stand on their own.");
5190
+ }
5191
+ if (report.outcome !== "proven" && report.output.trim() !== "") {
5192
+ lines.push(" the app printed:");
5193
+ for (const line of report.output.trim().split("\n").slice(0, 20)) lines.push(` ${line}`);
5194
+ }
5195
+ return lines;
5196
+ }
5197
+
4533
5198
  // src/map-command.ts
4534
5199
  import { writeFileSync as writeFileSync8 } from "fs";
4535
5200
 
4536
5201
  // src/map/ts-loader.ts
4537
5202
  import { createRequire } from "module";
4538
5203
  import { pathToFileURL } from "url";
4539
- import { join as join13 } from "path";
5204
+ import { join as join14 } from "path";
4540
5205
  async function loadTypeScript(cwd) {
4541
5206
  try {
4542
- const req = createRequire(pathToFileURL(join13(cwd, "package.json")));
5207
+ const req = createRequire(pathToFileURL(join14(cwd, "package.json")));
4543
5208
  const resolved = req.resolve("typescript");
4544
5209
  const mod = await import(pathToFileURL(resolved).href);
4545
5210
  return mod.default ?? mod;
@@ -4554,9 +5219,9 @@ async function loadTypeScript(cwd) {
4554
5219
  }
4555
5220
 
4556
5221
  // 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";
5222
+ import { readFileSync as readFileSync13, realpathSync as realpathSync4 } from "fs";
5223
+ import { createHash as createHash5 } from "crypto";
5224
+ import { relative as relative5 } from "path";
4560
5225
 
4561
5226
  // src/map/ast.ts
4562
5227
  function rootIdentifier(node, ts) {
@@ -4810,8 +5475,8 @@ function npmPackageOf(spec) {
4810
5475
  }
4811
5476
 
4812
5477
  // 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";
5478
+ import { readFileSync as readFileSync9, readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
5479
+ import { join as join15, relative as relative3, isAbsolute } from "path";
4815
5480
 
4816
5481
  // src/map/routes.ts
4817
5482
  var ROUTE_REGISTER_NAMES = ["get", "post", "put", "patch", "delete", "options", "all", "head", "use"];
@@ -4895,7 +5560,7 @@ function hasEntrySignal(text) {
4895
5560
  }
4896
5561
  function detectFramework(cwd) {
4897
5562
  try {
4898
- const pkg = JSON.parse(readFileSync8(join14(cwd, "package.json"), "utf8"));
5563
+ const pkg = JSON.parse(readFileSync9(join15(cwd, "package.json"), "utf8"));
4899
5564
  const d = { ...pkg.dependencies, ...pkg.devDependencies };
4900
5565
  if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
4901
5566
  if (d["next"]) return "next";
@@ -4907,11 +5572,11 @@ function detectFramework(cwd) {
4907
5572
  } catch {
4908
5573
  }
4909
5574
  try {
4910
- if (statSync3(join14(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
5575
+ if (statSync4(join15(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
4911
5576
  } catch {
4912
5577
  }
4913
5578
  try {
4914
- if (statSync3(join14(cwd, "functions")).isDirectory()) return "deno-functions";
5579
+ if (statSync4(join15(cwd, "functions")).isDirectory()) return "deno-functions";
4915
5580
  } catch {
4916
5581
  }
4917
5582
  return "unknown";
@@ -4953,7 +5618,7 @@ function probeForSource(dir, budget) {
4953
5618
  for (const e of entries) {
4954
5619
  if (budget.left-- <= 0) return "unknown";
4955
5620
  if (e.isDirectory()) {
4956
- if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join14(dir, e.name));
5621
+ if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join15(dir, e.name));
4957
5622
  } else if (isSourceFile(e.name)) return true;
4958
5623
  else if (e.isSymbolicLink()) return "unknown";
4959
5624
  }
@@ -4966,7 +5631,7 @@ function probeForSource(dir, budget) {
4966
5631
  function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ new Set(), stats = { discovered: 0, unwalked: 0, skippedWithSource: [], skippedUnknown: 0 }, probeBudget = { left: PROBE_BUDGET }) {
4967
5632
  let key;
4968
5633
  try {
4969
- key = realpathSync(dir);
5634
+ key = realpathSync2(dir);
4970
5635
  } catch {
4971
5636
  stats.unwalked++;
4972
5637
  return out;
@@ -4990,19 +5655,19 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
4990
5655
  const probeable = !SKIP_IS_DEFINITIONAL.has(e.name);
4991
5656
  if (probeable && !e.isDirectory() && e.isSymbolicLink()) stats.skippedUnknown++;
4992
5657
  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));
5658
+ const held = probeForSource(join15(dir, e.name), probeBudget);
5659
+ if (held === true) stats.skippedWithSource.push(join15(dir, e.name));
4995
5660
  else if (held === "unknown") stats.skippedUnknown++;
4996
5661
  }
4997
5662
  continue;
4998
5663
  }
4999
- const full = join14(dir, e.name);
5664
+ const full = join15(dir, e.name);
5000
5665
  if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
5001
5666
  else if (e.isSymbolicLink()) {
5002
5667
  let st, real;
5003
5668
  try {
5004
- st = statSync3(full);
5005
- real = realpathSync(full);
5669
+ st = statSync4(full);
5670
+ real = realpathSync2(full);
5006
5671
  } catch {
5007
5672
  stats.unwalked++;
5008
5673
  continue;
@@ -5025,7 +5690,7 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
5025
5690
  }
5026
5691
  function isInside(candidate, boundary) {
5027
5692
  if (candidate === boundary) return true;
5028
- const rel = relative2(boundary, candidate);
5693
+ const rel = relative3(boundary, candidate);
5029
5694
  return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
5030
5695
  }
5031
5696
  var DEPLOYMENT_SHAPES = [
@@ -5052,13 +5717,13 @@ var DEPLOYMENT_SHAPES = [
5052
5717
  function detectDeploymentShapes(cwd, opts = {}) {
5053
5718
  let boundary = opts.boundary ?? cwd;
5054
5719
  try {
5055
- boundary = realpathSync(boundary);
5720
+ boundary = realpathSync2(boundary);
5056
5721
  } catch {
5057
5722
  }
5058
- const inProject = (path13) => {
5723
+ const inProject = (path14) => {
5059
5724
  if (opts.followOutside) return true;
5060
5725
  try {
5061
- return isInside(realpathSync(path13), boundary);
5726
+ return isInside(realpathSync2(path14), boundary);
5062
5727
  } catch {
5063
5728
  return false;
5064
5729
  }
@@ -5066,9 +5731,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
5066
5731
  const found = [];
5067
5732
  for (const candidate of DEPLOYMENT_SHAPES) {
5068
5733
  for (const file of candidate.files ?? []) {
5069
- const full = join14(cwd, file);
5734
+ const full = join15(cwd, file);
5070
5735
  try {
5071
- if (statSync3(full).isFile() && inProject(full)) {
5736
+ if (statSync4(full).isFile() && inProject(full)) {
5072
5737
  found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
5073
5738
  break;
5074
5739
  }
@@ -5076,9 +5741,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
5076
5741
  }
5077
5742
  }
5078
5743
  for (const dir of candidate.dirs ?? []) {
5079
- const full = join14(cwd, dir);
5744
+ const full = join15(cwd, dir);
5080
5745
  try {
5081
- if (!statSync3(full).isDirectory() || !inProject(full)) continue;
5746
+ if (!statSync4(full).isDirectory() || !inProject(full)) continue;
5082
5747
  } catch {
5083
5748
  continue;
5084
5749
  }
@@ -5095,7 +5760,7 @@ function holdsFunctionEntry(dir) {
5095
5760
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
5096
5761
  if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
5097
5762
  try {
5098
- for (const nested of readdirSync5(join14(dir, entry.name), { withFileTypes: true })) {
5763
+ for (const nested of readdirSync5(join15(dir, entry.name), { withFileTypes: true })) {
5099
5764
  if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
5100
5765
  }
5101
5766
  } catch {
@@ -5111,7 +5776,7 @@ function holdsSourceFile(dir) {
5111
5776
  if (entry.isFile() && isSourceFile(entry.name)) return true;
5112
5777
  if (entry.isDirectory()) {
5113
5778
  try {
5114
- for (const nested of readdirSync5(join14(dir, entry.name), { withFileTypes: true })) {
5779
+ for (const nested of readdirSync5(join15(dir, entry.name), { withFileTypes: true })) {
5115
5780
  if (nested.isFile() && isSourceFile(nested.name)) return true;
5116
5781
  }
5117
5782
  } catch {
@@ -5124,8 +5789,8 @@ function holdsSourceFile(dir) {
5124
5789
  }
5125
5790
 
5126
5791
  // src/map/surface.ts
5127
- import { readFileSync as readFileSync9 } from "fs";
5128
- import { join as join15 } from "path";
5792
+ import { readFileSync as readFileSync10 } from "fs";
5793
+ import { join as join16 } from "path";
5129
5794
  var STATIC_GENERATORS = [
5130
5795
  { dep: "vite", label: "vite" },
5131
5796
  { dep: "react-scripts", label: "create-react-app" },
@@ -5163,7 +5828,7 @@ var SHAPE_SIGNAL = {
5163
5828
  };
5164
5829
  function readManifest(cwd) {
5165
5830
  try {
5166
- const parsed = JSON.parse(readFileSync9(join15(cwd, "package.json"), "utf8"));
5831
+ const parsed = JSON.parse(readFileSync10(join16(cwd, "package.json"), "utf8"));
5167
5832
  return typeof parsed === "object" && parsed !== null ? parsed : null;
5168
5833
  } catch {
5169
5834
  return null;
@@ -5176,7 +5841,7 @@ function nextExportsStatically(cwd, manifest, ts) {
5176
5841
  for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
5177
5842
  let text;
5178
5843
  try {
5179
- text = readFileSync9(join15(cwd, file), "utf8");
5844
+ text = readFileSync10(join16(cwd, file), "utf8");
5180
5845
  } catch {
5181
5846
  continue;
5182
5847
  }
@@ -5304,7 +5969,7 @@ function surfaceNote(surface) {
5304
5969
  }
5305
5970
 
5306
5971
  // src/map/sinks.ts
5307
- import { createHash as createHash3 } from "crypto";
5972
+ import { createHash as createHash4 } from "crypto";
5308
5973
  var DB_OPS = /* @__PURE__ */ new Set(["insert", "update", "delete", "select", "upsert", "rpc"]);
5309
5974
  var PRISMA_OPS = /* @__PURE__ */ new Set(["create", "createMany", "update", "updateMany", "delete", "deleteMany", "upsert", "findFirst", "findUnique", "findMany", "count", "aggregate"]);
5310
5975
  var FS_CALLS = /^(readFile|writeFile|readFileSync|writeFileSync|appendFile|createReadStream|createWriteStream|unlink|rm|rmSync|mkdir|readdir|stat|open)$/;
@@ -5403,14 +6068,14 @@ function directSinks(node, ts, bindings, ctx) {
5403
6068
  const root = base ? rootIdentifier(base, ts) : void 0;
5404
6069
  if (!root) return {};
5405
6070
  const spec = bindings.resolve(root);
5406
- const relative5 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
6071
+ const relative6 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
5407
6072
  const pkg = npmPackageOf(spec);
5408
6073
  if (pkg) return { pkg, root, spec };
5409
- if (relative5 && ctx && spec) {
6074
+ if (relative6 && ctx && spec) {
5410
6075
  const viaModule = ctx.graph.importedBinding(ctx.file, spec, bindings.exportNameOf(root) ?? root);
5411
6076
  if (viaModule) return { pkg: viaModule.package, root, spec };
5412
6077
  }
5413
- return { local: bindings.locals.has(root), root, spec, relative: relative5 };
6078
+ return { local: bindings.locals.has(root), root, spec, relative: relative6 };
5414
6079
  };
5415
6080
  const attributionOf = (b, pkg) => b.pkg ? "import" : pkg ? "inferred" : void 0;
5416
6081
  const dbApi = (pkg, attribution) => {
@@ -5571,7 +6236,7 @@ function argumentRoleOf(sinkKind, method, index, total = 0) {
5571
6236
  return table?.[index] ?? "unknown";
5572
6237
  }
5573
6238
  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);
6239
+ 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
6240
  }
5576
6241
  function dedupeSinks(sinks, owner) {
5577
6242
  const seen = /* @__PURE__ */ new Set();
@@ -5587,8 +6252,8 @@ function dedupeSinks(sinks, owner) {
5587
6252
  }
5588
6253
 
5589
6254
  // 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";
6255
+ import { readFileSync as readFileSync11, realpathSync as realpathSync3, statSync as statSync5 } from "fs";
6256
+ import { dirname as dirname7, join as join17, relative as relative4, resolve as resolvePath } from "path";
5592
6257
  var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
5593
6258
  function createModuleGraph(ts, opts) {
5594
6259
  const cache = /* @__PURE__ */ new Map();
@@ -5596,7 +6261,7 @@ function createModuleGraph(ts, opts) {
5596
6261
  if (cache.has(file)) return cache.get(file) ?? null;
5597
6262
  let entry = null;
5598
6263
  try {
5599
- const text = readFileSync10(file, "utf8");
6264
+ const text = readFileSync11(file, "utf8");
5600
6265
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
5601
6266
  const bindings = buildModuleBindings(sf, ts);
5602
6267
  entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
@@ -5612,7 +6277,7 @@ function createModuleGraph(ts, opts) {
5612
6277
  if (!opts.followOutside) {
5613
6278
  let real = target;
5614
6279
  try {
5615
- real = realpathSync2(target);
6280
+ real = realpathSync3(target);
5616
6281
  } catch {
5617
6282
  }
5618
6283
  if (!isInside(real, opts.boundary)) return void 0;
@@ -5630,7 +6295,7 @@ function createModuleGraph(ts, opts) {
5630
6295
  for (const callee of mod.calleesOf.get(exportName) ?? []) {
5631
6296
  for (const s of mod.fnSinks.get(callee) ?? []) collected.push(s);
5632
6297
  }
5633
- const rel = relative3(opts.cwd, target);
6298
+ const rel = relative4(opts.cwd, target);
5634
6299
  return collected.map((s) => ({ ...s, file: rel }));
5635
6300
  },
5636
6301
  // A different question about the same module: not "what sinks are in there" but "what does this
@@ -5666,10 +6331,10 @@ function resolveRelativeModule(fromFile, spec) {
5666
6331
  }
5667
6332
  candidates.push(base);
5668
6333
  for (const e of RESOLVE_EXTS) candidates.push(base + e);
5669
- for (const e of RESOLVE_EXTS) candidates.push(join16(base, "index" + e));
6334
+ for (const e of RESOLVE_EXTS) candidates.push(join17(base, "index" + e));
5670
6335
  for (const c of candidates) {
5671
6336
  try {
5672
- if (statSync4(c).isFile()) return c;
6337
+ if (statSync5(c).isFile()) return c;
5673
6338
  } catch {
5674
6339
  }
5675
6340
  }
@@ -5695,8 +6360,8 @@ function collectCallees(sf, ts) {
5695
6360
  }
5696
6361
 
5697
6362
  // src/map/coordinates.ts
5698
- function runtimeCoordinate(source, path13) {
5699
- if (/\[\d*\]/.test(path13)) {
6363
+ function runtimeCoordinate(source, path14) {
6364
+ if (/\[\d*\]/.test(path14)) {
5700
6365
  return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
5701
6366
  }
5702
6367
  switch (source) {
@@ -5705,15 +6370,15 @@ function runtimeCoordinate(source, path13) {
5705
6370
  case "multipart":
5706
6371
  case "body":
5707
6372
  case "server-fn-data":
5708
- return { runtimeParameter: `post.${path13}` };
6373
+ return { runtimeParameter: `post.${path14}` };
5709
6374
  case "query":
5710
- return { runtimeParameter: `get.${path13}` };
6375
+ return { runtimeParameter: `get.${path14}` };
5711
6376
  case "cookie":
5712
- return { runtimeParameter: `cookie.${path13}` };
6377
+ return { runtimeParameter: `cookie.${path14}` };
5713
6378
  case "file":
5714
- return { runtimeParameter: `files.${path13}` };
6379
+ return { runtimeParameter: `files.${path14}` };
5715
6380
  case "header":
5716
- return { runtimeParameter: `server.HTTP_${path13.toUpperCase().replace(/-/g, "_")}` };
6381
+ return { runtimeParameter: `server.HTTP_${path14.toUpperCase().replace(/-/g, "_")}` };
5717
6382
  case "route-param":
5718
6383
  return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
5719
6384
  default:
@@ -5745,8 +6410,8 @@ function addressSpaceOf(source) {
5745
6410
  function isProvenFlow(confidence) {
5746
6411
  return confidence === "exact-local" || confidence === "transformed-local";
5747
6412
  }
5748
- function inputIdOf(source, path13) {
5749
- return `${addressSpaceOf(source)}:${path13}`;
6413
+ function inputIdOf(source, path14) {
6414
+ return `${addressSpaceOf(source)}:${path14}`;
5750
6415
  }
5751
6416
  function withCoordinates(fields, source) {
5752
6417
  return fields.map((f) => {
@@ -5956,8 +6621,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts) {
5956
6621
  if (!bodyNode || sinks.length === 0 || inputs.length === 0) return { flows: [], limitations: [] };
5957
6622
  const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
5958
6623
  const rootPath = /* @__PURE__ */ new Map();
5959
- const addRoot = (name, path13, space) => {
5960
- if (!rootPath.has(name)) rootPath.set(name, { path: path13, space });
6624
+ const addRoot = (name, path14, space) => {
6625
+ if (!rootPath.has(name)) rootPath.set(name, { path: path14, space });
5961
6626
  };
5962
6627
  for (const p of params ?? []) {
5963
6628
  if (!p?.name) continue;
@@ -6102,8 +6767,8 @@ function dedupeLimitations(list) {
6102
6767
  function join22(base, seg) {
6103
6768
  return base ? `${base}.${seg}` : seg;
6104
6769
  }
6105
- function normalizePath(path13) {
6106
- return path13.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
6770
+ function normalizePath(path14) {
6771
+ return path14.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
6107
6772
  }
6108
6773
  function fluentChainCalls(call, ts) {
6109
6774
  let root = call;
@@ -6349,8 +7014,8 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
6349
7014
  }
6350
7015
 
6351
7016
  // src/map/imports.ts
6352
- import { readFileSync as readFileSync11 } from "fs";
6353
- import { join as join17 } from "path";
7017
+ import { readFileSync as readFileSync12 } from "fs";
7018
+ import { join as join18 } from "path";
6354
7019
  var MAX_SITES = 5;
6355
7020
  var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
6356
7021
  function isNpmPackageName(pkg) {
@@ -6361,7 +7026,7 @@ function readPathAliases(cwd) {
6361
7026
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
6362
7027
  let paths;
6363
7028
  try {
6364
- const raw = readFileSync11(join17(cwd, name), "utf8");
7029
+ const raw = readFileSync12(join18(cwd, name), "utf8");
6365
7030
  const parsed = JSON.parse(stripJsonComments(raw));
6366
7031
  paths = parsed.compilerOptions?.paths;
6367
7032
  } catch {
@@ -6721,7 +7386,7 @@ async function extractInputMap(cwd, ts, options = {}) {
6721
7386
  const failed = [];
6722
7387
  let boundary = cwd;
6723
7388
  try {
6724
- boundary = realpathSync3(cwd);
7389
+ boundary = realpathSync4(cwd);
6725
7390
  } catch {
6726
7391
  }
6727
7392
  const graph = createModuleGraph(ts, { cwd, boundary, followOutside: options.followSymlinks });
@@ -6740,8 +7405,8 @@ async function extractInputMap(cwd, ts, options = {}) {
6740
7405
  const hopTargets = /* @__PURE__ */ new Set();
6741
7406
  for (const file of files) {
6742
7407
  try {
6743
- const text = readFileSync12(file, "utf8");
6744
- const relFile = relative4(cwd, file);
7408
+ const text = readFileSync13(file, "utf8");
7409
+ const relFile = relative5(cwd, file);
6745
7410
  sourceBytes += text.length;
6746
7411
  if (!hasEntrySignal(text)) {
6747
7412
  preFiltered++;
@@ -6753,7 +7418,7 @@ async function extractInputMap(cwd, ts, options = {}) {
6753
7418
  }
6754
7419
  parsed++;
6755
7420
  entryFiles.add(file);
6756
- const fingerprint = createHash4("sha256").update(text).digest("hex").slice(0, 16);
7421
+ const fingerprint = createHash5("sha256").update(text).digest("hex").slice(0, 16);
6757
7422
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
6758
7423
  const bindings = buildModuleBindings(sf, ts);
6759
7424
  for (const specifier of bindings.imports) {
@@ -6787,7 +7452,7 @@ async function extractInputMap(cwd, ts, options = {}) {
6787
7452
  }
6788
7453
  } catch (e) {
6789
7454
  if (typeof process !== "undefined" && process.env?.PS_MAP_DEBUG) console.error("[patchstack] map error", file, e);
6790
- failed.push(relative4(cwd, file));
7455
+ failed.push(relative5(cwd, file));
6791
7456
  }
6792
7457
  }
6793
7458
  let hopParsed = 0;
@@ -6795,10 +7460,10 @@ async function extractInputMap(cwd, ts, options = {}) {
6795
7460
  for (const target of hopTargets) {
6796
7461
  if (entryFiles.has(target)) continue;
6797
7462
  try {
6798
- const text = readFileSync12(target, "utf8");
7463
+ const text = readFileSync13(target, "utf8");
6799
7464
  const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
6800
7465
  const bindings = buildModuleBindings(sf, ts);
6801
- const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative4(cwd, target), graph });
7466
+ const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative5(cwd, target), graph });
6802
7467
  invocations.add(called.invocations);
6803
7468
  calls.total += called.counts.total;
6804
7469
  calls.dependency += called.counts.dependency;
@@ -6830,7 +7495,7 @@ async function extractInputMap(cwd, ts, options = {}) {
6830
7495
  if (endpoints.length === 0) notes.push("No recognized server-side entry points found under the analyzed roots.");
6831
7496
  const importList = imports.list();
6832
7497
  const unmodelled = importList.filter((d) => d.recognizedSinkKinds.length === 0).length;
6833
- const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative4(cwd, d));
7498
+ const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative5(cwd, d));
6834
7499
  const importCoverageGaps = {
6835
7500
  unreadableFiles: failed.length,
6836
7501
  unscannableFiles: importScanFailures,
@@ -7018,8 +7683,8 @@ async function runMap(flags) {
7018
7683
  }
7019
7684
 
7020
7685
  // src/setup.ts
7021
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
7022
- import path12 from "path";
7686
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
7687
+ import path13 from "path";
7023
7688
  var SCAN_COMMAND = "patchstack-connect scan";
7024
7689
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
7025
7690
  function setupProtection(cwd) {
@@ -7037,8 +7702,8 @@ function appendHook(existing, command) {
7037
7702
  return `${existing} && ${command}`;
7038
7703
  }
7039
7704
  function wireBuildScripts(cwd, packageManager) {
7040
- const target = path12.join(cwd, "package.json");
7041
- const raw = readFileSync13(target, "utf8");
7705
+ const target = path13.join(cwd, "package.json");
7706
+ const raw = readFileSync14(target, "utf8");
7042
7707
  const pkg = JSON.parse(raw);
7043
7708
  const scripts = pkg.scripts ?? {};
7044
7709
  const build = scripts.build;
@@ -7192,6 +7857,15 @@ Usage:
7192
7857
  demonstrations, not production).
7193
7858
  --check verifies the guard is wired (exit 1
7194
7859
  if not) \u2014 for the wire-then-verify loop.
7860
+ It reads the app's source and never runs it.
7861
+ --check --runtime additionally STARTS THE APP
7862
+ on a loopback port and sends it one request,
7863
+ to establish that a request reaches the
7864
+ guard. Opt-in, and the only mode that runs
7865
+ the app. Exit 0 traversed, 1 a listener
7866
+ answered instead of the guard, 2 could not
7867
+ be established (e.g. a TypeScript or
7868
+ framework entry this must not start).
7195
7869
  patchstack-connect demo node-serialize [--url URL] Run the production-backed node-serialize
7196
7870
  walkthrough: verify the vulnerable package,
7197
7871
  scan it, wait for live rule 18843, install +
@@ -7204,6 +7878,20 @@ Usage:
7204
7878
  what's missing, with tailored commands), then
7205
7879
  print the full setup guide. --full prints the
7206
7880
  guide even when setup is complete
7881
+ patchstack-connect claim [--wait] Attach this site to a Patchstack account from
7882
+ the terminal, so its reports become visible.
7883
+ Prints a link for the user to open and sign in
7884
+ (or sign up) \u2014 whoever approves becomes the
7885
+ site's owner. In a terminal it then waits. When
7886
+ the output is piped or captured \u2014 an assistant
7887
+ running it \u2014 it prints the link and EXITS, so
7888
+ the link is visible immediately. Run it AGAIN
7889
+ once the user confirms: it resumes the same
7890
+ request rather than starting a new one. --wait
7891
+ blocks instead of returning. Does not rotate
7892
+ the site's credential. The same claim can be
7893
+ done by opening the dashboard link that scan
7894
+ and status print. Not usable in CI
7207
7895
  patchstack-connect login [--wait] Recover this site's credential when
7208
7896
  .patchstackrc.local.json has been lost. Needs the
7209
7897
  site UUID, which lives in the committed
@@ -7321,6 +8009,107 @@ async function runInit(args) {
7321
8009
  console.log("Next: run `npx @patchstack/connect scan` to send your first manifest.");
7322
8010
  return 0;
7323
8011
  }
8012
+ async function runClaim(args) {
8013
+ if (process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false") {
8014
+ console.error("`claim` is interactive and cannot run in CI. Claim the site from a developer machine.");
8015
+ return 1;
8016
+ }
8017
+ const config = await resolveConfig({
8018
+ cwd: process.cwd(),
8019
+ cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
8020
+ cliEndpoint: getStringFlag(args.flags, "endpoint")
8021
+ });
8022
+ const siteUuid = config.siteUuid;
8023
+ if (siteUuid === null) {
8024
+ console.error("\n No site to claim yet \u2014 run `npx @patchstack/connect scan` first.\n");
8025
+ return 1;
8026
+ }
8027
+ const settled = async (result2) => {
8028
+ console.log(
8029
+ `
8030
+ \u2713 Site claimed${result2.account !== void 0 ? ` by ${result2.account}` : ""}.`
8031
+ );
8032
+ console.log(` ${result2.dashboardUrl ?? buildClaimUrl(config.endpoint, siteUuid)}`);
8033
+ if (result2.credentialSaved === true) {
8034
+ const ignore = await secretFileIgnored(process.cwd());
8035
+ console.log(` A credential for this site was issued and saved to ${SECRET_CONFIG_FILENAME}.`);
8036
+ console.log(
8037
+ ignore.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${ignore.reason ?? "unknown reason"}. Add it to .gitignore yourself before committing.`
8038
+ );
8039
+ }
8040
+ console.log("");
8041
+ return 0;
8042
+ };
8043
+ const prompt = (userCode, verificationUri) => {
8044
+ console.log(`
8045
+ Your code: ${userCode}`);
8046
+ console.log(` Claim at: ${verificationUri}
8047
+ `);
8048
+ console.log(" Open that link and sign in to Patchstack \u2014 or create an account \u2014 to attach");
8049
+ console.log(" this site to it. Whoever approves becomes the site's owner.\n");
8050
+ };
8051
+ if (args.flags.has("wait")) {
8052
+ const pending = readPendingClaim(siteUuid);
8053
+ if (pending === null) {
8054
+ console.error("\n No claim is waiting for approval. Run `patchstack-connect claim` first.\n");
8055
+ return 1;
8056
+ }
8057
+ const resumed = await waitForClaim(config, pending);
8058
+ if (resumed.status === "claimed") return await settled(resumed);
8059
+ console.error(`
8060
+ ${resumed.message ?? "Claim failed."}
8061
+ `);
8062
+ return 1;
8063
+ }
8064
+ if (process.stdout.isTTY !== true) {
8065
+ const existing = readPendingClaim(siteUuid);
8066
+ if (existing !== null && Date.now() < existing.expiresAt) {
8067
+ const outcome = await redeemClaimIfApproved(config, existing);
8068
+ if (outcome !== "pending" && outcome.status === "claimed") return await settled(outcome);
8069
+ if (outcome === "pending") {
8070
+ const secondsLeft = Math.round((existing.expiresAt - Date.now()) / 1e3);
8071
+ console.log(`
8072
+ Still waiting for the site to be claimed with code ${existing.userCode}.`);
8073
+ console.log(` Claim at: ${existing.verificationUri}`);
8074
+ console.log(` (valid for another ${secondsLeft}s \u2014 run this again once the user confirms)
8075
+ `);
8076
+ return 0;
8077
+ }
8078
+ }
8079
+ const started = await startClaim(config);
8080
+ if (started.status !== "started" || started.pending === void 0) {
8081
+ if (started.status === "already-claimed") {
8082
+ console.log(`
8083
+ ${started.message ?? "This site is already claimed."}
8084
+ `);
8085
+ return 0;
8086
+ }
8087
+ console.error(`
8088
+ ${started.message ?? "Claim failed."}
8089
+ `);
8090
+ return 1;
8091
+ }
8092
+ prompt(started.pending.userCode, started.pending.verificationUri);
8093
+ console.log(" Give that link to the user. When they confirm they have claimed the site, run");
8094
+ console.log(" this same command again (or `claim --wait` to block until they do).\n");
8095
+ return 0;
8096
+ }
8097
+ const result = await claim2(config, (userCode, verificationUri) => {
8098
+ prompt(userCode, verificationUri);
8099
+ console.log(" Waiting (the code expires in 10 minutes)\u2026");
8100
+ });
8101
+ if (result.status === "claimed") return await settled(result);
8102
+ if (result.status === "already-claimed") {
8103
+ console.log(`
8104
+ ${result.message ?? "This site is already claimed."}
8105
+ `);
8106
+ return 0;
8107
+ }
8108
+ console.error(`
8109
+ ${result.message ?? "Claim failed."}
8110
+ `);
8111
+ return 1;
8112
+ }
7324
8113
  async function runLogin(args) {
7325
8114
  if (process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false") {
7326
8115
  console.error("`login` is interactive and cannot run in CI. Set PATCHSTACK_PULSE_AUTH instead.");
@@ -7523,6 +8312,9 @@ async function runScan(args, options = {}) {
7523
8312
  if (config.endpoint !== DEFAULT_ENDPOINT) {
7524
8313
  console.log(" (this URL inherits the endpoint override above)");
7525
8314
  }
8315
+ console.log("");
8316
+ console.log("Or attach it to your account from this terminal:");
8317
+ console.log(" npx @patchstack/connect claim");
7526
8318
  }
7527
8319
  if (options.showRemainingSetup !== false) {
7528
8320
  try {
@@ -7610,6 +8402,11 @@ function reportSourceMarker(framework) {
7610
8402
  }
7611
8403
  }
7612
8404
  async function runProtectCommand(args) {
8405
+ const runtime = args.flags.get("runtime") === true;
8406
+ if (runtime && args.flags.get("check") !== true) {
8407
+ console.error("patchstack protect: --runtime only applies to --check. Run `protect --check --runtime`.");
8408
+ return 1;
8409
+ }
7613
8410
  if (args.flags.get("check") === true) {
7614
8411
  const report = runVerify(process.cwd());
7615
8412
  console.log(`patchstack protect --check (${report.stack}):`);
@@ -7628,7 +8425,12 @@ async function runProtectCommand(args) {
7628
8425
  if (report.checks.some((c) => c.unverifiable)) {
7629
8426
  console.log("One or more checks could not be answered from here \u2014 see the `?` lines above.");
7630
8427
  }
7631
- return report.wired ? 0 : 1;
8428
+ if (!report.wired) return 1;
8429
+ if (!runtime) return 0;
8430
+ console.log("");
8431
+ const runtimeReport = await runRuntimeCheck(process.cwd());
8432
+ for (const line2 of formatRuntimeCheck(runtimeReport)) console.log(line2);
8433
+ return runtimeExitCode(runtimeReport);
7632
8434
  }
7633
8435
  const demo = args.flags.get("demo") === true;
7634
8436
  try {
@@ -7751,7 +8553,7 @@ async function runGuide(args) {
7751
8553
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
7752
8554
  console.log("");
7753
8555
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
7754
- console.log(readFileSync14(guidePath, "utf8"));
8556
+ console.log(readFileSync15(guidePath, "utf8"));
7755
8557
  return 0;
7756
8558
  }
7757
8559
  async function runSetup(args) {
@@ -7825,6 +8627,7 @@ async function runStatus(args) {
7825
8627
  console.log(`Environment: ${config.environment}`);
7826
8628
  if (config.siteUuid !== null) {
7827
8629
  console.log(`Dashboard URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
8630
+ console.log(" Not attached to an account yet? Run `npx @patchstack/connect claim`.");
7828
8631
  switch (await fetchSiteStatus(config)) {
7829
8632
  case "active":
7830
8633
  console.log("Site status: active on Patchstack");
@@ -7944,7 +8747,7 @@ async function runMarkBuild(args) {
7944
8747
  let marked = 0;
7945
8748
  let widgetTouched = 0;
7946
8749
  for (const file of files) {
7947
- const before = readFileSync14(file, "utf8");
8750
+ const before = readFileSync15(file, "utf8");
7948
8751
  let after = injectMarker(before, snippet);
7949
8752
  if (widgetUuid !== null) {
7950
8753
  const ensured = ensureWidgetInHtml(after, widgetUuid);
@@ -8006,6 +8809,8 @@ async function main() {
8006
8809
  return runSetup(args);
8007
8810
  case "map":
8008
8811
  return runMap(args.flags);
8812
+ case "claim":
8813
+ return runClaim(args);
8009
8814
  case "login":
8010
8815
  return runLogin(args);
8011
8816
  default: