@patchstack/connect 0.3.26 → 0.3.28

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();
@@ -1016,10 +1016,23 @@ function buildEndpointUrl(base, siteUuid) {
1016
1016
  const trimmed = base.replace(/\/$/, "");
1017
1017
  return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
1018
1018
  }
1019
+ function authFailureMessage(status, config) {
1020
+ const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
1021
+ if (status === 401 && !hasCredential) {
1022
+ return "Patchstack requires an API credential for this site and none is configured. Run `npx patchstack-connect login`, or set PATCHSTACK_API_KEY.";
1023
+ }
1024
+ if (status === 401) {
1025
+ return "Patchstack rejected this API credential. It may have expired, been revoked, or the site may no longer exist. Run `npx patchstack-connect login` to issue a new one.";
1026
+ }
1027
+ if (status === 403) {
1028
+ return "This API credential is not permitted to act on this site. Check that siteUuid in .patchstackrc.json matches the credential (a credential is issued for one site).";
1029
+ }
1030
+ return null;
1031
+ }
1019
1032
  function buildRulesUrl(manifestEndpoint, siteUuid) {
1020
1033
  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)}`;
1034
+ const path12 = url.pathname.replace(/\/$/, "");
1035
+ url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
1023
1036
  url.search = "";
1024
1037
  url.hash = "";
1025
1038
  return url.toString();
@@ -1034,16 +1047,16 @@ function buildSettingsUrl(endpoint, siteUuid) {
1034
1047
  }
1035
1048
  function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
1036
1049
  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)}`;
1050
+ const path12 = url.pathname.replace(/\/$/, "");
1051
+ url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1039
1052
  url.search = "";
1040
1053
  url.hash = "";
1041
1054
  return url.toString();
1042
1055
  }
