@patchstack/connect 0.5.0 → 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();
@@ -1291,6 +1291,25 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
1291
1291
  // src/client.ts
1292
1292
  var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
1293
1293
  var DEFAULT_TIMEOUT_MS = 3e4;
1294
+ var CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
1295
+ function claimTokenHeader(config) {
1296
+ return typeof config.claimToken === "string" && config.claimToken !== "" ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
1297
+ }
1298
+ function claimOutcomeLines(claim3, config) {
1299
+ if (typeof config.claimToken !== "string" || config.claimToken === "") return [];
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
+ return [
1303
+ claim3.state === "claimed" ? "Connected to your Patchstack account." : "This site is already connected to your Patchstack account.",
1304
+ ...dashboard
1305
+ ];
1306
+ }
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
+ return [
1309
+ `Not connected to your account: ${why}.`,
1310
+ "Open the dashboard link below to connect it, or copy a fresh prompt from the dashboard."
1311
+ ];
1312
+ }
1294
1313
  function buildEndpointUrl(base, siteUuid) {
1295
1314
  const trimmed = base.replace(/\/$/, "");
1296
1315
  return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
@@ -1310,8 +1329,8 @@ function authFailureMessage(status, config) {
1310
1329
  }
1311
1330
  function buildRulesUrl(manifestEndpoint, siteUuid) {
1312
1331
  const url = new URL(manifestEndpoint);
1313
- const path13 = url.pathname.replace(/\/$/, "");
1314
- 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)}`;
1315
1334
  url.search = "";
1316
1335
  url.hash = "";
1317
1336
  return url.toString();
@@ -1326,16 +1345,16 @@ function buildSettingsUrl(endpoint, siteUuid) {
1326
1345
  }
1327
1346
  function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
1328
1347
  const url = new URL(manifestEndpoint);
1329
- const path13 = url.pathname.replace(/\/$/, "");
1330
- 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)}`;
1331
1350
  url.search = "";
1332
1351
  url.hash = "";
1333
1352
  return url.toString();
1334
1353
  }
1335
1354
  function buildInputMapUrl(manifestEndpoint, siteUuid) {
1336
1355
  const url = new URL(manifestEndpoint);
1337
- const path13 = url.pathname.replace(/\/$/, "");
1338
- 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)}`;
1339
1358
  url.search = "";
1340
1359
  url.hash = "";
1341
1360
  return url.toString();
@@ -1454,7 +1473,8 @@ async function postManifest(config, payload) {
1454
1473
  headers: {
1455
1474
  "Content-Type": "application/json",
1456
1475
  Accept: "application/json",
1457
- "User-Agent": "@patchstack/connect"
1476
+ "User-Agent": "@patchstack/connect",
1477
+ ...claimTokenHeader(config)
1458
1478
  },
1459
1479
  body: JSON.stringify(buildManifestBody(config, payload)),
1460
1480
  signal: AbortSignal.timeout(timeoutMs)
@@ -1670,7 +1690,7 @@ async function waitForDemoRule(manifestEndpoint, siteUuid, scenario, options = {
1670
1690
  const pollIntervalMs = options.pollIntervalMs ?? 1e3;
1671
1691
  const requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
1672
1692
  const fetchFn = options.fetchFn ?? fetch;
1673
- const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
1693
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve4) => setTimeout(resolve4, ms)));
1674
1694
  const now = options.now ?? Date.now;
1675
1695
  const started = now();
1676
1696
  const rulesUrl = buildRulesUrl(manifestEndpoint, siteUuid);
@@ -1944,6 +1964,7 @@ async function resolveConfig(options) {
1944
1964
  const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
1945
1965
  const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
1946
1966
  const identity = options.detectSiteIdentity === true ? await resolveSiteIdentity(options.cwd, fromEnv, fromFile) : { url: null, name: null };
1967
+ const claimToken = stated(options.cliClaimToken) ?? stated(process.env.PATCHSTACK_CLAIM_TOKEN);
1947
1968
  return {
1948
1969
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
1949
1970
  apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
@@ -1953,7 +1974,8 @@ async function resolveConfig(options) {
1953
1974
  endpoint,
1954
1975
  timeoutMs,
1955
1976
  environment,
1956
- widget: fromFile.widget !== false
1977
+ widget: fromFile.widget !== false,
1978
+ claimToken
1957
1979
  };
1958
1980
  }
1959
1981
  async function writeConfigFile(cwd, config) {
@@ -2989,6 +3011,112 @@ import { join as join10, dirname as dirname5 } from "path";
2989
3011
  import { execFileSync } from "child_process";
2990
3012
  import { existsSync as existsSync7 } from "fs";
2991
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
2992
3120
  var IMPORT_LINE = /^\s*(?:import\b|export\s+(?:\*|\{)|(?:const|let|var)\s+[^=]+=\s*require\()/;
2993
3121
  function lastTopLevelImportLine(source) {
2994
3122
  const lines = source.split("\n");
@@ -3067,7 +3195,10 @@ function advanceDepth(line, inBlockComment, add) {
3067
3195
  function parses(filePath) {
3068
3196
  if (!/\.(?:js|cjs|mjs)$/.test(filePath)) return null;
3069
3197
  try {
3070
- 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
+ });
3071
3202
  return true;
3072
3203
  } catch (error) {
3073
3204
  return isSyntaxError(error) ? false : null;
@@ -4336,12 +4467,15 @@ function renderGuideChecklist(state, useColor) {
4336
4467
  lines.push(detail(`${check.label}${check.hint ? ` \u2014 ${check.hint}` : ""}`));
4337
4468
  }
4338
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"));
4339
4471
  }
4340
4472
  lines.push("");
4341
4473
  if (state.claimUrl !== null) {
4342
4474
  lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Dashboard link (open to view reports):")}`);
