@patchstack/connect 0.3.25 → 0.3.27

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 readFileSync12, writeFileSync as writeFileSync7 } from "fs";
4
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
5
5
 
6
6
  // src/parsers/index.ts
7
7
  import { access } from "fs/promises";
@@ -931,8 +931,8 @@ function computeManifestChecksum(packages) {
931
931
  var TOKEN_SKEW_MS = 6e4;
932
932
  function buildTokenUrl(manifestEndpoint) {
933
933
  const url = new URL(manifestEndpoint);
934
- const path11 = url.pathname.replace(/\/$/, "");
935
- url.pathname = path11.endsWith("/manifest") ? `${path11.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
934
+ const path12 = url.pathname.replace(/\/$/, "");
935
+ url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
936
936
  url.search = "";
937
937
  url.hash = "";
938
938
  return url.toString();
@@ -1018,8 +1018,8 @@ function buildEndpointUrl(base, siteUuid) {
1018
1018
  }
1019
1019
  function buildRulesUrl(manifestEndpoint, siteUuid) {
1020
1020
  const url = new URL(manifestEndpoint);
1021
- const path11 = url.pathname.replace(/\/$/, "");
1022
- url.pathname = path11.endsWith("/manifest") ? `${path11.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
1021
+ const path12 = url.pathname.replace(/\/$/, "");
1022
+ url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
1023
1023
  url.search = "";
1024
1024
  url.hash = "";
1025
1025
  return url.toString();
@@ -1034,16 +1034,16 @@ function buildSettingsUrl(endpoint, siteUuid) {
1034
1034
  }
1035
1035
  function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
1036
1036
  const url = new URL(manifestEndpoint);
1037
- const path11 = url.pathname.replace(/\/$/, "");
1038
- url.pathname = path11.endsWith("/manifest") ? `${path11.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1037
+ const path12 = url.pathname.replace(/\/$/, "");
1038
+ url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1039
1039
  url.search = "";
1040
1040
  url.hash = "";
1041
1041
  return url.toString();
1042
1042
  }
1043
1043
  function buildInputMapUrl(manifestEndpoint, siteUuid) {
1044
1044
  const url = new URL(manifestEndpoint);
1045
- const path11 = url.pathname.replace(/\/$/, "");
1046
- url.pathname = path11.endsWith("/manifest") ? `${path11.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1045
+ const path12 = url.pathname.replace(/\/$/, "");
1046
+ url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1047
1047
  url.search = "";
1048
1048
  url.hash = "";
1049
1049
  return url.toString();
@@ -1457,13 +1457,9 @@ async function persistSiteUuid(cwd, siteUuid) {
1457
1457
  return writeConfigFile(cwd, { ...existing, siteUuid });
1458
1458
  }
1459
1459
  async function persistApiKey(cwd, apiKey) {
1460
- const existing = await readConfigFile(cwd);
1460
+ const { pulseAuth: _dropped, ...existing } = await readConfigFile(cwd);
1461
1461
  return writeConfigFile(cwd, { ...existing, apiKey });
1462
1462
  }
1463
- async function persistPulseAuth(cwd, pulseAuth) {
1464
- const existing = await readConfigFile(cwd);
1465
- return writeConfigFile(cwd, { ...existing, pulseAuth });
1466
- }
1467
1463
  async function readConfigFile(cwd) {
1468
1464
  const target = path6.join(cwd, CONFIG_FILENAME);
1469
1465
  let raw;
@@ -2953,17 +2949,40 @@ function renderGuideChecklist(state, useColor) {
2953
2949
  }
2954
2950
 
2955
2951
  // src/login.ts
2952
+ import { mkdtempSync, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync6 } from "fs";
2953
+ import { tmpdir } from "os";
2954
+ import path10 from "path";
2955
+ import { createHash as createHash2 } from "crypto";
2956
2956
  function baseFrom(manifestEndpoint) {
2957
2957
  const url = new URL(manifestEndpoint);
2958
- const path11 = url.pathname.replace(/\/$/, "");
2959
- url.pathname = path11.endsWith("/manifest") ? path11.slice(0, -"/manifest".length) : "/monitor/pulse";
2958
+ const p = url.pathname.replace(/\/$/, "");
2959
+ url.pathname = p.endsWith("/manifest") ? p.slice(0, -"/manifest".length) : "/monitor/pulse";
2960
2960
  url.search = "";
2961
2961
  url.hash = "";
2962
2962
  return url.toString().replace(/\/$/, "");
2963
2963
  }
2964
- async function login(config, onPrompt, deps = {}) {
2964
+ function pendingPath(siteUuid) {
2965
+ const key = createHash2("sha256").update(siteUuid).digest("hex").slice(0, 16);
2966
+ return path10.join(tmpdir(), `patchstack-login-${key}.json`);
2967
+ }
2968
+ function savePendingLogin(siteUuid, pending) {
2969
+ writeFileSync6(pendingPath(siteUuid), JSON.stringify(pending), { encoding: "utf8", mode: 384 });
2970
+ }
2971
+ function readPendingLogin(siteUuid) {
2972
+ try {
2973
+ return JSON.parse(readFileSync6(pendingPath(siteUuid), "utf8"));
2974
+ } catch {
2975
+ return null;
2976
+ }
2977
+ }
2978
+ function clearPendingLogin(siteUuid) {
2979
+ try {
2980
+ rmSync(pendingPath(siteUuid));
2981
+ } catch {
2982
+ }
2983
+ }
2984
+ async function startLogin(config, deps = {}) {
2965
2985
  const fetchImpl = deps.fetchImpl ?? fetch;
2966
- const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2967
2986
  const now = deps.now ?? (() => Date.now());
2968
2987
  if (config.siteUuid === null) {
2969
2988
  return { status: "failed", message: "No site UUID configured \u2014 run `patchstack-connect scan` first." };
@@ -2986,29 +3005,72 @@ async function login(config, onPrompt, deps = {}) {
2986
3005
  if (!started.ok) {
2987
3006
  return { status: "failed", message: `Could not start the login (HTTP ${started.status}).` };
2988
3007
  }
2989
- const { device_code: deviceCode, user_code: userCode, expires_in: expiresIn, interval } = await started.json();
2990
- const verificationUri = `${base}/device?code=${encodeURIComponent(userCode)}`;
2991
- onPrompt(userCode, verificationUri);
2992
- const deadline = now() + expiresIn * 1e3;
2993
- const intervalMs = Math.max(1, interval) * 1e3;
3008
+ const body = await started.json();
3009
+ const pending = {
3010
+ deviceCode: body.device_code,
3011
+ userCode: body.user_code,
3012
+ // Points at the API, which redirects to the dashboard SPA — the CLI only
3013
+ // knows the API origin, and the approval page lives on the app.
3014
+ verificationUri: `${base}/device?code=${encodeURIComponent(body.user_code)}`,
3015
+ expiresAt: now() + body.expires_in * 1e3,
3016
+ intervalMs: Math.max(1, body.interval) * 1e3
3017
+ };
3018
+ savePendingLogin(config.siteUuid, pending);
3019
+ return { status: "started", pending };
3020
+ }
3021
+ async function redeemIfApproved(config, pending, deps = {}) {
3022
+ const fetchImpl = deps.fetchImpl ?? fetch;
3023
+ const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
3024
+ method: "POST",
3025
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
3026
+ body: JSON.stringify({ device_code: pending.deviceCode })
3027
+ });
3028
+ if (polled.status === 428) return "pending";
3029
+ if (!polled.ok) {
3030
+ if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
3031
+ return "expired";
3032
+ }
3033
+ const { api_key: apiKey } = await polled.json();
3034
+ if (typeof apiKey !== "string" || apiKey.length === 0) return "expired";
3035
+ await persistApiKey(process.cwd(), apiKey);
3036
+ if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
3037
+ return "approved";
3038
+ }
3039
+ async function waitForApproval(config, pending, deps = {}) {
3040
+ const fetchImpl = deps.fetchImpl ?? fetch;
3041
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
3042
+ const now = deps.now ?? (() => Date.now());
3043
+ const deadline = Math.min(pending.expiresAt, deps.until ?? pending.expiresAt);
3044
+ const expired = { status: "expired", message: "The login request expired. Run the command again." };
2994
3045
  while (now() < deadline) {
2995
- await sleep(intervalMs);
2996
- const polled = await fetchImpl(`${base}/device/token`, {
3046
+ await sleep(pending.intervalMs);
3047
+ const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
2997
3048
  method: "POST",
2998
3049
  headers: { "Content-Type": "application/json", Accept: "application/json" },
2999
- body: JSON.stringify({ device_code: deviceCode })
3050
+ body: JSON.stringify({ device_code: pending.deviceCode })
3000
3051
  });
3001
3052
  if (polled.status === 428) continue;
3002
- if (!polled.ok) return { status: "expired", message: "The login request expired. Run the command again." };
3053
+ if (!polled.ok) {
3054
+ if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
3055
+ return expired;
3056
+ }
3003
3057
  const { api_key: apiKey } = await polled.json();
3004
3058
  if (typeof apiKey !== "string" || apiKey.length === 0) {
3005
3059
  return { status: "failed", message: "Patchstack approved the request but returned no credential." };
3006
3060
  }
3007
- await persistPulseAuth(process.cwd(), apiKey);
3008
3061
  await persistApiKey(process.cwd(), apiKey);
3009
- return { status: "approved", userCode, verificationUri };
3062
+ if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
3063
+ return { status: "approved", userCode: pending.userCode, verificationUri: pending.verificationUri };
3064
+ }
3065
+ return expired;
3066
+ }
3067
+ async function login(config, onPrompt, deps = {}) {
3068
+ const started = await startLogin(config, deps);
3069
+ if (started.status !== "started" || started.pending === void 0) {
3070
+ return { status: started.status === "started" ? "failed" : started.status, message: started.message };
3010
3071
  }
3011
- return { status: "expired", message: "The login request expired. Run the command again." };
3072
+ onPrompt(started.pending.userCode, started.pending.verificationUri);
3073
+ return waitForApproval(config, started.pending, deps);
3012
3074
  }
3013
3075
 
3014
3076
  // src/map/ts-loader.ts
@@ -3032,8 +3094,8 @@ async function loadTypeScript(cwd) {
3032
3094
  }
3033
3095
 
3034
3096
  // src/map/extract.ts
3035
- import { readFileSync as readFileSync10, realpathSync as realpathSync3 } from "fs";
3036
- import { createHash as createHash3 } from "crypto";
3097
+ import { readFileSync as readFileSync11, realpathSync as realpathSync3 } from "fs";
3098
+ import { createHash as createHash4 } from "crypto";
3037
3099
  import { relative as relative4 } from "path";
3038
3100
 
3039
3101
  // src/map/ast.ts
@@ -3288,7 +3350,7 @@ function npmPackageOf(spec) {
3288
3350
  }
3289
3351
 
3290
3352
  // src/map/sources.ts
3291
- import { readFileSync as readFileSync6, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
3353
+ import { readFileSync as readFileSync7, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
3292
3354
  import { join as join12, relative as relative2, isAbsolute } from "path";
3293
3355
 
3294
3356
  // src/map/routes.ts
@@ -3373,7 +3435,7 @@ function hasEntrySignal(text) {
3373
3435
  }
3374
3436
  function detectFramework(cwd) {
3375
3437
  try {
3376
- const pkg = JSON.parse(readFileSync6(join12(cwd, "package.json"), "utf8"));
3438
+ const pkg = JSON.parse(readFileSync7(join12(cwd, "package.json"), "utf8"));
3377
3439
  const d = { ...pkg.dependencies, ...pkg.devDependencies };
3378
3440
  if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
3379
3441
  if (d["next"]) return "next";
@@ -3501,10 +3563,10 @@ function detectDeploymentShapes(cwd, opts = {}) {
3501
3563
  boundary = realpathSync(boundary);
3502
3564
  } catch {
3503
3565
  }
3504
- const inProject = (path11) => {
3566
+ const inProject = (path12) => {
3505
3567
  if (opts.followOutside) return true;
3506
3568
  try {
3507
- return isInside(realpathSync(path11), boundary);
3569
+ return isInside(realpathSync(path12), boundary);
3508
3570
  } catch {
3509
3571
  return false;
3510
3572
  }
@@ -3570,7 +3632,7 @@ function holdsSourceFile(dir) {
3570
3632
  }
3571
3633
 
3572
3634
  // src/map/surface.ts
3573
- import { readFileSync as readFileSync7 } from "fs";
3635
+ import { readFileSync as readFileSync8 } from "fs";
3574
3636
  import { join as join13 } from "path";
3575
3637
  var STATIC_GENERATORS = [
3576
3638
  { dep: "vite", label: "vite" },
@@ -3609,7 +3671,7 @@ var SHAPE_SIGNAL = {
3609
3671
  };
3610
3672
  function readManifest(cwd) {
3611
3673
  try {
3612
- const parsed = JSON.parse(readFileSync7(join13(cwd, "package.json"), "utf8"));
3674
+ const parsed = JSON.parse(readFileSync8(join13(cwd, "package.json"), "utf8"));
3613
3675
  return typeof parsed === "object" && parsed !== null ? parsed : null;
3614
3676
  } catch {
3615
3677
  return null;
@@ -3622,7 +3684,7 @@ function nextExportsStatically(cwd, manifest, ts) {
3622
3684
  for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
3623
3685
  let text;
3624
3686
  try {
3625
- text = readFileSync7(join13(cwd, file), "utf8");
3687
+ text = readFileSync8(join13(cwd, file), "utf8");
3626
3688
  } catch {
3627
3689
  continue;
3628
3690
  }
@@ -3741,7 +3803,7 @@ function classifyServerSurface(cwd, endpointCount, deploymentShapes, ts) {
3741
3803
  }
3742
3804
  function surfaceNote(surface) {
3743
3805
  if (surface.state === "server-runtime-detected") {
3744
- return "serverSurface: a server runtime was recognized in the analysed source (see its evidence). Request-path protection applies to this app.";
3806
+ return "serverSurface: a server runtime was recognized in the analysed source (see its evidence). Request-path shielding is APPLICABLE to these paths \u2014 this is not a statement that a guard is installed, fetching rules, or enforcing them.";
3745
3807
  }
3746
3808
  if (surface.state === "static-build-detected") {
3747
3809
  return "serverSurface: a static build was identified and no server runtime was recognized in the analysed source. This is NOT deployment attestation \u2014 it describes the source, not what is deployed, and a serverless function added at the platform level would not appear here. Advisories against this app are dependency and bundle concerns rather than request-path risk.";
@@ -3750,7 +3812,7 @@ function surfaceNote(surface) {
3750
3812
  }
3751
3813
 
3752
3814
  // src/map/sinks.ts
3753
- import { createHash as createHash2 } from "crypto";
3815
+ import { createHash as createHash3 } from "crypto";
3754
3816
  var DB_OPS = /* @__PURE__ */ new Set(["insert", "update", "delete", "select", "upsert", "rpc"]);
3755
3817
  var PRISMA_OPS = /* @__PURE__ */ new Set(["create", "createMany", "update", "updateMany", "delete", "deleteMany", "upsert", "findFirst", "findUnique", "findMany", "count", "aggregate"]);
3756
3818
  var FS_CALLS = /^(readFile|writeFile|readFileSync|writeFileSync|appendFile|createReadStream|createWriteStream|unlink|rm|rmSync|mkdir|readdir|stat|open)$/;
@@ -4006,7 +4068,7 @@ function argumentRoleOf(sinkKind, method, index, total = 0) {
4006
4068
  return table?.[index] ?? "unknown";
4007
4069
  }
4008
4070
  function sinkId(s, owner) {
4009
- return createHash2("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);
4071
+ 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);
4010
4072
  }
4011
4073
  function dedupeSinks(sinks, owner) {
4012
4074
  const seen = /* @__PURE__ */ new Set();
@@ -4022,7 +4084,7 @@ function dedupeSinks(sinks, owner) {
4022
4084
  }
4023
4085
 
4024
4086
  // src/map/module-graph.ts
4025
- import { readFileSync as readFileSync8, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
4087
+ import { readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
4026
4088
  import { dirname as dirname4, join as join14, relative as relative3, resolve as resolvePath } from "path";
4027
4089
  var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
4028
4090
  function createModuleGraph(ts, opts) {
@@ -4031,7 +4093,7 @@ function createModuleGraph(ts, opts) {
4031
4093
  if (cache.has(file)) return cache.get(file) ?? null;
4032
4094
  let entry = null;
4033
4095
  try {
4034
- const text = readFileSync8(file, "utf8");
4096
+ const text = readFileSync9(file, "utf8");
4035
4097
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
4036
4098
  const bindings = buildModuleBindings(sf, ts);
4037
4099
  entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
@@ -4130,8 +4192,8 @@ function collectCallees(sf, ts) {
4130
4192
  }
4131
4193
 
4132
4194
  // src/map/coordinates.ts
4133
- function runtimeCoordinate(source, path11) {
4134
- if (/\[\d*\]/.test(path11)) {
4195
+ function runtimeCoordinate(source, path12) {
4196
+ if (/\[\d*\]/.test(path12)) {
4135
4197
  return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
4136
4198
  }
4137
4199
  switch (source) {
@@ -4140,15 +4202,15 @@ function runtimeCoordinate(source, path11) {
4140
4202
  case "multipart":
4141
4203
  case "body":
4142
4204
  case "server-fn-data":
4143
- return { runtimeParameter: `post.${path11}` };
4205
+ return { runtimeParameter: `post.${path12}` };
4144
4206
  case "query":
4145
- return { runtimeParameter: `get.${path11}` };
4207
+ return { runtimeParameter: `get.${path12}` };
4146
4208
  case "cookie":
4147
- return { runtimeParameter: `cookie.${path11}` };
4209
+ return { runtimeParameter: `cookie.${path12}` };
4148
4210
  case "file":
4149
- return { runtimeParameter: `files.${path11}` };
4211
+ return { runtimeParameter: `files.${path12}` };
4150
4212
  case "header":
4151
- return { runtimeParameter: `server.HTTP_${path11.toUpperCase().replace(/-/g, "_")}` };
4213
+ return { runtimeParameter: `server.HTTP_${path12.toUpperCase().replace(/-/g, "_")}` };
4152
4214
  case "route-param":
4153
4215
  return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
4154
4216
  default:
@@ -4180,8 +4242,8 @@ function addressSpaceOf(source) {
4180
4242
  function isProvenFlow(confidence) {
4181
4243
  return confidence === "exact-local" || confidence === "transformed-local";
4182
4244
  }
4183
- function inputIdOf(source, path11) {
4184
- return `${addressSpaceOf(source)}:${path11}`;
4245
+ function inputIdOf(source, path12) {
4246
+ return `${addressSpaceOf(source)}:${path12}`;
4185
4247
  }
4186
4248
  function withCoordinates(fields, source) {
4187
4249
  return fields.map((f) => {
@@ -4380,8 +4442,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts) {
4380
4442
  if (!bodyNode || sinks.length === 0 || inputs.length === 0) return { flows: [], limitations: [] };
4381
4443
  const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
4382
4444
  const rootPath = /* @__PURE__ */ new Map();
4383
- const addRoot = (name, path11, space) => {
4384
- if (!rootPath.has(name)) rootPath.set(name, { path: path11, space });
4445
+ const addRoot = (name, path12, space) => {
4446
+ if (!rootPath.has(name)) rootPath.set(name, { path: path12, space });
4385
4447
  };
4386
4448
  for (const p of params ?? []) {
4387
4449
  if (!p?.name) continue;
@@ -4526,8 +4588,8 @@ function dedupeLimitations(list) {
4526
4588
  function join22(base, seg) {
4527
4589
  return base ? `${base}.${seg}` : seg;
4528
4590
  }
4529
- function normalizePath(path11) {
4530
- return path11.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
4591
+ function normalizePath(path12) {
4592
+ return path12.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
4531
4593
  }
4532
4594
  function fluentChainCalls(call, ts) {
4533
4595
  let root = call;
@@ -4773,7 +4835,7 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
4773
4835
  }
4774
4836
 
4775
4837
  // src/map/imports.ts
4776
- import { readFileSync as readFileSync9 } from "fs";
4838
+ import { readFileSync as readFileSync10 } from "fs";
4777
4839
  import { join as join15 } from "path";
4778
4840
  var MAX_SITES = 5;
4779
4841
  var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
@@ -4785,7 +4847,7 @@ function readPathAliases(cwd) {
4785
4847
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
4786
4848
  let paths;
4787
4849
  try {
4788
- const raw = readFileSync9(join15(cwd, name), "utf8");
4850
+ const raw = readFileSync10(join15(cwd, name), "utf8");
4789
4851
  const parsed = JSON.parse(stripJsonComments(raw));
4790
4852
  paths = parsed.compilerOptions?.paths;
4791
4853
  } catch {
@@ -5164,7 +5226,7 @@ async function extractInputMap(cwd, ts, options = {}) {
5164
5226
  const hopTargets = /* @__PURE__ */ new Set();
5165
5227
  for (const file of files) {
5166
5228
  try {
5167
- const text = readFileSync10(file, "utf8");
5229
+ const text = readFileSync11(file, "utf8");
5168
5230
  const relFile = relative4(cwd, file);
5169
5231
  sourceBytes += text.length;
5170
5232
  if (!hasEntrySignal(text)) {
@@ -5177,7 +5239,7 @@ async function extractInputMap(cwd, ts, options = {}) {
5177
5239
  }
5178
5240
  parsed++;
5179
5241
  entryFiles.add(file);
5180
- const fingerprint = createHash3("sha256").update(text).digest("hex").slice(0, 16);
5242
+ const fingerprint = createHash4("sha256").update(text).digest("hex").slice(0, 16);
5181
5243
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
5182
5244
  const bindings = buildModuleBindings(sf, ts);
5183
5245
  for (const specifier of bindings.imports) {
@@ -5219,7 +5281,7 @@ async function extractInputMap(cwd, ts, options = {}) {
5219
5281
  for (const target of hopTargets) {
5220
5282
  if (entryFiles.has(target)) continue;
5221
5283
  try {
5222
- const text = readFileSync10(target, "utf8");
5284
+ const text = readFileSync11(target, "utf8");
5223
5285
  const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
5224
5286
  const bindings = buildModuleBindings(sf, ts);
5225
5287
  const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative4(cwd, target), graph });
@@ -5350,8 +5412,8 @@ async function buildInputMap(cwd, options = {}) {
5350
5412
  }
5351
5413
 
5352
5414
  // src/setup.ts
5353
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
5354
- import path10 from "path";
5415
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
5416
+ import path11 from "path";
5355
5417
  var SCAN_COMMAND = "patchstack-connect scan";
5356
5418
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
5357
5419
  function setupProtection(cwd) {
@@ -5369,8 +5431,8 @@ function appendHook(existing, command) {
5369
5431
  return `${existing} && ${command}`;
5370
5432
  }
5371
5433
  function wireBuildScripts(cwd, packageManager) {
5372
- const target = path10.join(cwd, "package.json");
5373
- const raw = readFileSync11(target, "utf8");
5434
+ const target = path11.join(cwd, "package.json");
5435
+ const raw = readFileSync12(target, "utf8");
5374
5436
  const pkg = JSON.parse(raw);
5375
5437
  const scripts = pkg.scripts ?? {};
5376
5438
  const build = scripts.build;
@@ -5419,7 +5481,7 @@ function wireBuildScripts(cwd, packageManager) {
5419
5481
  const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
5420
5482
  const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
5421
5483
  const trailingNewline = raw.endsWith("\n") ? "\n" : "";
5422
- writeFileSync6(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
5484
+ writeFileSync7(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
5423
5485
  if (build === void 0 || build.trim().length === 0) {
5424
5486
  return {
5425
5487
  changed: true,
@@ -5497,11 +5559,21 @@ Usage:
5497
5559
  what's missing, with tailored commands), then
5498
5560
  print the full setup guide. --full prints the
5499
5561
  guide even when setup is complete
5500
- patchstack-connect login [options] Recover this site's Patchstack credential when
5501
- .patchstackrc.json has been lost. Prints a short
5502
- code to approve in the dashboard; approving
5503
- rotates the credential, so the old one stops
5504
- working
5562
+ patchstack-connect login [--wait] Recover this site's credential when
5563
+ .patchstackrc.json has been lost. Prints a link
5564
+ for the site's OWNER to approve in the dashboard.
5565
+ In a terminal it then waits. When the output is
5566
+ piped or captured \u2014 an assistant running it \u2014 it
5567
+ prints the link and EXITS, so the link is visible
5568
+ immediately. Run it AGAIN once the user confirms
5569
+ they approved: it resumes the same request rather
5570
+ than starting a new one, and finishes the flow.
5571
+ --wait blocks instead of returning. Use this
5572
+ instead of deleting
5573
+ .patchstackrc.json and re-scanning, which would
5574
+ provision a second site. Approving ROTATES the
5575
+ credential: CI, deploys and other machines using
5576
+ the old one must be updated. Not usable in CI
5505
5577
  patchstack-connect help Print this message
5506
5578
 
5507
5579
  Options (for scan, setup, status, and uninstall):
@@ -5520,7 +5592,7 @@ Options (for demo and demo-guide):
5520
5592
  Environment:
5521
5593
  PATCHSTACK_SITE_UUID Site UUID
5522
5594
  PATCHSTACK_API_KEY WP-format site API key for block-log reporting (never put in the widget)
5523
- PATCHSTACK_PULSE_AUTH Credential for authenticated Pulse ingest (defaults to PATCHSTACK_API_KEY)
5595
+ PATCHSTACK_PULSE_AUTH Only if Pulse ingest uses a different credential from block-logs
5524
5596
  PATCHSTACK_TELEMETRY Set to off to disable block-log reporting
5525
5597
  PATCHSTACK_API_BASE API origin for /oauth/token and /api/logs/log (default: https://api.patchstack.com)
5526
5598
  PATCHSTACK_ENDPOINT API endpoint (default: https://api.patchstack.com/monitor/pulse/manifest)
@@ -5602,17 +5674,66 @@ async function runLogin(args) {
5602
5674
  cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
5603
5675
  cliEndpoint: getStringFlag(args.flags, "endpoint")
5604
5676
  });
5605
- const result = await login(config, (userCode, verificationUri) => {
5677
+ const approved = () => {
5678
+ console.log("\n \u2713 Credential restored and saved to .patchstackrc.json.");
5679
+ console.log(" The previous credential no longer works. Update it anywhere else it was set:");
5680
+ console.log(" CI secrets, hosting env vars, preview environments, other checkouts.\n");
5681
+ return 0;
5682
+ };
5683
+ if (args.flags.has("wait")) {
5684
+ const pending = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
5685
+ if (pending === null) {
5686
+ console.error("\n No login is waiting for approval. Run `patchstack-connect login` first.\n");
5687
+ return 1;
5688
+ }
5689
+ const resumed = await waitForApproval(config, pending);
5690
+ if (resumed.status === "approved") return approved();
5691
+ console.error(`
5692
+ ${resumed.message ?? "Login failed."}
5693
+ `);
5694
+ return 1;
5695
+ }
5696
+ const prompt = (userCode, verificationUri) => {
5606
5697
  console.log(`
5607
5698
  Your code: ${userCode}`);
5608
5699
  console.log(` Approve at: ${verificationUri}
5609
5700
  `);
5610
- console.log(" Waiting for approval\u2026");
5611
- });
5612
- if (result.status === "approved") {
5613
- console.log("\n \u2713 Credential restored and saved to .patchstackrc.json.\n");
5701
+ console.log(" Open that link and approve it as the site's owner. Approving issues a new");
5702
+ console.log(" credential and stops the current one working \u2014 CI, deploys and any other");
5703
+ console.log(" machine using it will need the new value.\n");
5704
+ };
5705
+ if (process.stdout.isTTY !== true) {
5706
+ const existing = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
5707
+ if (existing !== null && Date.now() < existing.expiresAt) {
5708
+ const outcome = await redeemIfApproved(config, existing);
5709
+ if (outcome === "approved") return approved();
5710
+ if (outcome === "pending") {
5711
+ const secondsLeft = Math.round((existing.expiresAt - Date.now()) / 1e3);
5712
+ console.log(`
5713
+ Still waiting for approval of code ${existing.userCode}.`);
5714
+ console.log(` Approve at: ${existing.verificationUri}`);
5715
+ console.log(` (valid for another ${secondsLeft}s \u2014 run this again once the user confirms)
5716
+ `);
5717
+ return 0;
5718
+ }
5719
+ }
5720
+ const started = await startLogin(config);
5721
+ if (started.status !== "started" || started.pending === void 0) {
5722
+ console.error(`
5723
+ ${started.message ?? "Login failed."}
5724
+ `);
5725
+ return 1;
5726
+ }
5727
+ prompt(started.pending.userCode, started.pending.verificationUri);
5728
+ console.log(" Give that link to the user. When they confirm they have approved it, run");
5729
+ console.log(" this same command again (or `login --wait` to block until they do).\n");
5614
5730
  return 0;
5615
5731
  }
5732
+ const result = await login(config, (userCode, verificationUri) => {
5733
+ prompt(userCode, verificationUri);
5734
+ console.log(" Waiting for approval (the code expires in 10 minutes)\u2026");
5735
+ });
5736
+ if (result.status === "approved") return approved();
5616
5737
  console.error(`
5617
5738
  ${result.message ?? "Login failed."}
5618
5739
  `);
@@ -5661,7 +5782,7 @@ async function runMap(args) {
5661
5782
  const json = JSON.stringify(map, null, 2);
5662
5783
  const out = getStringFlag(args.flags, "out");
5663
5784
  if (out) {
5664
- writeFileSync7(out, json);
5785
+ writeFileSync8(out, json);
5665
5786
  console.error(`patchstack: wrote ${out}`);
5666
5787
  } else if (args.flags.get("upload") !== true) {
5667
5788
  console.log(json);
@@ -5745,7 +5866,6 @@ async function runScan(args, options = {}) {
5745
5866
  }
5746
5867
  if (typeof response.api_key === "string" && response.api_key.length > 0) {
5747
5868
  const target = await persistApiKey(process.cwd(), response.api_key);
5748
- await persistPulseAuth(process.cwd(), response.api_key);
5749
5869
  console.log(`Saved API key to ${target} (authenticates Pulse ingest and block-log reporting; keep out of the public widget).`);
5750
5870
  }
5751
5871
  if (response.stored) {
@@ -5948,7 +6068,7 @@ async function runGuide(args) {
5948
6068
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
5949
6069
  console.log("");
5950
6070
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
5951
- console.log(readFileSync12(guidePath, "utf8"));
6071
+ console.log(readFileSync13(guidePath, "utf8"));
5952
6072
  return 0;
5953
6073
  }
5954
6074
  async function runSetup(args) {
@@ -6129,7 +6249,7 @@ async function runMarkBuild(args) {
6129
6249
  let marked = 0;
6130
6250
  let widgetTouched = 0;
6131
6251
  for (const file of files) {
6132
- const before = readFileSync12(file, "utf8");
6252
+ const before = readFileSync13(file, "utf8");
6133
6253
  let after = injectMarker(before, snippet);
6134
6254
  if (widgetUuid !== null) {
6135
6255
  const ensured = ensureWidgetInHtml(after, widgetUuid);
@@ -6139,7 +6259,7 @@ async function runMarkBuild(args) {
6139
6259
  after = ensured.html;
6140
6260
  }
6141
6261
  if (after !== before) {
6142
- writeFileSync7(file, after);
6262
+ writeFileSync8(file, after);
6143
6263
  marked += 1;
6144
6264
  }
6145
6265
  }