1043
1056
  function buildInputMapUrl(manifestEndpoint, siteUuid) {
1044
1057
  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)}`;
1058
+ const path12 = url.pathname.replace(/\/$/, "");
1059
+ url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1047
1060
  url.search = "";
1048
1061
  url.hash = "";
1049
1062
  return url.toString();
@@ -1070,6 +1083,10 @@ async function postInputMap(config, map) {
1070
1083
  if (response.status === 422) {
1071
1084
  return { result: "failed", message: `Patchstack does not accept this map schema (version ${map.version}). Update @patchstack/connect.` };
1072
1085
  }
1086
+ const refused = authFailureMessage(response.status, config);
1087
+ if (refused !== null) {
1088
+ return { result: "failed", message: refused };
1089
+ }
1073
1090
  if (!response.ok) {
1074
1091
  return { result: "failed", message: `Patchstack returned ${response.status}.` };
1075
1092
  }
@@ -1100,6 +1117,13 @@ async function postPackageRemoved(config) {
1100
1117
  if (response.status === 404) {
1101
1118
  return { result: "gone", message: null };
1102
1119
  }
1120
+ if (response.status === 401 && await fetchSiteStatus(config) === "removed") {
1121
+ return { result: "gone", message: null };
1122
+ }
1123
+ const refused = authFailureMessage(response.status, config);
1124
+ if (refused !== null) {
1125
+ return { result: "failed", message: refused };
1126
+ }
1103
1127
  if (!response.ok) {
1104
1128
  return { result: "failed", message: `Patchstack returned ${response.status}.` };
1105
1129
  }
@@ -1181,6 +1205,10 @@ async function postManifest(config, payload) {
1181
1205
  "VALIDATION_ERROR"
1182
1206
  );
1183
1207
  }
1208
+ const refused = authFailureMessage(response.status, config);
1209
+ if (refused !== null) {
1210
+ throw new PatchstackError(refused, "UNAUTHORIZED");
1211
+ }
1184
1212
  if (response.status < 200 || response.status >= 300) {
1185
1213
  throw new PatchstackError(
1186
1214
  `Patchstack returned ${response.status}: ${text.slice(0, 200)}`,
@@ -2949,17 +2977,40 @@ function renderGuideChecklist(state, useColor) {
2949
2977
  }
2950
2978
 
2951
2979
  // src/login.ts
2980
+ import { mkdtempSync, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync6 } from "fs";
2981
+ import { tmpdir } from "os";
2982
+ import path10 from "path";
2983
+ import { createHash as createHash2 } from "crypto";
2952
2984
  function baseFrom(manifestEndpoint) {
2953
2985
  const url = new URL(manifestEndpoint);
2954
- const path11 = url.pathname.replace(/\/$/, "");
2955
- url.pathname = path11.endsWith("/manifest") ? path11.slice(0, -"/manifest".length) : "/monitor/pulse";
2986
+ const p = url.pathname.replace(/\/$/, "");
2987
+ url.pathname = p.endsWith("/manifest") ? p.slice(0, -"/manifest".length) : "/monitor/pulse";
2956
2988
  url.search = "";
2957
2989
  url.hash = "";
2958
2990
  return url.toString().replace(/\/$/, "");
2959
2991
  }
2960
- async function login(config, onPrompt, deps = {}) {
2992
+ function pendingPath(siteUuid) {
2993
+ const key = createHash2("sha256").update(siteUuid).digest("hex").slice(0, 16);
2994
+ return path10.join(tmpdir(), `patchstack-login-${key}.json`);
2995
+ }
2996
+ function savePendingLogin(siteUuid, pending) {
2997
+ writeFileSync6(pendingPath(siteUuid), JSON.stringify(pending), { encoding: "utf8", mode: 384 });
2998
+ }
2999
+ function readPendingLogin(siteUuid) {
3000
+ try {
3001
+ return JSON.parse(readFileSync6(pendingPath(siteUuid), "utf8"));
3002
+ } catch {
3003
+ return null;
3004
+ }
3005
+ }
3006
+ function clearPendingLogin(siteUuid) {
3007
+ try {
3008
+ rmSync(pendingPath(siteUuid));
3009
+ } catch {
3010
+ }
3011
+ }
3012
+ async function startLogin(config, deps = {}) {
2961
3013
  const fetchImpl = deps.fetchImpl ?? fetch;
2962
- const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2963
3014
  const now = deps.now ?? (() => Date.now());
2964
3015
  if (config.siteUuid === null) {
2965
3016
  return { status: "failed", message: "No site UUID configured \u2014 run `patchstack-connect scan` first." };
@@ -2982,28 +3033,72 @@ async function login(config, onPrompt, deps = {}) {
2982
3033
  if (!started.ok) {
2983
3034
  return { status: "failed", message: `Could not start the login (HTTP ${started.status}).` };
2984
3035
  }
2985
- const { device_code: deviceCode, user_code: userCode, expires_in: expiresIn, interval } = await started.json();
2986
- const verificationUri = `${base}/device?code=${encodeURIComponent(userCode)}`;
2987
- onPrompt(userCode, verificationUri);
2988
- const deadline = now() + expiresIn * 1e3;
2989
- const intervalMs = Math.max(1, interval) * 1e3;
3036
+ const body = await started.json();
3037
+ const pending = {
3038
+ deviceCode: body.device_code,
3039
+ userCode: body.user_code,
3040
+ // Points at the API, which redirects to the dashboard SPA — the CLI only
3041
+ // knows the API origin, and the approval page lives on the app.
3042
+ verificationUri: `${base}/device?code=${encodeURIComponent(body.user_code)}`,
3043
+ expiresAt: now() + body.expires_in * 1e3,
3044
+ intervalMs: Math.max(1, body.interval) * 1e3
3045
+ };
3046
+ savePendingLogin(config.siteUuid, pending);
3047
+ return { status: "started", pending };
3048
+ }
3049
+ async function redeemIfApproved(config, pending, deps = {}) {
3050
+ const fetchImpl = deps.fetchImpl ?? fetch;
3051
+ const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
3052
+ method: "POST",
3053
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
3054
+ body: JSON.stringify({ device_code: pending.deviceCode })
3055
+ });
3056
+ if (polled.status === 428) return "pending";
3057
+ if (!polled.ok) {
3058
+ if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
3059
+ return "expired";
3060
+ }
3061
+ const { api_key: apiKey } = await polled.json();
3062
+ if (typeof apiKey !== "string" || apiKey.length === 0) return "expired";
3063
+ await persistApiKey(process.cwd(), apiKey);
3064
+ if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
3065
+ return "approved";
3066
+ }
3067
+ async function waitForApproval(config, pending, deps = {}) {
3068
+ const fetchImpl = deps.fetchImpl ?? fetch;
3069
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
3070
+ const now = deps.now ?? (() => Date.now());
3071
+ const deadline = Math.min(pending.expiresAt, deps.until ?? pending.expiresAt);
3072
+ const expired = { status: "expired", message: "The login request expired. Run the command again." };
2990
3073
  while (now() < deadline) {
2991
- await sleep(intervalMs);
2992
- const polled = await fetchImpl(`${base}/device/token`, {
3074
+ await sleep(pending.intervalMs);
3075
+ const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
2993
3076
  method: "POST",
2994
3077
  headers: { "Content-Type": "application/json", Accept: "application/json" },
2995
- body: JSON.stringify({ device_code: deviceCode })
3078
+ body: JSON.stringify({ device_code: pending.deviceCode })
2996
3079
  });
2997
3080
  if (polled.status === 428) continue;
2998
- if (!polled.ok) return { status: "expired", message: "The login request expired. Run the command again." };
3081
+ if (!polled.ok) {
3082
+ if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
3083
+ return expired;
3084
+ }
2999
3085
  const { api_key: apiKey } = await polled.json();
3000
3086
  if (typeof apiKey !== "string" || apiKey.length === 0) {
3001
3087
  return { status: "failed", message: "Patchstack approved the request but returned no credential." };
3002
3088
  }
3003
3089
  await persistApiKey(process.cwd(), apiKey);
3004
- return { status: "approved", userCode, verificationUri };
3090
+ if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
3091
+ return { status: "approved", userCode: pending.userCode, verificationUri: pending.verificationUri };
3005
3092
  }
3006
- return { status: "expired", message: "The login request expired. Run the command again." };
3093
+ return expired;
3094
+ }
3095
+ async function login(config, onPrompt, deps = {}) {
3096
+ const started = await startLogin(config, deps);
3097
+ if (started.status !== "started" || started.pending === void 0) {
3098
+ return { status: started.status === "started" ? "failed" : started.status, message: started.message };
3099
+ }
3100
+ onPrompt(started.pending.userCode, started.pending.verificationUri);
3101
+ return waitForApproval(config, started.pending, deps);
3007
3102
  }
3008
3103
 
3009
3104
  // src/map/ts-loader.ts
@@ -3027,8 +3122,8 @@ async function loadTypeScript(cwd) {
3027
3122
  }
3028
3123
 
3029
3124
  // src/map/extract.ts
3030
- import { readFileSync as readFileSync10, realpathSync as realpathSync3 } from "fs";
3031
- import { createHash as createHash3 } from "crypto";
3125
+ import { readFileSync as readFileSync11, realpathSync as realpathSync3 } from "fs";
3126
+ import { createHash as createHash4 } from "crypto";
3032
3127
  import { relative as relative4 } from "path";
3033
3128
 
3034
3129
  // src/map/ast.ts
@@ -3283,7 +3378,7 @@ function npmPackageOf(spec) {
3283
3378
  }
3284
3379
 
3285
3380
  // src/map/sources.ts
3286
- import { readFileSync as readFileSync6, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
3381
+ import { readFileSync as readFileSync7, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
3287
3382
  import { join as join12, relative as relative2, isAbsolute } from "path";
3288
3383
 
3289
3384
  // src/map/routes.ts
@@ -3368,7 +3463,7 @@ function hasEntrySignal(text) {
3368
3463
  }
3369
3464
  function detectFramework(cwd) {
3370
3465
  try {
3371
- const pkg = JSON.parse(readFileSync6(join12(cwd, "package.json"), "utf8"));
3466
+ const pkg = JSON.parse(readFileSync7(join12(cwd, "package.json"), "utf8"));
3372
3467
  const d = { ...pkg.dependencies, ...pkg.devDependencies };
3373
3468
  if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
3374
3469
  if (d["next"]) return "next";
@@ -3496,10 +3591,10 @@ function detectDeploymentShapes(cwd, opts = {}) {
3496
3591
  boundary = realpathSync(boundary);
3497
3592
  } catch {
3498
3593
  }
3499
- const inProject = (path11) => {
3594
+ const inProject = (path12) => {
3500
3595
  if (opts.followOutside) return true;
3501
3596
  try {
3502
- return isInside(realpathSync(path11), boundary);
3597
+ return isInside(realpathSync(path12), boundary);
3503
3598
  } catch {
3504
3599
  return false;
3505
3600
  }
@@ -3565,7 +3660,7 @@ function holdsSourceFile(dir) {
3565
3660
  }
3566
3661
 
3567
3662
  // src/map/surface.ts
3568
- import { readFileSync as readFileSync7 } from "fs";
3663
+ import { readFileSync as readFileSync8 } from "fs";
3569
3664
  import { join as join13 } from "path";
3570
3665
  var STATIC_GENERATORS = [
3571
3666
  { dep: "vite", label: "vite" },
@@ -3604,7 +3699,7 @@ var SHAPE_SIGNAL = {
3604
3699
  };
3605
3700
  function readManifest(cwd) {
3606
3701
  try {
3607
- const parsed = JSON.parse(readFileSync7(join13(cwd, "package.json"), "utf8"));
3702
+ const parsed = JSON.parse(readFileSync8(join13(cwd, "package.json"), "utf8"));
3608
3703
  return typeof parsed === "object" && parsed !== null ? parsed : null;
3609
3704
  } catch {
3610
3705
  return null;
@@ -3617,7 +3712,7 @@ function nextExportsStatically(cwd, manifest, ts) {
3617
3712
  for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
3618
3713
  let text;
3619
3714
  try {
3620
- text = readFileSync7(join13(cwd, file), "utf8");
3715
+ text = readFileSync8(join13(cwd, file), "utf8");
3621
3716
  } catch {
3622
3717
  continue;
3623
3718
  }
@@ -3745,7 +3840,7 @@ function surfaceNote(surface) {
3745
3840
  }
3746
3841
 
3747
3842
  // src/map/sinks.ts
3748
- import { createHash as createHash2 } from "crypto";
3843
+ import { createHash as createHash3 } from "crypto";
3749
3844
  var DB_OPS = /* @__PURE__ */ new Set(["insert", "update", "delete", "select", "upsert", "rpc"]);
3750
3845
  var PRISMA_OPS = /* @__PURE__ */ new Set(["create", "createMany", "update", "updateMany", "delete", "deleteMany", "upsert", "findFirst", "findUnique", "findMany", "count", "aggregate"]);
3751
3846
  var FS_CALLS = /^(readFile|writeFile|readFileSync|writeFileSync|appendFile|createReadStream|createWriteStream|unlink|rm|rmSync|mkdir|readdir|stat|open)$/;
@@ -4001,7 +4096,7 @@ function argumentRoleOf(sinkKind, method, index, total = 0) {
4001
4096
  return table?.[index] ?? "unknown";
4002
4097
  }
4003
4098
  function sinkId(s, owner) {
4004
- 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);
4099
+ 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);
4005
4100
  }
4006
4101
  function dedupeSinks(sinks, owner) {
4007
4102
  const seen = /* @__PURE__ */ new Set();
@@ -4017,7 +4112,7 @@ function dedupeSinks(sinks, owner) {
4017
4112
  }
4018
4113
 
4019
4114
  // src/map/module-graph.ts
4020
- import { readFileSync as readFileSync8, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
4115
+ import { readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
4021
4116
  import { dirname as dirname4, join as join14, relative as relative3, resolve as resolvePath } from "path";
4022
4117
  var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
4023
4118
  function createModuleGraph(ts, opts) {
@@ -4026,7 +4121,7 @@ function createModuleGraph(ts, opts) {
4026
4121
  if (cache.has(file)) return cache.get(file) ?? null;
4027
4122
  let entry = null;
4028
4123
  try {
4029
- const text = readFileSync8(file, "utf8");
4124
+ const text = readFileSync9(file, "utf8");
4030
4125
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
4031
4126
  const bindings = buildModuleBindings(sf, ts);
4032
4127
  entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
@@ -4125,8 +4220,8 @@ function collectCallees(sf, ts) {
4125
4220
  }
4126
4221
 
4127
4222
  // src/map/coordinates.ts
4128
- function runtimeCoordinate(source, path11) {
4129
- if (/\[\d*\]/.test(path11)) {
4223
+ function runtimeCoordinate(source, path12) {
4224
+ if (/\[\d*\]/.test(path12)) {
4130
4225
  return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
4131
4226
  }
4132
4227
  switch (source) {
@@ -4135,15 +4230,15 @@ function runtimeCoordinate(source, path11) {
4135
4230
  case "multipart":
4136
4231
  case "body":
4137
4232
  case "server-fn-data":
4138
- return { runtimeParameter: `post.${path11}` };
4233
+ return { runtimeParameter: `post.${path12}` };
4139
4234
  case "query":
4140
- return { runtimeParameter: `get.${path11}` };
4235
+ return { runtimeParameter: `get.${path12}` };
4141
4236
  case "cookie":
4142
- return { runtimeParameter: `cookie.${path11}` };
4237
+ return { runtimeParameter: `cookie.${path12}` };
4143
4238
  case "file":
4144
- return { runtimeParameter: `files.${path11}` };
4239
+ return { runtimeParameter: `files.${path12}` };
4145
4240
  case "header":
4146
- return { runtimeParameter: `server.HTTP_${path11.toUpperCase().replace(/-/g, "_")}` };
4241
+ return { runtimeParameter: `server.HTTP_${path12.toUpperCase().replace(/-/g, "_")}` };
4147
4242
  case "route-param":
4148
4243
  return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
4149
4244
  default:
@@ -4175,8 +4270,8 @@ function addressSpaceOf(source) {
4175
4270
  function isProvenFlow(confidence) {
4176
4271
  return confidence === "exact-local" || confidence === "transformed-local";
4177
4272
  }
4178
- function inputIdOf(source, path11) {
4179
- return `${addressSpaceOf(source)}:${path11}`;
4273
+ function inputIdOf(source, path12) {
4274
+ return `${addressSpaceOf(source)}:${path12}`;
4180
4275
  }
4181
4276
  function withCoordinates(fields, source) {
4182
4277
  return fields.map((f) => {
@@ -4375,8 +4470,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts) {
4375
4470
  if (!bodyNode || sinks.length === 0 || inputs.length === 0) return { flows: [], limitations: [] };
4376
4471
  const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
4377
4472
  const rootPath = /* @__PURE__ */ new Map();
4378
- const addRoot = (name, path11, space) => {
4379
- if (!rootPath.has(name)) rootPath.set(name, { path: path11, space });
4473
+ const addRoot = (name, path12, space) => {
4474
+ if (!rootPath.has(name)) rootPath.set(name, { path: path12, space });
4380
4475
  };
4381
4476
  for (const p of params ?? []) {
4382
4477
  if (!p?.name) continue;
@@ -4521,8 +4616,8 @@ function dedupeLimitations(list) {
4521
4616
  function join22(base, seg) {
4522
4617
  return base ? `${base}.${seg}` : seg;
4523
4618
  }
4524
- function normalizePath(path11) {
4525
- return path11.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
4619
+ function normalizePath(path12) {
4620
+ return path12.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
4526
4621
  }
4527
4622
  function fluentChainCalls(call, ts) {
4528
4623
  let root = call;
@@ -4768,7 +4863,7 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
4768
4863
  }
4769
4864
 
4770
4865
  // src/map/imports.ts
4771
- import { readFileSync as readFileSync9 } from "fs";
4866
+ import { readFileSync as readFileSync10 } from "fs";
4772
4867
  import { join as join15 } from "path";
4773
4868
  var MAX_SITES = 5;
4774
4869
  var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
@@ -4780,7 +4875,7 @@ function readPathAliases(cwd) {
4780
4875
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
4781
4876
  let paths;
4782
4877
  try {
4783
- const raw = readFileSync9(join15(cwd, name), "utf8");
4878
+ const raw = readFileSync10(join15(cwd, name), "utf8");
4784
4879
  const parsed = JSON.parse(stripJsonComments(raw));
4785
4880
  paths = parsed.compilerOptions?.paths;
4786
4881
  } catch {
@@ -5159,7 +5254,7 @@ async function extractInputMap(cwd, ts, options = {}) {
5159
5254
  const hopTargets = /* @__PURE__ */ new Set();
5160
5255
  for (const file of files) {
5161
5256
  try {
5162
- const text = readFileSync10(file, "utf8");
5257
+ const text = readFileSync11(file, "utf8");
5163
5258
  const relFile = relative4(cwd, file);
5164
5259
  sourceBytes += text.length;
5165
5260
  if (!hasEntrySignal(text)) {
@@ -5172,7 +5267,7 @@ async function extractInputMap(cwd, ts, options = {}) {
5172
5267
  }
5173
5268
  parsed++;
5174
5269
  entryFiles.add(file);
5175
- const fingerprint = createHash3("sha256").update(text).digest("hex").slice(0, 16);
5270
+ const fingerprint = createHash4("sha256").update(text).digest("hex").slice(0, 16);
5176
5271
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
5177
5272
  const bindings = buildModuleBindings(sf, ts);
5178
5273
  for (const specifier of bindings.imports) {
@@ -5214,7 +5309,7 @@ async function extractInputMap(cwd, ts, options = {}) {
5214
5309
  for (const target of hopTargets) {
5215
5310
  if (entryFiles.has(target)) continue;
5216
5311
  try {
5217
- const text = readFileSync10(target, "utf8");
5312
+ const text = readFileSync11(target, "utf8");
5218
5313
  const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
5219
5314
  const bindings = buildModuleBindings(sf, ts);
5220
5315
  const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative4(cwd, target), graph });
@@ -5345,8 +5440,8 @@ async function buildInputMap(cwd, options = {}) {
5345
5440
  }
5346
5441
 
5347
5442
  // src/setup.ts
5348
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
5349
- import path10 from "path";
5443
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
5444
+ import path11 from "path";
5350
5445
  var SCAN_COMMAND = "patchstack-connect scan";
5351
5446
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
5352
5447
  function setupProtection(cwd) {
@@ -5364,8 +5459,8 @@ function appendHook(existing, command) {
5364
5459
  return `${existing} && ${command}`;
5365
5460
  }
5366
5461
  function wireBuildScripts(cwd, packageManager) {
5367
- const target = path10.join(cwd, "package.json");
5368
- const raw = readFileSync11(target, "utf8");
5462
+ const target = path11.join(cwd, "package.json");
5463
+ const raw = readFileSync12(target, "utf8");
5369
5464
  const pkg = JSON.parse(raw);
5370
5465
  const scripts = pkg.scripts ?? {};
5371
5466
  const build = scripts.build;
@@ -5414,7 +5509,7 @@ function wireBuildScripts(cwd, packageManager) {
5414
5509
  const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
5415
5510
  const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
5416
5511
  const trailingNewline = raw.endsWith("\n") ? "\n" : "";
5417
- writeFileSync6(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
5512
+ writeFileSync7(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
5418
5513
  if (build === void 0 || build.trim().length === 0) {
5419
5514
  return {
5420
5515
  changed: true,
@@ -5492,10 +5587,17 @@ Usage:
5492
5587
  what's missing, with tailored commands), then
5493
5588
  print the full setup guide. --full prints the
5494
5589
  guide even when setup is complete
5495
- patchstack-connect login [options] Recover this site's credential when
5590
+ patchstack-connect login [--wait] Recover this site's credential when
5496
5591
  .patchstackrc.json has been lost. Prints a link
5497
- for the site's OWNER to approve in the dashboard,
5498
- then waits (10 min). Use this instead of deleting
5592
+ for the site's OWNER to approve in the dashboard.
5593
+ In a terminal it then waits. When the output is
5594
+ piped or captured \u2014 an assistant running it \u2014 it
5595
+ prints the link and EXITS, so the link is visible
5596
+ immediately. Run it AGAIN once the user confirms
5597
+ they approved: it resumes the same request rather
5598
+ than starting a new one, and finishes the flow.
5599
+ --wait blocks instead of returning. Use this
5600
+ instead of deleting
5499
5601
  .patchstackrc.json and re-scanning, which would
5500
5602
  provision a second site. Approving ROTATES the
5501
5603
  credential: CI, deploys and other machines using
@@ -5600,7 +5702,26 @@ async function runLogin(args) {
5600
5702
  cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
5601
5703
  cliEndpoint: getStringFlag(args.flags, "endpoint")
5602
5704
  });
5603
- const result = await login(config, (userCode, verificationUri) => {
5705
+ const approved = () => {
5706
+ console.log("\n \u2713 Credential restored and saved to .patchstackrc.json.");
5707
+ console.log(" The previous credential no longer works. Update it anywhere else it was set:");
5708
+ console.log(" CI secrets, hosting env vars, preview environments, other checkouts.\n");
5709
+ return 0;
5710
+ };
5711
+ if (args.flags.has("wait")) {
5712
+ const pending = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
5713
+ if (pending === null) {
5714
+ console.error("\n No login is waiting for approval. Run `patchstack-connect login` first.\n");
5715
+ return 1;
5716
+ }
5717
+ const resumed = await waitForApproval(config, pending);
5718
+ if (resumed.status === "approved") return approved();
5719
+ console.error(`
5720
+ ${resumed.message ?? "Login failed."}
5721
+ `);
5722
+ return 1;
5723
+ }
5724
+ const prompt = (userCode, verificationUri) => {
5604
5725
  console.log(`