4343
4475
  lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
4344
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)"));
4345
4479
  if (state.endpointOverride !== null) {
4346
4480
  lines.push(detail("(this URL inherits the endpoint override above)"));
4347
4481
  }
@@ -4384,7 +4518,12 @@ function renderGuideChecklist(state, useColor) {
4384
4518
  }
4385
4519
 
4386
4520
  // src/login.ts
4387
- 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";
4388
4527
  import { tmpdir } from "os";
4389
4528
  import path11 from "path";
4390
4529
  import { createHash as createHash2 } from "crypto";
@@ -4396,108 +4535,126 @@ function baseFrom(manifestEndpoint) {
4396
4535
  url.hash = "";
4397
4536
  return url.toString().replace(/\/$/, "");
4398
4537
  }
4399
- function pendingPath(siteUuid) {
4538
+ function pendingPath(siteUuid, intent) {
4400
4539
  const key = createHash2("sha256").update(siteUuid).digest("hex").slice(0, 16);
4401
- return path11.join(tmpdir(), `patchstack-login-${key}.json`);
4540
+ return path11.join(tmpdir(), `patchstack-${intent}-${key}.json`);
4402
4541
  }
4403
- function savePendingLogin(siteUuid, pending) {
4404
- 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
+ });
4405
4547
  }