5605
5726
  Your code: ${userCode}`);
5606
5727
  console.log(` Approve at: ${verificationUri}
@@ -5608,14 +5729,39 @@ async function runLogin(args) {
5608
5729
  console.log(" Open that link and approve it as the site's owner. Approving issues a new");
5609
5730
  console.log(" credential and stops the current one working \u2014 CI, deploys and any other");
5610
5731
  console.log(" machine using it will need the new value.\n");
5611
- console.log(" Waiting for approval (the code expires in 10 minutes)\u2026");
5612
- });
5613
- if (result.status === "approved") {
5614
- console.log("\n \u2713 Credential restored and saved to .patchstackrc.json.");
5615
- console.log(" The previous credential no longer works. Update it anywhere else it was set:");
5616
- console.log(" CI secrets, hosting env vars, preview environments, other checkouts.\n");
5732
+ };
5733
+ if (process.stdout.isTTY !== true) {
5734
+ const existing = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
5735
+ if (existing !== null && Date.now() < existing.expiresAt) {
5736
+ const outcome = await redeemIfApproved(config, existing);
5737
+ if (outcome === "approved") return approved();
5738
+ if (outcome === "pending") {
5739
+ const secondsLeft = Math.round((existing.expiresAt - Date.now()) / 1e3);
5740
+ console.log(`
5741
+ Still waiting for approval of code ${existing.userCode}.`);
5742
+ console.log(` Approve at: ${existing.verificationUri}`);
5743
+ console.log(` (valid for another ${secondsLeft}s \u2014 run this again once the user confirms)
5744
+ `);
5745
+ return 0;
5746
+ }
5747
+ }
5748
+ const started = await startLogin(config);
5749
+ if (started.status !== "started" || started.pending === void 0) {
5750
+ console.error(`
5751
+ ${started.message ?? "Login failed."}
5752
+ `);
5753
+ return 1;
5754
+ }
5755
+ prompt(started.pending.userCode, started.pending.verificationUri);
5756
+ console.log(" Give that link to the user. When they confirm they have approved it, run");
5757
+ console.log(" this same command again (or `login --wait` to block until they do).\n");
5617
5758
  return 0;
5618
5759
  }
5760
+ const result = await login(config, (userCode, verificationUri) => {
5761
+ prompt(userCode, verificationUri);
5762
+ console.log(" Waiting for approval (the code expires in 10 minutes)\u2026");
5763
+ });
5764
+ if (result.status === "approved") return approved();
5619
5765
  console.error(`
5620
5766
  ${result.message ?? "Login failed."}
5621
5767
  `);
@@ -5664,7 +5810,7 @@ async function runMap(args) {
5664
5810
  const json = JSON.stringify(map, null, 2);
5665
5811
  const out = getStringFlag(args.flags, "out");
5666
5812
  if (out) {
5667
- writeFileSync7(out, json);
5813
+ writeFileSync8(out, json);
5668
5814
  console.error(`patchstack: wrote ${out}`);
5669
5815
  } else if (args.flags.get("upload") !== true) {
5670
5816
  console.log(json);
@@ -5950,7 +6096,7 @@ async function runGuide(args) {
5950
6096
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
5951
6097
  console.log("");
5952
6098
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
5953
- console.log(readFileSync12(guidePath, "utf8"));
6099
+ console.log(readFileSync13(guidePath, "utf8"));
5954
6100
  return 0;
5955
6101
  }
5956
6102
  async function runSetup(args) {
@@ -6131,7 +6277,7 @@ async function runMarkBuild(args) {
6131
6277
  let marked = 0;
6132
6278
  let widgetTouched = 0;
6133
6279
  for (const file of files) {
6134
- const before = readFileSync12(file, "utf8");
6280
+ const before = readFileSync13(file, "utf8");
6135
6281
  let after = injectMarker(before, snippet);
6136
6282
  if (widgetUuid !== null) {
6137
6283
  const ensured = ensureWidgetInHtml(after, widgetUuid);
@@ -6141,7 +6287,7 @@ async function runMarkBuild(args) {
6141
6287
  after = ensured.html;
6142
6288
  }
6143
6289
  if (after !== before) {
6144
- writeFileSync7(file, after);
6290
+ writeFileSync8(file, after);
6145
6291
  marked += 1;
6146
6292
  }
6147
6293
  }