4406
- function readPendingLogin(siteUuid) {
4548
+ function readPending(siteUuid, intent) {
4407
4549
  try {
4408
- return JSON.parse(readFileSync7(pendingPath(siteUuid), "utf8"));
4550
+ return JSON.parse(readFileSync7(pendingPath(siteUuid, intent), "utf8"));
4409
4551
  } catch {
4410
4552
  return null;
4411
4553
  }
4412
4554
  }
4413
- function clearPendingLogin(siteUuid) {
4555
+ function clearPending(siteUuid, intent) {
4414
4556
  try {
4415
- rmSync(pendingPath(siteUuid));
4557
+ rmSync(pendingPath(siteUuid, intent));
4416
4558
  } catch {
4417
4559
  }
4418
4560
  }
4419
- async function startLogin(config, deps = {}) {
4561
+ async function requestDeviceCode(config, intent, conflictMessage, deps = {}) {
4420
4562
  const fetchImpl = deps.fetchImpl ?? fetch;
4421
4563
  const now = deps.now ?? (() => Date.now());
4422
4564
  if (config.siteUuid === null) {
4423
- return { status: "failed", message: "No site UUID configured \u2014 run `patchstack-connect scan` first." };
4424
- }
4425
- const base = baseFrom(config.endpoint);
4426
- const started = await fetchImpl(`${base}/device/code`, {
4427
- method: "POST",
4428
- headers: { "Content-Type": "application/json", Accept: "application/json" },
4429
- body: JSON.stringify({ site_uuid: config.siteUuid })
4430
- });
4431
- if (started.status === 409) {
4432
4565
  return {
4433
- status: "unclaimed",
4434
- 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."
4435
4568
  };
4436
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 };
4437
4582
  if (started.status === 404) {
4438
4583
  return { status: "not-found", message: "Patchstack does not recognise this site UUID." };
4439
4584
  }
4440
4585
  if (!started.ok) {
4441
- 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}).` };
4442
4587
  }
4443
4588
  const body = await started.json();
4444
4589
  const pending = {
4445
4590
  deviceCode: body.device_code,
4446
4591
  userCode: body.user_code,
4447
- // Points at the API, which redirects to the dashboard SPA — the CLI only
4448
- // 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.
4449
4594
  verificationUri: `${base}/device?code=${encodeURIComponent(body.user_code)}`,
4450
4595
  expiresAt: now() + body.expires_in * 1e3,
4451
4596
  intervalMs: Math.max(1, body.interval) * 1e3
4452
4597
  };
4453
- savePendingLogin(config.siteUuid, pending);
4598
+ savePending(config.siteUuid, intent, pending);
4454
4599
  return { status: "started", pending };
4455
4600
  }
4456
- async function redeemIfApproved(config, pending, deps = {}) {
4601
+ async function pollDeviceToken(config, intent, pending, deps = {}) {
4457
4602
  const fetchImpl = deps.fetchImpl ?? fetch;
4458
4603
  const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
4459
4604
  method: "POST",
4460
4605
  headers: { "Content-Type": "application/json", Accept: "application/json" },
4461
4606
  body: JSON.stringify({ device_code: pending.deviceCode })
4462
4607
  });
4463
- if (polled.status === 428) return "pending";
4608
+ if (polled.status === 428) return { state: "pending" };
4464
4609
  if (!polled.ok) {
4465
- if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
4466
- return "expired";
4610
+ if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
4611
+ return { state: "expired" };
4467
4612
  }
4468
- const { api_key: apiKey } = await polled.json();
4469
- if (typeof apiKey !== "string" || apiKey.length === 0) return "expired";
4470
- await persistApiKey(process.cwd(), apiKey);
4471
- if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
4472
- return "approved";
4613
+ const body = await polled.json();
4614
+ if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
4615
+ return { state: "approved", body };
4473
4616
  }
4474
- async function waitForApproval(config, pending, deps = {}) {
4475
- const fetchImpl = deps.fetchImpl ?? fetch;
4617
+ async function pollUntilApproved(config, intent, pending, deps = {}) {
4476
4618
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4477
4619
  const now = deps.now ?? (() => Date.now());
4478
4620
  const deadline = Math.min(pending.expiresAt, deps.until ?? pending.expiresAt);
4479
- const expired = { status: "expired", message: "The login request expired. Run the command again." };
4480
4621
  while (now() < deadline) {
4481
4622
  await sleep(pending.intervalMs);
4482
- const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
4483
- method: "POST",
4484
- headers: { "Content-Type": "application/json", Accept: "application/json" },
4485
- body: JSON.stringify({ device_code: pending.deviceCode })
4486
- });
4487
- if (polled.status === 428) continue;
4488
- if (!polled.ok) {
4489
- if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
4490
- return expired;
4491
- }
4492
- const { api_key: apiKey } = await polled.json();
4493
- if (typeof apiKey !== "string" || apiKey.length === 0) {
4494
- return { status: "failed", message: "Patchstack approved the request but returned no credential." };
4495
- }
4496
- await persistApiKey(process.cwd(), apiKey);
4497
- if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
4498
- 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;
4625
+ }
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." };
4499
4655
  }
4500
- return expired;
4656
+ await persistApiKey(process.cwd(), apiKey);
4657
+ return { status: "approved", userCode: pending.userCode, verificationUri: pending.verificationUri };
4501
4658
  }
4502
4659
  async function login(config, onPrompt, deps = {}) {
4503
4660
  const started = await startLogin(config, deps);
@@ -4508,16 +4665,546 @@ async function login(config, onPrompt, deps = {}) {
4508
4665
  return waitForApproval(config, started.pending, deps);
4509
4666
  }
4510
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
+
4511
5198
  // src/map-command.ts
4512
5199
  import { writeFileSync as writeFileSync8 } from "fs";
4513
5200
 
4514
5201
  // src/map/ts-loader.ts
4515
5202
  import { createRequire } from "module";
4516
5203
  import { pathToFileURL } from "url";
4517
- import { join as join13 } from "path";
5204
+ import { join as join14 } from "path";
4518
5205
  async function loadTypeScript(cwd) {
4519
5206
  try {
4520
- const req = createRequire(pathToFileURL(join13(cwd, "package.json")));
5207
+ const req = createRequire(pathToFileURL(join14(cwd, "package.json")));
4521
5208
  const resolved = req.resolve("typescript");
4522
5209
  const mod = await import(pathToFileURL(resolved).href);
4523
5210
  return mod.default ?? mod;
@@ -4532,9 +5219,9 @@ async function loadTypeScript(cwd) {
4532
5219
  }
4533
5220
 
4534
5221
  // src/map/extract.ts
4535
- import { readFileSync as readFileSync12, realpathSync as realpathSync3 } from "fs";
4536
- import { createHash as createHash4 } from "crypto";
4537
- 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";
4538
5225
 
4539
5226
  // src/map/ast.ts
4540
5227
  function rootIdentifier(node, ts) {
@@ -4788,8 +5475,8 @@ function npmPackageOf(spec) {
4788
5475
  }
4789
5476
 
4790
5477
  // src/map/sources.ts
4791
- import { readFileSync as readFileSync8, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
4792
- 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";
4793
5480
 
4794
5481
  // src/map/routes.ts
4795
5482
  var ROUTE_REGISTER_NAMES = ["get", "post", "put", "patch", "delete", "options", "all", "head", "use"];
@@ -4873,7 +5560,7 @@ function hasEntrySignal(text) {
4873
5560
  }
4874
5561
  function detectFramework(cwd) {
4875
5562
  try {
4876
- const pkg = JSON.parse(readFileSync8(join14(cwd, "package.json"), "utf8"));
5563
+ const pkg = JSON.parse(readFileSync9(join15(cwd, "package.json"), "utf8"));
4877
5564
  const d = { ...pkg.dependencies, ...pkg.devDependencies };
4878
5565
  if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
4879
5566
  if (d["next"]) return "next";
@@ -4885,11 +5572,11 @@ function detectFramework(cwd) {
4885
5572
  } catch {
4886
5573
  }
4887
5574
  try {
4888
- if (statSync3(join14(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
5575
+ if (statSync4(join15(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
4889
5576
  } catch {
4890
5577
  }
4891
5578
  try {
4892
- if (statSync3(join14(cwd, "functions")).isDirectory()) return "deno-functions";
5579
+ if (statSync4(join15(cwd, "functions")).isDirectory()) return "deno-functions";
4893
5580
  } catch {
4894
5581
  }
4895
5582
  return "unknown";
@@ -4931,7 +5618,7 @@ function probeForSource(dir, budget) {
4931
5618
  for (const e of entries) {
4932
5619
  if (budget.left-- <= 0) return "unknown";
4933
5620
  if (e.isDirectory()) {
4934
- 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));
4935
5622
  } else if (isSourceFile(e.name)) return true;
4936
5623
  else if (e.isSymbolicLink()) return "unknown";
4937
5624
  }
@@ -4944,7 +5631,7 @@ function probeForSource(dir, budget) {
4944
5631
  function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ new Set(), stats = { discovered: 0, unwalked: 0, skippedWithSource: [], skippedUnknown: 0 }, probeBudget = { left: PROBE_BUDGET }) {
4945
5632
  let key;
4946
5633
  try {
4947
- key = realpathSync(dir);
5634
+ key = realpathSync2(dir);
4948
5635
  } catch {
4949
5636
  stats.unwalked++;
4950
5637
  return out;
@@ -4968,19 +5655,19 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
4968
5655
  const probeable = !SKIP_IS_DEFINITIONAL.has(e.name);
4969
5656
  if (probeable && !e.isDirectory() && e.isSymbolicLink()) stats.skippedUnknown++;
4970
5657
  else if (probeable && e.isDirectory()) {
4971
- const held = probeForSource(join14(dir, e.name), probeBudget);
4972
- 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));
4973
5660
  else if (held === "unknown") stats.skippedUnknown++;
4974
5661
  }
4975
5662
  continue;
4976
5663
  }
4977
- const full = join14(dir, e.name);
5664
+ const full = join15(dir, e.name);
4978
5665
  if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
4979
5666
  else if (e.isSymbolicLink()) {
4980
5667
  let st, real;
4981
5668
  try {
4982
- st = statSync3(full);
4983
- real = realpathSync(full);
5669
+ st = statSync4(full);
5670
+ real = realpathSync2(full);
4984
5671
  } catch {
4985
5672
  stats.unwalked++;
4986
5673
  continue;
@@ -5003,7 +5690,7 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
5003
5690
  }
5004
5691
  function isInside(candidate, boundary) {
5005
5692
  if (candidate === boundary) return true;
5006
- const rel = relative2(boundary, candidate);
5693
+ const rel = relative3(boundary, candidate);
5007
5694
  return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
5008
5695
  }
5009
5696
  var DEPLOYMENT_SHAPES = [
@@ -5030,13 +5717,13 @@ var DEPLOYMENT_SHAPES = [
5030
5717
  function detectDeploymentShapes(cwd, opts = {}) {
5031
5718
  let boundary = opts.boundary ?? cwd;
5032
5719
  try {
5033
- boundary = realpathSync(boundary);
5720
+ boundary = realpathSync2(boundary);
5034
5721
  } catch {
5035
5722
  }
5036
- const inProject = (path13) => {
5723
+ const inProject = (path14) => {
5037
5724
  if (opts.followOutside) return true;
5038
5725
  try {
5039
- return isInside(realpathSync(path13), boundary);
5726
+ return isInside(realpathSync2(path14), boundary);
5040
5727
  } catch {
5041
5728
  return false;
5042
5729
  }
@@ -5044,9 +5731,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
5044
5731
  const found = [];
5045
5732
  for (const candidate of DEPLOYMENT_SHAPES) {
5046
5733
  for (const file of candidate.files ?? []) {
5047
- const full = join14(cwd, file);
5734
+ const full = join15(cwd, file);
5048
5735
  try {
5049
- if (statSync3(full).isFile() && inProject(full)) {
5736
+ if (statSync4(full).isFile() && inProject(full)) {
5050
5737
  found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
5051
5738
  break;
5052
5739
  }
@@ -5054,9 +5741,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
5054
5741
  }
5055
5742
  }
5056
5743
  for (const dir of candidate.dirs ?? []) {
5057
- const full = join14(cwd, dir);
5744
+ const full = join15(cwd, dir);
5058
5745
  try {
5059
- if (!statSync3(full).isDirectory() || !inProject(full)) continue;
5746
+ if (!statSync4(full).isDirectory() || !inProject(full)) continue;
5060
5747
  } catch {
5061
5748
  continue;
5062
5749
  }
@@ -5073,7 +5760,7 @@ function holdsFunctionEntry(dir) {
5073
5760
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
5074
5761
  if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
5075
5762
  try {
5076
- for (const nested of readdirSync5(join14(dir, entry.name), { withFileTypes: true })) {
5763
+ for (const nested of readdirSync5(join15(dir, entry.name), { withFileTypes: true })) {
5077
5764
  if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
5078
5765
  }
5079
5766
  } catch {
@@ -5089,7 +5776,7 @@ function holdsSourceFile(dir) {
5089
5776
  if (entry.isFile() && isSourceFile(entry.name)) return true;
5090
5777
  if (entry.isDirectory()) {
5091
5778
  try {
5092
- for (const nested of readdirSync5(join14(dir, entry.name), { withFileTypes: true })) {
5779
+ for (const nested of readdirSync5(join15(dir, entry.name), { withFileTypes: true })) {
5093
5780
  if (nested.isFile() && isSourceFile(nested.name)) return true;
5094
5781
  }
5095
5782
  } catch {
@@ -5102,8 +5789,8 @@ function holdsSourceFile(dir) {
5102
5789
  }
5103
5790
 
5104
5791
  // src/map/surface.ts
5105
- import { readFileSync as readFileSync9 } from "fs";
5106
- import { join as join15 } from "path";
5792
+ import { readFileSync as readFileSync10 } from "fs";
5793
+ import { join as join16 } from "path";
5107
5794
  var STATIC_GENERATORS = [
5108
5795
  { dep: "vite", label: "vite" },
5109
5796
  { dep: "react-scripts", label: "create-react-app" },
@@ -5141,7 +5828,7 @@ var SHAPE_SIGNAL = {
5141
5828
  };
5142
5829
  function readManifest(cwd) {
5143
5830
  try {
5144
- const parsed = JSON.parse(readFileSync9(join15(cwd, "package.json"), "utf8"));
5831
+ const parsed = JSON.parse(readFileSync10(join16(cwd, "package.json"), "utf8"));
5145
5832
  return typeof parsed === "object" && parsed !== null ? parsed : null;
5146
5833
  } catch {
5147
5834
  return null;
@@ -5154,7 +5841,7 @@ function nextExportsStatically(cwd, manifest, ts) {
5154
5841
  for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
5155
5842
  let text;
5156
5843
  try {
5157
- text = readFileSync9(join15(cwd, file), "utf8");
5844
+ text = readFileSync10(join16(cwd, file), "utf8");
5158
5845
  } catch {
5159
5846
  continue;
5160
5847
  }
@@ -5282,7 +5969,7 @@ function surfaceNote(surface) {
5282
5969
  }
5283
5970
 
5284
5971
  // src/map/sinks.ts
5285
- import { createHash as createHash3 } from "crypto";
5972
+ import { createHash as createHash4 } from "crypto";
5286
5973
  var DB_OPS = /* @__PURE__ */ new Set(["insert", "update", "delete", "select", "upsert", "rpc"]);
5287
5974
  var PRISMA_OPS = /* @__PURE__ */ new Set(["create", "createMany", "update", "updateMany", "delete", "deleteMany", "upsert", "findFirst", "findUnique", "findMany", "count", "aggregate"]);
5288
5975
  var FS_CALLS = /^(readFile|writeFile|readFileSync|writeFileSync|appendFile|createReadStream|createWriteStream|unlink|rm|rmSync|mkdir|readdir|stat|open)$/;
@@ -5381,14 +6068,14 @@ function directSinks(node, ts, bindings, ctx) {
5381
6068
  const root = base ? rootIdentifier(base, ts) : void 0;
5382
6069
  if (!root) return {};
5383
6070
  const spec = bindings.resolve(root);
5384
- const relative5 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
6071
+ const relative6 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
5385
6072
  const pkg = npmPackageOf(spec);
5386
6073
  if (pkg) return { pkg, root, spec };
5387
- if (relative5 && ctx && spec) {
6074
+ if (relative6 && ctx && spec) {
5388
6075
  const viaModule = ctx.graph.importedBinding(ctx.file, spec, bindings.exportNameOf(root) ?? root);
5389
6076
  if (viaModule) return { pkg: viaModule.package, root, spec };
5390
6077
  }
5391
- return { local: bindings.locals.has(root), root, spec, relative: relative5 };
6078
+ return { local: bindings.locals.has(root), root, spec, relative: relative6 };
5392
6079
  };
5393
6080
  const attributionOf = (b, pkg) => b.pkg ? "import" : pkg ? "inferred" : void 0;
5394
6081
  const dbApi = (pkg, attribution) => {
@@ -5549,7 +6236,7 @@ function argumentRoleOf(sinkKind, method, index, total = 0) {
5549
6236
  return table?.[index] ?? "unknown";
5550
6237
  }
5551
6238
  function sinkId(s, owner) {
5552
- 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);
5553
6240
  }
5554
6241
  function dedupeSinks(sinks, owner) {
5555
6242
  const seen = /* @__PURE__ */ new Set();
@@ -5565,8 +6252,8 @@ function dedupeSinks(sinks, owner) {
5565
6252
  }
5566
6253
 
5567
6254
  // src/map/module-graph.ts
5568
- import { readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
5569
- 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";
5570
6257
  var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
5571
6258
  function createModuleGraph(ts, opts) {
5572
6259
  const cache = /* @__PURE__ */ new Map();
@@ -5574,7 +6261,7 @@ function createModuleGraph(ts, opts) {
5574
6261
  if (cache.has(file)) return cache.get(file) ?? null;
5575
6262
  let entry = null;
5576
6263
  try {
5577
- const text = readFileSync10(file, "utf8");
6264
+ const text = readFileSync11(file, "utf8");
5578
6265
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
5579
6266
  const bindings = buildModuleBindings(sf, ts);
5580
6267
  entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
@@ -5590,7 +6277,7 @@ function createModuleGraph(ts, opts) {
5590
6277
  if (!opts.followOutside) {
5591
6278
  let real = target;
5592
6279
  try {
5593
- real = realpathSync2(target);
6280
+ real = realpathSync3(target);
5594
6281
  } catch {
5595
6282
  }
5596
6283
  if (!isInside(real, opts.boundary)) return void 0;
@@ -5608,7 +6295,7 @@ function createModuleGraph(ts, opts) {
5608
6295
  for (const callee of mod.calleesOf.get(exportName) ?? []) {
5609
6296
  for (const s of mod.fnSinks.get(callee) ?? []) collected.push(s);
5610
6297
  }
5611
- const rel = relative3(opts.cwd, target);
6298
+ const rel = relative4(opts.cwd, target);
5612
6299
  return collected.map((s) => ({ ...s, file: rel }));
5613
6300
  },
5614
6301
  // A different question about the same module: not "what sinks are in there" but "what does this
@@ -5644,10 +6331,10 @@ function resolveRelativeModule(fromFile, spec) {
5644
6331
  }
5645
6332
  candidates.push(base);
5646
6333
  for (const e of RESOLVE_EXTS) candidates.push(base + e);
5647
- 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));
5648
6335
  for (const c of candidates) {
5649
6336
  try {
5650
- if (statSync4(c).isFile()) return c;
6337
+ if (statSync5(c).isFile()) return c;
5651
6338
  } catch {
5652
6339
  }
5653
6340
  }
@@ -5673,8 +6360,8 @@ function collectCallees(sf, ts) {
5673
6360
  }
5674
6361
 
5675
6362
  // src/map/coordinates.ts
5676
- function runtimeCoordinate(source, path13) {
5677
- if (/\[\d*\]/.test(path13)) {
6363
+ function runtimeCoordinate(source, path14) {
6364
+ if (/\[\d*\]/.test(path14)) {
5678
6365
  return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
5679
6366
  }
5680
6367
  switch (source) {
@@ -5683,15 +6370,15 @@ function runtimeCoordinate(source, path13) {
5683
6370
  case "multipart":
5684
6371
  case "body":
5685
6372
  case "server-fn-data":
5686
- return { runtimeParameter: `post.${path13}` };
6373
+ return { runtimeParameter: `post.${path14}` };
5687
6374
  case "query":
5688
- return { runtimeParameter: `get.${path13}` };
6375
+ return { runtimeParameter: `get.${path14}` };
5689
6376
  case "cookie":
5690
- return { runtimeParameter: `cookie.${path13}` };
6377
+ return { runtimeParameter: `cookie.${path14}` };
5691
6378
  case "file":
5692
- return { runtimeParameter: `files.${path13}` };
6379
+ return { runtimeParameter: `files.${path14}` };
5693
6380
  case "header":
5694
- return { runtimeParameter: `server.HTTP_${path13.toUpperCase().replace(/-/g, "_")}` };
6381
+ return { runtimeParameter: `server.HTTP_${path14.toUpperCase().replace(/-/g, "_")}` };
5695
6382
  case "route-param":
5696
6383
  return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
5697
6384
  default:
@@ -5723,8 +6410,8 @@ function addressSpaceOf(source) {
5723
6410
  function isProvenFlow(confidence) {
5724
6411
  return confidence === "exact-local" || confidence === "transformed-local";
5725
6412
  }
5726
- function inputIdOf(source, path13) {
5727
- return `${addressSpaceOf(source)}:${path13}`;
6413
+ function inputIdOf(source, path14) {
6414
+ return `${addressSpaceOf(source)}:${path14}`;
5728
6415
  }
5729
6416
  function withCoordinates(fields, source) {
5730
6417
  return fields.map((f) => {
@@ -5934,8 +6621,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts) {
5934
6621
  if (!bodyNode || sinks.length === 0 || inputs.length === 0) return { flows: [], limitations: [] };
5935
6622
  const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
5936
6623
  const rootPath = /* @__PURE__ */ new Map();
5937
- const addRoot = (name, path13, space) => {
5938
- 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 });
5939
6626
  };
5940
6627
  for (const p of params ?? []) {
5941
6628
  if (!p?.name) continue;
@@ -6080,8 +6767,8 @@ function dedupeLimitations(list) {
6080
6767
  function join22(base, seg) {
6081
6768
  return base ? `${base}.${seg}` : seg;
6082
6769
  }
6083
- function normalizePath(path13) {
6084
- return path13.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
6770
+ function normalizePath(path14) {
6771
+ return path14.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
6085
6772
  }
6086
6773
  function fluentChainCalls(call, ts) {
6087
6774
  let root = call;
@@ -6327,8 +7014,8 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
6327
7014
  }
6328
7015
 
6329
7016
  // src/map/imports.ts
6330
- import { readFileSync as readFileSync11 } from "fs";
6331
- import { join as join17 } from "path";
7017
+ import { readFileSync as readFileSync12 } from "fs";
7018
+ import { join as join18 } from "path";
6332
7019
  var MAX_SITES = 5;
6333
7020
  var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
6334
7021
  function isNpmPackageName(pkg) {
@@ -6339,7 +7026,7 @@ function readPathAliases(cwd) {
6339
7026
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
6340
7027
  let paths;
6341
7028
  try {
6342
- const raw = readFileSync11(join17(cwd, name), "utf8");
7029
+ const raw = readFileSync12(join18(cwd, name), "utf8");
6343
7030
  const parsed = JSON.parse(stripJsonComments(raw));
6344
7031
  paths = parsed.compilerOptions?.paths;
6345
7032
  } catch {
@@ -6699,7 +7386,7 @@ async function extractInputMap(cwd, ts, options = {}) {
6699
7386
  const failed = [];
6700
7387
  let boundary = cwd;
6701
7388
  try {
6702
- boundary = realpathSync3(cwd);
7389
+ boundary = realpathSync4(cwd);
6703
7390
  } catch {
6704
7391
  }
6705
7392
  const graph = createModuleGraph(ts, { cwd, boundary, followOutside: options.followSymlinks });
@@ -6718,8 +7405,8 @@ async function extractInputMap(cwd, ts, options = {}) {
6718
7405
  const hopTargets = /* @__PURE__ */ new Set();
6719
7406
  for (const file of files) {
6720
7407
  try {
6721
- const text = readFileSync12(file, "utf8");
6722
- const relFile = relative4(cwd, file);
7408
+ const text = readFileSync13(file, "utf8");
7409
+ const relFile = relative5(cwd, file);
6723
7410
  sourceBytes += text.length;
6724
7411
  if (!hasEntrySignal(text)) {
6725
7412
  preFiltered++;
@@ -6731,7 +7418,7 @@ async function extractInputMap(cwd, ts, options = {}) {
6731
7418
  }
6732
7419
  parsed++;
6733
7420
  entryFiles.add(file);
6734
- const fingerprint = createHash4("sha256").update(text).digest("hex").slice(0, 16);
7421
+ const fingerprint = createHash5("sha256").update(text).digest("hex").slice(0, 16);
6735
7422
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
6736
7423
  const bindings = buildModuleBindings(sf, ts);
6737
7424
  for (const specifier of bindings.imports) {
@@ -6765,7 +7452,7 @@ async function extractInputMap(cwd, ts, options = {}) {
6765
7452
  }
6766
7453
  } catch (e) {
6767
7454
  if (typeof process !== "undefined" && process.env?.PS_MAP_DEBUG) console.error("[patchstack] map error", file, e);
6768
- failed.push(relative4(cwd, file));
7455
+ failed.push(relative5(cwd, file));
6769
7456
  }
6770
7457
  }
6771
7458
  let hopParsed = 0;
@@ -6773,10 +7460,10 @@ async function extractInputMap(cwd, ts, options = {}) {
6773
7460
  for (const target of hopTargets) {
6774
7461
  if (entryFiles.has(target)) continue;
6775
7462
  try {
6776
- const text = readFileSync12(target, "utf8");
7463
+ const text = readFileSync13(target, "utf8");
6777
7464
  const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
6778
7465
  const bindings = buildModuleBindings(sf, ts);
6779
- 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 });
6780
7467
  invocations.add(called.invocations);
6781
7468
  calls.total += called.counts.total;
6782
7469
  calls.dependency += called.counts.dependency;
@@ -6808,7 +7495,7 @@ async function extractInputMap(cwd, ts, options = {}) {
6808
7495
  if (endpoints.length === 0) notes.push("No recognized server-side entry points found under the analyzed roots.");
6809
7496
  const importList = imports.list();
6810
7497
  const unmodelled = importList.filter((d) => d.recognizedSinkKinds.length === 0).length;
6811
- const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative4(cwd, d));
7498
+ const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative5(cwd, d));
6812
7499
  const importCoverageGaps = {
6813
7500
  unreadableFiles: failed.length,
6814
7501
  unscannableFiles: importScanFailures,
@@ -6996,8 +7683,8 @@ async function runMap(flags) {
6996
7683
  }
6997
7684
 
6998
7685
  // src/setup.ts
6999
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
7000
- import path12 from "path";
7686
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
7687
+ import path13 from "path";
7001
7688
  var SCAN_COMMAND = "patchstack-connect scan";
7002
7689
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
7003
7690
  function setupProtection(cwd) {
@@ -7015,8 +7702,8 @@ function appendHook(existing, command) {
7015
7702
  return `${existing} && ${command}`;
7016
7703
  }
7017
7704
  function wireBuildScripts(cwd, packageManager) {
7018
- const target = path12.join(cwd, "package.json");
7019
- const raw = readFileSync13(target, "utf8");
7705
+ const target = path13.join(cwd, "package.json");
7706
+ const raw = readFileSync14(target, "utf8");
7020
7707
  const pkg = JSON.parse(raw);
7021
7708
  const scripts = pkg.scripts ?? {};
7022
7709
  const build = scripts.build;
@@ -7170,6 +7857,15 @@ Usage:
7170
7857
  demonstrations, not production).
7171
7858
  --check verifies the guard is wired (exit 1
7172
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).
7173
7869
  patchstack-connect demo node-serialize [--url URL] Run the production-backed node-serialize
7174
7870
  walkthrough: verify the vulnerable package,
7175
7871
  scan it, wait for live rule 18843, install +
@@ -7182,6 +7878,20 @@ Usage:
7182
7878
  what's missing, with tailored commands), then
7183
7879
  print the full setup guide. --full prints the
7184
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
7185
7895
  patchstack-connect login [--wait] Recover this site's credential when
7186
7896
  .patchstackrc.local.json has been lost. Needs the
7187
7897
  site UUID, which lives in the committed
@@ -7214,6 +7924,10 @@ Options (for scan, setup, status, and uninstall):
7214
7924
  same under a workspace that pins its own copy), so an
7215
7925
  advisory can be matched to the copy your code actually
7216
7926
  loads. Off by default; never source file paths
7927
+ --claim-token <token> (scan, setup) Connect the site to the Patchstack account
7928
+ that issued the token \u2014 it comes from the dashboard's
7929
+ "Connect website" prompt. PATCHSTACK_CLAIM_TOKEN works
7930
+ too. Never written to a file, never printed back
7217
7931
 
7218
7932
  Options (for mark-build):
7219
7933
  --dir <path> Build output directory (default: auto-detect
@@ -7247,7 +7961,7 @@ Examples:
7247
7961
  npx @patchstack/connect demo node-serialize
7248
7962
  npx @patchstack/connect demo-guide node-serialize
7249
7963
  `;
7250
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url", "out"]);
7964
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url", "out", "claim-token"]);
7251
7965
  function parseArgs(argv) {
7252
7966
  const args = argv.slice(2);
7253
7967
  const positional = [];
@@ -7295,6 +8009,107 @@ async function runInit(args) {
7295
8009
  console.log("Next: run `npx @patchstack/connect scan` to send your first manifest.");
7296
8010
  return 0;
7297
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
+ }
7298
8113
  async function runLogin(args) {
7299
8114
  if (process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false") {
7300
8115
  console.error("`login` is interactive and cannot run in CI. Set PATCHSTACK_PULSE_AUTH instead.");
@@ -7381,6 +8196,7 @@ async function runScan(args, options = {}) {
7381
8196
  cwd: process.cwd(),
7382
8197
  cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
7383
8198
  cliEndpoint: getStringFlag(args.flags, "endpoint"),
8199
+ cliClaimToken: getStringFlag(args.flags, "claim-token"),
7384
8200
  // The one command that reports them, so the one command that resolves them.
7385
8201
  detectSiteIdentity: true
7386
8202
  });
@@ -7421,6 +8237,9 @@ async function runScan(args, options = {}) {
7421
8237
  if (typeof body.name === "string") {
7422
8238
  console.log(`Reporting this app's name as "${body.name}".`);
7423
8239
  }
8240
+ if (typeof config.claimToken === "string" && config.claimToken !== "") {
8241
+ console.log("A claim token is set: the site will be connected to the Patchstack account that issued it.");
8242
+ }
7424
8243
  if (dryRun) {
7425
8244
  console.log("");
7426
8245
  if (config.siteUuid === null) {
@@ -7475,17 +8294,27 @@ async function runScan(args, options = {}) {
7475
8294
  } else {
7476
8295
  console.log(`Server response: ${response.message ?? JSON.stringify(response)}`);
7477
8296
  }
8297
+ const claimLines = claimOutcomeLines(response.claim, config);
8298
+ if (claimLines.length > 0) {
8299
+ console.log("");
8300
+ for (const line of claimLines) console.log(line);
8301
+ }
8302
+ const connected = response.claim?.state === "claimed" || response.claim?.state === "owned-by-you";
7478
8303
  const effectiveUuid = config.siteUuid ?? response.uuid ?? null;
7479
8304
  if (config.widget && effectiveUuid !== null && effectiveUuid.length > 0) {
7480
8305
  reportSourceWidget(effectiveUuid);
7481
8306
  }
7482
- if (provisioning && response.uuid !== void 0 && response.uuid.length > 0) {
8307
+ const linkUuid = response.uuid ?? config.siteUuid;
8308
+ if (!connected && (provisioning || claimLines.length > 0) && linkUuid !== null && linkUuid !== void 0 && linkUuid.length > 0) {
7483
8309
  console.log("");
7484
8310
  console.log("Open this dashboard link to view vulnerability reports:");
7485
- console.log(` ${buildClaimUrl(config.endpoint, response.uuid)}`);
8311
+ console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`);
7486
8312
  if (config.endpoint !== DEFAULT_ENDPOINT) {
7487
8313
  console.log(" (this URL inherits the endpoint override above)");
7488
8314
  }
8315
+ console.log("");
8316
+ console.log("Or attach it to your account from this terminal:");
8317
+ console.log(" npx @patchstack/connect claim");
7489
8318
  }
7490
8319
  if (options.showRemainingSetup !== false) {
7491
8320
  try {
@@ -7573,6 +8402,11 @@ function reportSourceMarker(framework) {
7573
8402
  }
7574
8403
  }
7575
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
+ }
7576
8410
  if (args.flags.get("check") === true) {
7577
8411
  const report = runVerify(process.cwd());
7578
8412
  console.log(`patchstack protect --check (${report.stack}):`);
@@ -7591,7 +8425,12 @@ async function runProtectCommand(args) {
7591
8425
  if (report.checks.some((c) => c.unverifiable)) {
7592
8426
  console.log("One or more checks could not be answered from here \u2014 see the `?` lines above.");
7593
8427
  }
7594
- 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);
7595
8434
  }
7596
8435
  const demo = args.flags.get("demo") === true;
7597
8436
  try {
@@ -7714,7 +8553,7 @@ async function runGuide(args) {
7714
8553
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
7715
8554
  console.log("");
7716
8555
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
7717
- console.log(readFileSync14(guidePath, "utf8"));
8556
+ console.log(readFileSync15(guidePath, "utf8"));
7718
8557
  return 0;
7719
8558
  }
7720
8559
  async function runSetup(args) {
@@ -7788,6 +8627,7 @@ async function runStatus(args) {
7788
8627
  console.log(`Environment: ${config.environment}`);
7789
8628
  if (config.siteUuid !== null) {
7790
8629
  console.log(`Dashboard URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
8630
+ console.log(" Not attached to an account yet? Run `npx @patchstack/connect claim`.");
7791
8631
  switch (await fetchSiteStatus(config)) {
7792
8632
  case "active":
7793
8633
  console.log("Site status: active on Patchstack");
@@ -7907,7 +8747,7 @@ async function runMarkBuild(args) {
7907
8747
  let marked = 0;
7908
8748
  let widgetTouched = 0;
7909
8749
  for (const file of files) {
7910
- const before = readFileSync14(file, "utf8");
8750
+ const before = readFileSync15(file, "utf8");
7911
8751
  let after = injectMarker(before, snippet);
7912
8752
  if (widgetUuid !== null) {
7913
8753
  const ensured = ensureWidgetInHtml(after, widgetUuid);
@@ -7969,6 +8809,8 @@ async function main() {
7969
8809
  return runSetup(args);
7970
8810
  case "map":
7971
8811
  return runMap(args.flags);
8812
+ case "claim":
8813
+ return runClaim(args);
7972
8814
  case "login":
7973
8815
  return runLogin(args);
7974
8816
  default: