@patchstack/connect 0.3.29 → 0.3.30

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.
Files changed (40) hide show
  1. package/AGENT-INSTALL.md +37 -21
  2. package/README.md +24 -10
  3. package/dist/{chunk-MJOTFUDE.js → chunk-LLKP5EJS.js} +1 -1
  4. package/dist/chunk-LLKP5EJS.js.map +1 -0
  5. package/dist/cli.js +1062 -189
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.cjs +419 -104
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +28 -9
  10. package/dist/index.d.ts +28 -9
  11. package/dist/index.js +412 -97
  12. package/dist/index.js.map +1 -1
  13. package/dist/protect/templates/astro-middleware.ts +33 -13
  14. package/dist/protect/templates/demo-rules.json +2 -3
  15. package/dist/protect/templates/express-guard.cjs +26 -15
  16. package/dist/protect/templates/express-guard.js +26 -15
  17. package/dist/protect/templates/express-guard.ts +33 -16
  18. package/dist/protect/templates/fastify-plugin.cjs +33 -11
  19. package/dist/protect/templates/fastify-plugin.js +33 -11
  20. package/dist/protect/templates/fastify-plugin.ts +40 -12
  21. package/dist/protect/templates/generic-guard.cjs +28 -12
  22. package/dist/protect/templates/generic-guard.js +28 -13
  23. package/dist/protect/templates/generic-guard.ts +41 -20
  24. package/dist/protect/templates/guard.ts +47 -27
  25. package/dist/protect/templates/next-middleware.ts +29 -12
  26. package/dist/protect/templates/nuxt-middleware.ts +29 -12
  27. package/dist/protect/templates/rules.json +2 -2
  28. package/dist/protect/templates/sveltekit-hooks.ts +33 -13
  29. package/dist/protect.cjs +1039 -235
  30. package/dist/protect.cjs.map +1 -1
  31. package/dist/protect.d.ts +39 -11
  32. package/dist/protect.edge.js +663 -132
  33. package/dist/protect.edge.js.map +4 -4
  34. package/dist/protect.js +665 -134
  35. package/dist/protect.js.map +1 -1
  36. package/dist/{refresh-manifest-VRBE6RH6.js → refresh-manifest-ZSP76JWQ.js} +351 -94
  37. package/dist/refresh-manifest-ZSP76JWQ.js.map +1 -0
  38. package/package.json +5 -2
  39. package/dist/chunk-MJOTFUDE.js.map +0 -1
  40. package/dist/refresh-manifest-VRBE6RH6.js.map +0 -1
package/dist/cli.js CHANGED
@@ -39,7 +39,7 @@ async function readDeclaredDependencyNames(cwd) {
39
39
  return [];
40
40
  }
41
41
  const names = /* @__PURE__ */ new Set();
42
- for (const group of ["dependencies", "devDependencies"]) {
42
+ for (const group of ["dependencies", "devDependencies", "optionalDependencies"]) {
43
43
  const deps = parsed[group];
44
44
  if (typeof deps !== "object" || deps === null) {
45
45
  continue;
@@ -57,14 +57,171 @@ function missingDependencies(declared, packages) {
57
57
  const present = new Set(packages.map((entry) => entry.name));
58
58
  return declared.filter((name) => !present.has(name));
59
59
  }
60
+ function disagreements(packages, other) {
61
+ const otherVersions = /* @__PURE__ */ new Map();
62
+ for (const entry of other) {
63
+ if (!otherVersions.has(entry.name)) otherVersions.set(entry.name, entry.version);
64
+ }
65
+ const seen = /* @__PURE__ */ new Set();
66
+ const conflicts = [];
67
+ for (const entry of packages) {
68
+ if (seen.has(entry.name)) continue;
69
+ seen.add(entry.name);
70
+ const otherVersion = otherVersions.get(entry.name);
71
+ if (otherVersion !== void 0 && otherVersion !== entry.version) {
72
+ conflicts.push({ name: entry.name, version: entry.version, otherVersion });
73
+ }
74
+ }
75
+ return conflicts;
76
+ }
60
77
 
61
- // src/parsers/npm.ts
78
+ // src/parsers/bun.ts
62
79
  import { readFile as readFile2 } from "fs/promises";
80
+
81
+ // src/parsers/report.ts
82
+ var MAX_SAMPLES = 3;
83
+ function newReport() {
84
+ return { unreadable: 0, samples: [] };
85
+ }
86
+ function recordUnreadable(report, sample) {
87
+ if (!report) return;
88
+ report.unreadable++;
89
+ if (report.samples.length < MAX_SAMPLES) report.samples.push(sample);
90
+ }
91
+ function unreadableWarning(filename, report) {
92
+ if (report.unreadable === 0) return null;
93
+ const shown = report.samples.join(", ");
94
+ const one = report.unreadable === 1;
95
+ return `${filename}: ${report.unreadable} entr${one ? "y was" : "ies were"} not in a form this scanner reads (${shown}${report.unreadable > report.samples.length ? ", \u2026" : ""}), so ${one ? "it is" : "they are"} absent from this manifest. Anything installed only through ${one ? "that entry" : "those entries"} is not being checked.`;
96
+ }
97
+
98
+ // src/parsers/bun.ts
99
+ async function parseBunLockfile(lockfilePath, report) {
100
+ let raw;
101
+ try {
102
+ raw = await readFile2(lockfilePath, "utf8");
103
+ } catch (cause) {
104
+ throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
105
+ }
106
+ let parsed;
107
+ try {
108
+ parsed = JSON.parse(stripTrailingCommas(raw));
109
+ } catch (cause) {
110
+ throw new PatchstackError(`Lockfile at ${lockfilePath} is not valid JSON`, "LOCKFILE_PARSE_ERROR", cause);
111
+ }
112
+ const packages = parsed?.packages;
113
+ if (typeof packages !== "object" || packages === null) {
114
+ throw new PatchstackError(`Lockfile at ${lockfilePath} has no "packages" entries`, "LOCKFILE_PARSE_ERROR");
115
+ }
116
+ const direct = directDependencyNames(parsed);
117
+ const entries = [];
118
+ let skipped = 0;
119
+ for (const [key, value] of Object.entries(packages)) {
120
+ const descriptor = Array.isArray(value) ? value[0] : value;
121
+ if (typeof descriptor !== "string") {
122
+ skipped++;
123
+ recordUnreadable(report, key);
124
+ continue;
125
+ }
126
+ const split = splitDescriptor(descriptor);
127
+ if (split === null) {
128
+ skipped++;
129
+ if (!isDeliberatelyExcluded(descriptor)) recordUnreadable(report, key);
130
+ continue;
131
+ }
132
+ entries.push({
133
+ name: split.name,
134
+ version: split.version,
135
+ ...direct.has(split.name) ? { direct: true } : {},
136
+ ...key === split.name ? {} : { path: key }
137
+ });
138
+ }
139
+ if (entries.length === 0) {
140
+ throw new PatchstackError(
141
+ `Lockfile at ${lockfilePath} lists no registry packages${skipped > 0 ? ` (${skipped} non-registry entr${skipped === 1 ? "y" : "ies"} skipped)` : ""}`,
142
+ "LOCKFILE_PARSE_ERROR"
143
+ );
144
+ }
145
+ return entries;
146
+ }
147
+ function splitDescriptor(descriptor) {
148
+ const at = descriptor.lastIndexOf("@");
149
+ if (at <= 0) return null;
150
+ const name = descriptor.slice(0, at);
151
+ const version = descriptor.slice(at + 1);
152
+ if (name === "" || version === "") return null;
153
+ if (NON_REGISTRY_VERSION.test(version)) return null;
154
+ return { name, version };
155
+ }
156
+ var NON_REGISTRY_VERSION = /^[a-z][a-z0-9+.-]*:|\//i;
157
+ function isDeliberatelyExcluded(descriptor) {
158
+ const at = descriptor.lastIndexOf("@");
159
+ if (at <= 0) return false;
160
+ return NON_REGISTRY_VERSION.test(descriptor.slice(at + 1));
161
+ }
162
+ function directDependencyNames(parsed) {
163
+ const names = /* @__PURE__ */ new Set();
164
+ const workspaces = parsed?.workspaces;
165
+ if (typeof workspaces !== "object" || workspaces === null) return names;
166
+ for (const workspace of Object.values(workspaces)) {
167
+ if (typeof workspace !== "object" || workspace === null) continue;
168
+ for (const group of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
169
+ const deps = workspace[group];
170
+ if (typeof deps !== "object" || deps === null) continue;
171
+ for (const name of Object.keys(deps)) names.add(name);
172
+ }
173
+ }
174
+ return names;
175
+ }
176
+ function stripTrailingCommas(source) {
177
+ const out = [];
178
+ let inString = false;
179
+ let pendingComma = -1;
180
+ for (let i = 0; i < source.length; i++) {
181
+ const ch = source[i];
182
+ if (inString) {
183
+ out.push(ch);
184
+ if (ch === "\\") {
185
+ const next = source[i + 1];
186
+ if (next !== void 0) {
187
+ out.push(next);
188
+ i++;
189
+ }
190
+ } else if (ch === '"') {
191
+ inString = false;
192
+ }
193
+ continue;
194
+ }
195
+ if (ch === '"') {
196
+ inString = true;
197
+ pendingComma = -1;
198
+ out.push(ch);
199
+ continue;
200
+ }
201
+ if (ch === ",") {
202
+ pendingComma = out.length;
203
+ out.push(ch);
204
+ continue;
205
+ }
206
+ if (ch === "}" || ch === "]") {
207
+ if (pendingComma !== -1) out[pendingComma] = " ";
208
+ pendingComma = -1;
209
+ out.push(ch);
210
+ continue;
211
+ }
212
+ if (!/\s/.test(ch)) pendingComma = -1;
213
+ out.push(ch);
214
+ }
215
+ return out.join("");
216
+ }
217
+
218
+ // src/parsers/npm.ts
219
+ import { readFile as readFile3 } from "fs/promises";
63
220
  import path2 from "path";
64
221
  async function parseNpmLockfile(lockfilePath) {
65
222
  let raw;
66
223
  try {
67
- raw = await readFile2(lockfilePath, "utf8");
224
+ raw = await readFile3(lockfilePath, "utf8");
68
225
  } catch (cause) {
69
226
  throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
70
227
  }
@@ -145,8 +302,9 @@ function isDirectV2(pkgPath) {
145
302
  }
146
303
 
147
304
  // src/parsers/node_modules.ts
148
- import { lstat, readFile as readFile3, readdir, stat } from "fs/promises";
305
+ import { lstat, readFile as readFile4, readdir, realpath, stat } from "fs/promises";
149
306
  import path3 from "path";
307
+ var PACKAGE_STORE_DIRS = /* @__PURE__ */ new Set([".bun", ".pnpm", ".store"]);
150
308
  async function walkNodeModules(cwd) {
151
309
  const root = path3.join(cwd, "node_modules");
152
310
  try {
@@ -167,96 +325,122 @@ async function walkNodeModules(cwd) {
167
325
  cause
168
326
  );
169
327
  }
170
- const entries = [];
171
- await walk(root, entries, 0);
172
- return entries;
328
+ const seen = /* @__PURE__ */ new Map();
329
+ try {
330
+ seen.set(await realpath(cwd), -1);
331
+ } catch {
332
+ }
333
+ return dedupeByShallowest(await walk(root, 0, seen));
173
334
  }
174
- async function walk(dir, acc, depth) {
335
+ function dedupeByShallowest(found) {
336
+ const best = /* @__PURE__ */ new Map();
337
+ for (const item of found) {
338
+ const current = best.get(item.realpath);
339
+ if (current === void 0 || item.depth < current.depth) best.set(item.realpath, item);
340
+ }
341
+ return [...best.values()].map(({ entry }) => entry);
342
+ }
343
+ var CONCURRENCY = 32;
344
+ async function walk(dir, depth, seen) {
175
345
  let names;
176
346
  try {
177
347
  names = await readdir(dir);
178
348
  } catch {
179
- return;
349
+ return [];
180
350
  }
181
- for (const name of names) {
182
- if (name.startsWith(".")) {
183
- continue;
184
- }
351
+ const perEntry = await mapLimit(names, async (name) => {
352
+ if (name.startsWith(".") && !PACKAGE_STORE_DIRS.has(name)) return [];
185
353
  const fullPath = path3.join(dir, name);
186
- if (!await isPlainDirectory(fullPath)) {
187
- continue;
188
- }
354
+ const resolved = await claim(fullPath, seen, depth);
355
+ if (resolved === null) return [];
356
+ if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen);
189
357
  if (name.startsWith("@")) {
190
358
  let subNames;
191
359
  try {
192
360
  subNames = await readdir(fullPath);
193
361
  } catch {
194
- continue;
362
+ return [];
195
363
  }
196
- for (const sub of subNames) {
197
- if (sub.startsWith(".")) {
198
- continue;
199
- }
364
+ const scoped = await mapLimit(subNames, async (sub) => {
365
+ if (sub.startsWith(".")) return [];
200
366
  const scopedDir = path3.join(fullPath, sub);
201
- if (!await isPlainDirectory(scopedDir)) {
202
- continue;
203
- }
204
- await readPackage(scopedDir, depth, acc);
205
- await walkNested(scopedDir, acc, depth);
206
- }
207
- continue;
367
+ const scopedReal = await claim(scopedDir, seen, depth);
368
+ if (scopedReal === null) return [];
369
+ return packageAndNested(scopedDir, scopedReal, depth, seen);
370
+ });
371
+ return scoped.flat();
208
372
  }
209
- await readPackage(fullPath, depth, acc);
210
- await walkNested(fullPath, acc, depth);
211
- }
373
+ return packageAndNested(fullPath, resolved, depth, seen);
374
+ });
375
+ return perEntry.flat();
376
+ }
377
+ async function packageAndNested(pkgDir, resolved, depth, seen) {
378
+ const [own, nested] = await Promise.all([readPackage(pkgDir, depth), walkNested(pkgDir, depth, seen)]);
379
+ return own === null ? nested : [{ entry: own, realpath: resolved, depth }, ...nested];
212
380
  }
213
- async function readPackage(pkgDir, depth, acc) {
381
+ async function mapLimit(items, fn) {
382
+ const results = new Array(items.length);
383
+ let next = 0;
384
+ const workers = Array.from({ length: Math.min(CONCURRENCY, items.length) }, async () => {
385
+ while (next < items.length) {
386
+ const index = next++;
387
+ results[index] = await fn(items[index]);
388
+ }
389
+ });
390
+ await Promise.all(workers);
391
+ return results;
392
+ }
393
+ async function readPackage(pkgDir, depth) {
214
394
  let raw;
215
395
  try {
216
- raw = await readFile3(path3.join(pkgDir, "package.json"), "utf8");
396
+ raw = await readFile4(path3.join(pkgDir, "package.json"), "utf8");
217
397
  } catch {
218
- return;
398
+ return null;
219
399
  }
220
400
  let parsed;
221
401
  try {
222
402
  parsed = JSON.parse(raw);
223
403
  } catch {
224
- return;
404
+ return null;
225
405
  }
226
406
  if (typeof parsed.name !== "string" || parsed.name.length === 0) {
227
- return;
407
+ return null;
228
408
  }
229
409
  if (typeof parsed.version !== "string" || parsed.version.length === 0) {
230
- return;
410
+ return null;
231
411
  }
232
- acc.push({
233
- name: parsed.name,
234
- version: parsed.version,
235
- direct: depth === 0
236
- });
412
+ return { name: parsed.name, version: parsed.version, direct: depth === 0 };
237
413
  }
238
- async function walkNested(pkgDir, acc, depth) {
414
+ async function walkNested(pkgDir, depth, seen) {
239
415
  const nested = path3.join(pkgDir, "node_modules");
240
- if (!await isPlainDirectory(nested)) {
241
- return;
416
+ if (await claim(nested, seen, depth + 1) === null) {
417
+ return [];
242
418
  }
243
- await walk(nested, acc, depth + 1);
419
+ return walk(nested, depth + 1, seen);
244
420
  }
245
- async function isPlainDirectory(dir) {
421
+ async function claim(dir, seen, depth) {
422
+ let resolved;
246
423
  try {
247
424
  const info = await lstat(dir);
248
- return info.isDirectory() && !info.isSymbolicLink();
425
+ if (!info.isDirectory() && !info.isSymbolicLink()) return null;
426
+ resolved = await realpath(dir);
427
+ const target = await stat(resolved);
428
+ if (!target.isDirectory()) return null;
249
429
  } catch {
250
- return false;
430
+ return null;
251
431
  }
432
+ const claimed = seen.get(resolved);
433
+ if (claimed !== void 0 && claimed <= depth) return null;
434
+ seen.set(resolved, depth);
435
+ return resolved;
252
436
  }
253
437
 
254
438
  // src/parsers/pnpm.ts
255
- import { readFile as readFile4 } from "fs/promises";
256
- async function parsePnpmLockfile(lockfilePath) {
439
+ import { readFile as readFile5 } from "fs/promises";
440
+ async function parsePnpmLockfile(lockfilePath, report) {
257
441
  let raw;
258
442
  try {
259
- raw = await readFile4(lockfilePath, "utf8");
443
+ raw = await readFile5(lockfilePath, "utf8");
260
444
  } catch (cause) {
261
445
  throw new PatchstackError(
262
446
  `Could not read lockfile at ${lockfilePath}`,
@@ -277,6 +461,7 @@ async function parsePnpmLockfile(lockfilePath) {
277
461
  for (const key of packageKeys) {
278
462
  const parsed = parsePackageKey(key);
279
463
  if (parsed === null) {
464
+ recordUnreadable(report, key);
280
465
  continue;
281
466
  }
282
467
  entries.push({
@@ -504,12 +689,12 @@ function extractLeafName(trimmed) {
504
689
  }
505
690
 
506
691
  // src/parsers/yarn.ts
507
- import { readFile as readFile5 } from "fs/promises";
692
+ import { readFile as readFile6 } from "fs/promises";
508
693
  import path4 from "path";
509
- async function parseYarnLockfile(lockfilePath) {
694
+ async function parseYarnLockfile(lockfilePath, report) {
510
695
  let raw;
511
696
  try {
512
- raw = await readFile5(lockfilePath, "utf8");
697
+ raw = await readFile6(lockfilePath, "utf8");
513
698
  } catch (cause) {
514
699
  throw new PatchstackError(
515
700
  `Could not read lockfile at ${lockfilePath}`,
@@ -517,7 +702,7 @@ async function parseYarnLockfile(lockfilePath) {
517
702
  cause
518
703
  );
519
704
  }
520
- const blocks = parseBlocks(raw);
705
+ const blocks = parseBlocks(raw, report);
521
706
  if (blocks.length === 0) {
522
707
  throw new PatchstackError(
523
708
  `Lockfile at ${lockfilePath} contains no package entries`,
@@ -546,13 +731,19 @@ async function parseYarnLockfile(lockfilePath) {
546
731
  }
547
732
  return entries;
548
733
  }
549
- function parseBlocks(raw) {
734
+ var NON_PACKAGE_KEYS = /* @__PURE__ */ new Set(["__metadata"]);
735
+ function parseBlocks(raw, report) {
550
736
  const lines = raw.split(/\r?\n/);
551
737
  const blocks = [];
552
738
  let current = null;
553
739
  const finalize = () => {
554
- if (current !== null && current.version.length > 0 && current.names.size > 0) {
740
+ if (current === null) {
741
+ return;
742
+ }
743
+ if (current.version.length > 0 && current.names.size > 0) {
555
744
  blocks.push(current);
745
+ } else if (current.unreadable) {
746
+ recordUnreadable(report, current.key);
556
747
  }
557
748
  current = null;
558
749
  };
@@ -569,13 +760,13 @@ function parseBlocks(raw) {
569
760
  }
570
761
  const keyLine = trimmed.slice(0, -1);
571
762
  const names = /* @__PURE__ */ new Set();
763
+ let unreadable = false;
572
764
  for (const spec of splitDescriptors(keyLine)) {
573
- const name = extractName(spec);
574
- if (name !== null) {
575
- names.add(name);
576
- }
765
+ const classified = classifyDescriptor(spec);
766
+ if (classified.name !== void 0) names.add(classified.name);
767
+ else if (!classified.excluded) unreadable = true;
577
768
  }
578
- current = { names, version: "" };
769
+ current = { names, version: "", key: keyLine, unreadable: unreadable && names.size === 0 };
579
770
  continue;
580
771
  }
581
772
  if (current === null) {
@@ -630,20 +821,37 @@ function splitDescriptors(keyLine) {
630
821
  }
631
822
  return parts;
632
823
  }
633
- function extractName(rawSpec) {
824
+ var NON_REGISTRY_PROTOCOLS = /^(?:workspace|patch|virtual|portal|file|link|exec|git|github|https?|ssh):/i;
825
+ function classifyDescriptor(rawSpec) {
634
826
  let s = rawSpec.trim();
635
827
  if (s.length === 0) {
636
- return null;
828
+ return { excluded: true };
637
829
  }
638
830
  if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
639
831
  s = s.slice(1, -1);
640
832
  }
641
- const atIdx = s.lastIndexOf("@");
833
+ if (NON_PACKAGE_KEYS.has(s)) {
834
+ return { excluded: true };
835
+ }
836
+ const atIdx = s.indexOf("@", 1);
642
837
  if (atIdx <= 0) {
643
- return null;
838
+ return {};
644
839
  }
645
840
  const name = s.slice(0, atIdx);
646
- return name.length > 0 ? name : null;
841
+ const range = s.slice(atIdx + 1);
842
+ if (name.length === 0) {
843
+ return {};
844
+ }
845
+ if (NON_REGISTRY_PROTOCOLS.test(range)) {
846
+ return { excluded: true };
847
+ }
848
+ const alias = /^npm:(.+)$/i.exec(range);
849
+ if (alias !== null) {
850
+ const target = alias[1] ?? "";
851
+ const targetAt = target.indexOf("@", 1);
852
+ return { name: targetAt > 0 ? target.slice(0, targetAt) : name };
853
+ }
854
+ return { name };
647
855
  }
648
856
  function parseVersionField(content) {
649
857
  if (!content.startsWith("version")) {
@@ -668,7 +876,7 @@ async function readDirectDepNames(cwd) {
668
876
  const names = /* @__PURE__ */ new Set();
669
877
  let raw;
670
878
  try {
671
- raw = await readFile5(path4.join(cwd, "package.json"), "utf8");
879
+ raw = await readFile6(path4.join(cwd, "package.json"), "utf8");
672
880
  } catch {
673
881
  return names;
674
882
  }
@@ -697,7 +905,10 @@ async function readDirectDepNames(cwd) {
697
905
  // src/parsers/index.ts
698
906
  var LOCKFILE_CANDIDATES = [
699
907
  { filename: "package-lock.json", strategy: "npm-lockfile" },
700
- { filename: "bun.lock", strategy: "node-modules-walk" },
908
+ // Read directly, not walked. Bun's isolated layout puts packages under a dot-directory and makes the
909
+ // top level symlinks into it, so a `node_modules` walk finds nothing there — and isolated is the default
910
+ // for new workspaces.
911
+ { filename: "bun.lock", strategy: "bun-lockfile" },
701
912
  { filename: "bun.lockb", strategy: "node-modules-walk" },
702
913
  { filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
703
914
  { filename: "yarn.lock", strategy: "yarn-lockfile" }
@@ -732,34 +943,58 @@ async function scanLockfile(cwd) {
732
943
  const declared = await readDeclaredDependencyNames(cwd);
733
944
  const warnings = [];
734
945
  let firstParsed = null;
735
- let walkTried = false;
946
+ let walked = null;
947
+ let walkWasCandidate = false;
948
+ const parsedSources = [];
736
949
  for (const candidate of candidates) {
737
950
  let packages;
951
+ const report = newReport();
738
952
  try {
739
- packages = await runStrategy(candidate, cwd);
953
+ packages = await runStrategy(candidate, cwd, report);
740
954
  } catch {
741
955
  continue;
742
956
  }
743
- walkTried ||= candidate.strategy === "node-modules-walk";
957
+ const unreadable = unreadableWarning(candidate.filename, report);
958
+ if (unreadable !== null) warnings.push(unreadable);
959
+ if (candidate.strategy === "node-modules-walk") {
960
+ walked = packages;
961
+ walkWasCandidate = true;
962
+ }
744
963
  firstParsed ??= { packages, filename: candidate.filename };
745
- const missing = missingDependencies(declared, packages);
746
- if (missing.length === 0) {
747
- return manifestWith(packages, warnings);
964
+ parsedSources.push({ packages, filename: candidate.filename });
965
+ }
966
+ for (const source of parsedSources) {
967
+ const missing = missingDependencies(declared, source.packages);
968
+ if (missing.length > 0) {
969
+ warnings.push(staleWarning(source.filename, missing));
970
+ continue;
971
+ }
972
+ const conflicts = otherCompleteSources(parsedSources, source, declared).flatMap(
973
+ (other) => disagreements(source.packages, other.packages).map((conflict) => ({ other: other.filename, conflict }))
974
+ );
975
+ if (conflicts.length === 0) {
976
+ return manifestWith(source.packages, warnings);
748
977
  }
749
- warnings.push(staleWarning(candidate.filename, missing));
978
+ warnings.push(conflictWarning(source.filename, conflicts));
979
+ break;
750
980
  }
751
- if (!walkTried) {
981
+ if (walked === null) {
752
982
  try {
753
- const packages = await walkNodeModules(cwd);
754
- const missing = missingDependencies(declared, packages);
755
- if (missing.length > 0) {
756
- warnings.push(staleWarning("node_modules/", missing));
757
- }
758
- warnings.push("Scanned node_modules/ instead. Delete the stale lockfile to silence this warning.");
759
- return manifestWith(packages, warnings);
983
+ walked = await walkNodeModules(cwd);
760
984
  } catch {
985
+ walked = null;
761
986
  }
762
987
  }
988
+ if (walked !== null) {
989
+ const missing = missingDependencies(declared, walked);
990
+ if (missing.length > 0) {
991
+ warnings.push(staleWarning("node_modules/", missing));
992
+ }
993
+ warnings.push(
994
+ walkWasCandidate ? "Reporting node_modules/, which is what the build loads." : "Scanned node_modules/ instead. Delete the stale lockfile to silence this warning."
995
+ );
996
+ return manifestWith(walked, warnings);
997
+ }
763
998
  if (firstParsed === null) {
764
999
  const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
765
1000
  throw new PatchstackError(
@@ -775,6 +1010,15 @@ async function scanLockfile(cwd) {
775
1010
  function manifestWith(packages, warnings) {
776
1011
  return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
777
1012
  }
1013
+ function otherCompleteSources(all, current, declared) {
1014
+ return all.filter(
1015
+ (source) => source.filename !== current.filename && missingDependencies(declared, source.packages).length === 0
1016
+ );
1017
+ }
1018
+ function conflictWarning(filename, conflicts) {
1019
+ const shown = conflicts.slice(0, 3).map(({ other, conflict }) => `${conflict.name} ${conflict.version} vs ${conflict.otherVersion} in ${other}`).join("; ");
1020
+ return `${filename} and another lockfile disagree about installed versions (${shown}${conflicts.length > 3 ? `, +${conflicts.length - 3} more` : ""}). Scanned node_modules/ instead. Remove whichever lockfile your package manager does not maintain.`;
1021
+ }
778
1022
  function staleWarning(source, missing) {
779
1023
  const sample = missing.slice(0, 3).join(", ");
780
1024
  const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
@@ -789,14 +1033,18 @@ async function presentLockfiles(cwd) {
789
1033
  );
790
1034
  return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
791
1035
  }
792
- async function runStrategy(detected, cwd) {
1036
+ async function runStrategy(detected, cwd, report) {
793
1037
  switch (detected.strategy) {
794
1038
  case "npm-lockfile":
795
1039
  return parseNpmLockfile(detected.filePath);
1040
+ case "bun-lockfile":
1041
+ return parseBunLockfile(detected.filePath, report);
1042
+ // The hand-written scanners. Each meets entries it was not written for, and each is asked to say how
1043
+ // many it dropped, because a manifest short a package reads exactly like a project without it.
796
1044
  case "pnpm-lockfile":
797
- return parsePnpmLockfile(detected.filePath);
1045
+ return parsePnpmLockfile(detected.filePath, report);
798
1046
  case "yarn-lockfile":
799
- return parseYarnLockfile(detected.filePath);
1047
+ return parseYarnLockfile(detected.filePath, report);
800
1048
  case "node-modules-walk":
801
1049
  return walkNodeModules(cwd);
802
1050
  }
@@ -1228,7 +1476,7 @@ function isTimeoutError(cause) {
1228
1476
  }
1229
1477
 
1230
1478
  // src/demo.ts
1231
- import { readFile as readFile6 } from "fs/promises";
1479
+ import { readFile as readFile7 } from "fs/promises";
1232
1480
  import { join } from "path";
1233
1481
  var NODE_SERIALIZE_DEMO = {
1234
1482
  name: "node-serialize",
@@ -1298,7 +1546,7 @@ Add this deliberately vulnerable package first: npm install --save-exact ${scena
1298
1546
  }
1299
1547
  async function readPersistedSiteUuid(cwd) {
1300
1548
  try {
1301
- const value = JSON.parse(await readFile6(join(cwd, ".patchstackrc.json"), "utf8")).siteUuid;
1549
+ const value = JSON.parse(await readFile7(join(cwd, ".patchstackrc.json"), "utf8")).siteUuid;
1302
1550
  return typeof value === "string" && value.length > 0 ? value : null;
1303
1551
  } catch {
1304
1552
  return null;
@@ -1383,7 +1631,7 @@ async function waitForDemoRule(manifestEndpoint, siteUuid, scenario, options = {
1383
1631
  const pollIntervalMs = options.pollIntervalMs ?? 1e3;
1384
1632
  const requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
1385
1633
  const fetchFn = options.fetchFn ?? fetch;
1386
- const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1634
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
1387
1635
  const now = options.now ?? Date.now;
1388
1636
  const started = now();
1389
1637
  const rulesUrl = buildRulesUrl(manifestEndpoint, siteUuid);
@@ -1432,12 +1680,14 @@ function renderDemoTestCommands(url, scenario) {
1432
1680
  }
1433
1681
 
1434
1682
  // src/config.ts
1435
- import { readFile as readFile7, writeFile } from "fs/promises";
1683
+ import { readFile as readFile8, writeFile, chmod } from "fs/promises";
1436
1684
  import path6 from "path";
1437
1685
  var CONFIG_FILENAME = ".patchstackrc.json";
1686
+ var SECRET_FILENAME = ".patchstackrc.local.json";
1438
1687
  var DEFAULT_ENVIRONMENT = "production";
1439
1688
  async function resolveConfig(options) {
1440
1689
  const fromFile = await readConfigFile(options.cwd);
1690
+ const fromSecretFile = await readSecretFile(options.cwd);
1441
1691
  const fromEnv = readEnv();
1442
1692
  const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
1443
1693
  const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
@@ -1462,8 +1712,8 @@ async function resolveConfig(options) {
1462
1712
  "CONFIG_MISSING"
1463
1713
  );
1464
1714
  }
1465
- const apiKeyRaw = fromEnv.apiKey ?? fromFile.apiKey ?? null;
1466
- const pulseAuthRaw = fromEnv.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
1715
+ const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
1716
+ const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
1467
1717
  return {
1468
1718
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
1469
1719
  apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
@@ -1480,19 +1730,102 @@ async function writeConfigFile(cwd, config) {
1480
1730
  await writeFile(target, content, "utf8");
1481
1731
  return target;
1482
1732
  }
1733
+ var SECRET_CONFIG_FILENAME = SECRET_FILENAME;
1734
+ async function writeSecretFile(cwd, secrets) {
1735
+ const target = path6.join(cwd, SECRET_FILENAME);
1736
+ await writeFile(target, JSON.stringify(secrets, null, 2) + "\n", { encoding: "utf8", mode: 384 });
1737
+ try {
1738
+ await chmod(target, 384);
1739
+ } catch {
1740
+ }
1741
+ const ignore = await ensureIgnored(cwd, SECRET_FILENAME);
1742
+ return { path: target, ...ignore };
1743
+ }
1744
+ async function ensureIgnored(cwd, entry) {
1745
+ const target = path6.join(cwd, ".gitignore");
1746
+ let existing = "";
1747
+ try {
1748
+ existing = await readFile8(target, "utf8");
1749
+ } catch (err) {
1750
+ const code = err.code;
1751
+ if (code !== "ENOENT") {
1752
+ return { ignored: false, reason: `.gitignore could not be read (${code ?? "unknown error"})` };
1753
+ }
1754
+ }
1755
+ if (ignoresEntry(existing, entry)) return { ignored: true };
1756
+ const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
1757
+ const block = `${separator}
1758
+ # Patchstack credential \u2014 never commit this
1759
+ ${entry}
1760
+ `;
1761
+ try {
1762
+ await writeFile(target, existing + block, "utf8");
1763
+ } catch (err) {
1764
+ const code = err.code;
1765
+ return { ignored: false, reason: `.gitignore could not be written (${code ?? "unknown error"})` };
1766
+ }
1767
+ try {
1768
+ const written = await readFile8(target, "utf8");
1769
+ if (ignoresEntry(written, entry)) return { ignored: true };
1770
+ return { ignored: false, reason: ".gitignore does not cover the file after writing it" };
1771
+ } catch (err) {
1772
+ const code = err.code;
1773
+ return { ignored: false, reason: `.gitignore could not be read back (${code ?? "unknown error"})` };
1774
+ }
1775
+ }
1776
+ async function secretFileIgnored(cwd) {
1777
+ try {
1778
+ const contents = await readFile8(path6.join(cwd, ".gitignore"), "utf8");
1779
+ if (ignoresEntry(contents, SECRET_FILENAME)) return { ignored: true };
1780
+ return { ignored: false, reason: `.gitignore does not cover ${SECRET_FILENAME}` };
1781
+ } catch (err) {
1782
+ const code = err.code;
1783
+ return {
1784
+ ignored: false,
1785
+ reason: code === "ENOENT" ? "this project has no .gitignore" : `.gitignore could not be read (${code ?? "unknown error"})`
1786
+ };
1787
+ }
1788
+ }
1789
+ function ignoresEntry(contents, entry) {
1790
+ let ignored = false;
1791
+ for (const raw of contents.split("\n")) {
1792
+ const line = raw.trim();
1793
+ if (line === "" || line.startsWith("#")) continue;
1794
+ const negated = line.startsWith("!");
1795
+ const pattern = negated ? line.slice(1).trim() : line;
1796
+ if (pattern === entry || pattern === `/${entry}`) ignored = !negated;
1797
+ }
1798
+ return ignored;
1799
+ }
1483
1800
  async function persistSiteUuid(cwd, siteUuid) {
1484
1801
  const existing = await readConfigFile(cwd);
1485
1802
  return writeConfigFile(cwd, { ...existing, siteUuid });
1486
1803
  }
1487
1804
  async function persistApiKey(cwd, apiKey) {
1488
- const { pulseAuth: _dropped, ...existing } = await readConfigFile(cwd);
1489
- return writeConfigFile(cwd, { ...existing, apiKey });
1805
+ const { apiKey: _movedKey, pulseAuth: _movedPulse, ...publicConfig } = await readConfigFile(cwd);
1806
+ const existingSecrets = await readSecretFile(cwd);
1807
+ const target = await writeSecretFile(cwd, { ...existingSecrets, apiKey, pulseAuth: void 0 });
1808
+ if (_movedKey !== void 0 || _movedPulse !== void 0) {
1809
+ await writeConfigFile(cwd, publicConfig);
1810
+ }
1811
+ return target;
1812
+ }
1813
+ async function credentialInCommittedConfig(cwd) {
1814
+ const config = await readConfigFile(cwd);
1815
+ return config.apiKey !== void 0 || config.pulseAuth !== void 0;
1816
+ }
1817
+ async function readSecretFile(cwd) {
1818
+ const file = await readJsonFile(cwd, SECRET_FILENAME);
1819
+ return { apiKey: file.apiKey, pulseAuth: file.pulseAuth };
1490
1820
  }
1491
1821
  async function readConfigFile(cwd) {
1492
- const target = path6.join(cwd, CONFIG_FILENAME);
1822
+ return readJsonFile(cwd, CONFIG_FILENAME);
1823
+ }
1824
+ async function readJsonFile(cwd, filename) {
1825
+ const target = path6.join(cwd, filename);
1493
1826
  let raw;
1494
1827
  try {
1495
- raw = await readFile7(target, "utf8");
1828
+ raw = await readFile8(target, "utf8");
1496
1829
  } catch (err) {
1497
1830
  if (err.code === "ENOENT") {
1498
1831
  return {};
@@ -1767,7 +2100,7 @@ function ensureSourceMarker(cwd, shell, framework) {
1767
2100
  }
1768
2101
 
1769
2102
  // src/guide.ts
1770
- import { existsSync as existsSync10, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
2103
+ import { existsSync as existsSync11, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
1771
2104
  import path9 from "path";
1772
2105
 
1773
2106
  // src/protect/install/util.ts
@@ -1784,6 +2117,20 @@ function hasDependency(cwd, name) {
1784
2117
  return false;
1785
2118
  }
1786
2119
  }
2120
+ function hasResolvableCredential(cwd) {
2121
+ if ((process.env.PATCHSTACK_API_KEY ?? "") !== "" || (process.env.PATCHSTACK_PULSE_AUTH ?? "") !== "") {
2122
+ return true;
2123
+ }
2124
+ for (const file of [".patchstackrc.local.json", ".patchstackrc.json"]) {
2125
+ try {
2126
+ const parsed = JSON.parse(read(join2(cwd, file)));
2127
+ if (typeof parsed.apiKey === "string" && parsed.apiKey !== "") return true;
2128
+ if (typeof parsed.pulseAuth === "string" && parsed.pulseAuth !== "") return true;
2129
+ } catch {
2130
+ }
2131
+ }
2132
+ return false;
2133
+ }
1787
2134
  var SITE_UUID_PLACEHOLDER = "__PATCHSTACK_SITE_UUID__";
1788
2135
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1789
2136
  function bakeSiteUuid(cwd, guardRelPath) {
@@ -1858,7 +2205,8 @@ var REQUEST_MIDDLEWARE_BLOCK = [
1858
2205
  " if (blocked) return blocked;",
1859
2206
  " }",
1860
2207
  " }",
1861
- " return screenResponse(await next());",
2208
+ " // The request is passed so route/method-scoped response rules can apply their scope.",
2209
+ " return screenResponse(await next(), request);",
1862
2210
  "});",
1863
2211
  "// #endregion patchstack-guard"
1864
2212
  ].join("\n");
@@ -2252,9 +2600,9 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
2252
2600
  var MAX_DEPTH = 8;
2253
2601
  function findAppInstance(cwd, re) {
2254
2602
  const root = existsSync6(join8(cwd, "src")) ? join8(cwd, "src") : cwd;
2255
- let hit = null;
2603
+ const found = [];
2256
2604
  const walk2 = (dir, depth) => {
2257
- if (hit || depth > MAX_DEPTH) return;
2605
+ if (depth > MAX_DEPTH) return;
2258
2606
  let entries;
2259
2607
  try {
2260
2608
  entries = readdirSync2(dir).sort();
@@ -2262,7 +2610,6 @@ function findAppInstance(cwd, re) {
2262
2610
  return;
2263
2611
  }
2264
2612
  for (const name of entries) {
2265
- if (hit) return;
2266
2613
  if (SKIP_DIRS.has(name) || name.startsWith(".")) continue;
2267
2614
  const p = join8(dir, name);
2268
2615
  let st;
@@ -2281,12 +2628,65 @@ function findAppInstance(cwd, re) {
2281
2628
  continue;
2282
2629
  }
2283
2630
  const m = re.exec(src);
2284
- if (m) hit = { relPath: relative(cwd, p).replace(/\\/g, "/"), appVar: m[1] };
2631
+ if (!m) continue;
2632
+ const appVar = m[1];
2633
+ found.push({
2634
+ relPath: relative(cwd, p).replace(/\\/g, "/"),
2635
+ appVar,
2636
+ listens: listensOn(src, appVar)
2637
+ });
2285
2638
  }
2286
2639
  }
2287
2640
  };
2288
2641
  walk2(root, 0);
2289
- return hit;
2642
+ if (found.length === 0) return null;
2643
+ const declared = declaredEntries(cwd);
2644
+ const chosen = found.slice().sort((a, b) => rankEntry({ ...a, declared: declared.has(a.relPath) }, { ...b, declared: declared.has(b.relPath) }))[0];
2645
+ return {
2646
+ relPath: chosen.relPath,
2647
+ appVar: chosen.appVar,
2648
+ others: found.filter((f) => f !== chosen && f.listens).map((f) => ({ relPath: f.relPath, appVar: f.appVar }))
2649
+ };
2650
+ }
2651
+ function listensOn(source, appVar) {
2652
+ return new RegExp(`\\b${appVar}\\s*\\.\\s*listen\\s*\\(`).test(source);
2653
+ }
2654
+ var ENTRY_NAMES = ["server", "index", "main", "app"];
2655
+ function rankEntry(a, b) {
2656
+ if (a.declared !== b.declared) return a.declared ? -1 : 1;
2657
+ if (a.listens !== b.listens) return a.listens ? -1 : 1;
2658
+ const named = entryRank(a.relPath) - entryRank(b.relPath);
2659
+ if (named !== 0) return named;
2660
+ const depth = a.relPath.split("/").length - b.relPath.split("/").length;
2661
+ if (depth !== 0) return depth;
2662
+ return a.relPath < b.relPath ? -1 : 1;
2663
+ }
2664
+ function entryRank(relPath) {
2665
+ const base = (relPath.split("/").pop() ?? "").replace(/\.(?:ts|js|mjs|cjs)$/, "");
2666
+ const index = ENTRY_NAMES.indexOf(base);
2667
+ return index === -1 ? ENTRY_NAMES.length : index;
2668
+ }
2669
+ function declaredEntries(cwd) {
2670
+ const paths = /* @__PURE__ */ new Set();
2671
+ let pkg;
2672
+ try {
2673
+ pkg = JSON.parse(readFileSync3(join8(cwd, "package.json"), "utf8"));
2674
+ } catch {
2675
+ return paths;
2676
+ }
2677
+ const add = (value) => {
2678
+ if (typeof value !== "string") return;
2679
+ for (const match of value.matchAll(/[\w./@-]+\.(?:ts|js|mjs|cjs)/g)) {
2680
+ paths.add(match[0].replace(/^\.\//, ""));
2681
+ }
2682
+ };
2683
+ add(pkg.main);
2684
+ add(pkg.module);
2685
+ const scripts = pkg.scripts;
2686
+ if (typeof scripts === "object" && scripts !== null) {
2687
+ for (const name of ["start", "serve", "dev"]) add(scripts[name]);
2688
+ }
2689
+ return paths;
2290
2690
  }
2291
2691
  function importSpecifier(fromRel, toRel, preserveExtension = false) {
2292
2692
  let spec = relative(dirname3(fromRel), toRel).replace(/\\/g, "/");
@@ -2296,28 +2696,252 @@ function importSpecifier(fromRel, toRel, preserveExtension = false) {
2296
2696
  }
2297
2697
 
2298
2698
  // src/protect/install/register.ts
2299
- import { writeFileSync as writeFileSync5, existsSync as existsSync8 } from "fs";
2300
- import { join as join10 } from "path";
2699
+ import { writeFileSync as writeFileSync5, existsSync as existsSync9 } from "fs";
2700
+ import { join as join11, resolve as resolve2 } from "path";
2701
+
2702
+ // src/protect/install/generic.ts
2703
+ import { readFileSync as readFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync4, copyFileSync as copyFileSync4, readdirSync as readdirSync3, lstatSync as lstatSync2 } from "fs";
2704
+ import { join as join10, dirname as dirname5 } from "path";
2705
+
2706
+ // src/protect/install/source-scope.ts
2707
+ import { execFileSync } from "child_process";
2708
+ import { existsSync as existsSync7 } from "fs";
2709
+ import { dirname as dirname4, join as join9, resolve } from "path";
2710
+ var IMPORT_LINE = /^\s*(?:import\b|export\s+(?:\*|\{)|(?:const|let|var)\s+[^=]+=\s*require\()/;
2711
+ function lastTopLevelImportLine(source) {
2712
+ const lines = source.split("\n");
2713
+ let depth = 0;
2714
+ let last = -1;
2715
+ let inBlockComment = false;
2716
+ for (let i = 0; i < lines.length; i++) {
2717
+ const line = lines[i] ?? "";
2718
+ if (!inBlockComment && depth === 0 && IMPORT_LINE.test(line)) last = i;
2719
+ inBlockComment = advanceDepth(line, inBlockComment, (delta) => {
2720
+ depth += delta;
2721
+ });
2722
+ if (depth < 0) return -1;
2723
+ }
2724
+ return depth === 0 ? last : -1;
2725
+ }
2726
+ function isTopLevelLine(source, index) {
2727
+ const lines = source.split("\n");
2728
+ let depth = 0;
2729
+ let inBlockComment = false;
2730
+ for (let i = 0; i < lines.length && i < index; i++) {
2731
+ inBlockComment = advanceDepth(lines[i] ?? "", inBlockComment, (delta) => {
2732
+ depth += delta;
2733
+ });
2734
+ }
2735
+ return depth === 0;
2736
+ }
2737
+ function inSameBlockAfter(source, appIndex, callIndex) {
2738
+ if (appIndex < 0 || callIndex <= appIndex) return false;
2739
+ const lines = source.split("\n");
2740
+ let depth = 0;
2741
+ let inBlockComment = false;
2742
+ for (let i = appIndex; i < callIndex; i++) {
2743
+ inBlockComment = advanceDepth(lines[i] ?? "", inBlockComment, (delta) => {
2744
+ depth += delta;
2745
+ });
2746
+ if (depth < 0) return false;
2747
+ }
2748
+ return depth === 0;
2749
+ }
2750
+ function advanceDepth(line, inBlockComment, add) {
2751
+ let quote = null;
2752
+ for (let i = 0; i < line.length; i++) {
2753
+ const ch = line[i];
2754
+ const next = line[i + 1];
2755
+ if (inBlockComment) {
2756
+ if (ch === "*" && next === "/") {
2757
+ inBlockComment = false;
2758
+ i++;
2759
+ }
2760
+ continue;
2761
+ }
2762
+ if (quote !== null) {
2763
+ if (ch === "\\") {
2764
+ i++;
2765
+ continue;
2766
+ }
2767
+ if (ch === quote) quote = null;
2768
+ continue;
2769
+ }
2770
+ if (ch === "/" && next === "/") return inBlockComment;
2771
+ if (ch === "/" && next === "*") {
2772
+ inBlockComment = true;
2773
+ i++;
2774
+ continue;
2775
+ }
2776
+ if (ch === '"' || ch === "'" || ch === "`") {
2777
+ quote = ch;
2778
+ continue;
2779
+ }
2780
+ if (ch === "{" || ch === "(" || ch === "[") add(1);
2781
+ else if (ch === "}" || ch === ")" || ch === "]") add(-1);
2782
+ }
2783
+ return inBlockComment;
2784
+ }
2785
+ function parses(filePath) {
2786
+ if (!/\.(?:js|cjs|mjs)$/.test(filePath)) return null;
2787
+ try {
2788
+ execFileSync(process.execPath, ["--check", filePath], { stdio: "pipe" });
2789
+ return true;
2790
+ } catch (error) {
2791
+ return isSyntaxError(error) ? false : null;
2792
+ }
2793
+ }
2794
+ function isSyntaxError(error) {
2795
+ const status = error?.status;
2796
+ return typeof status === "number" && status !== 0;
2797
+ }
2798
+ function stripComments(source) {
2799
+ let out = "";
2800
+ let quote = null;
2801
+ let inLine = false;
2802
+ let inBlock = false;
2803
+ for (let i = 0; i < source.length; i++) {
2804
+ const ch = source[i];
2805
+ const next = source[i + 1];
2806
+ if (inLine) {
2807
+ if (ch === "\n") {
2808
+ inLine = false;
2809
+ out += ch;
2810
+ }
2811
+ continue;
2812
+ }
2813
+ if (inBlock) {
2814
+ if (ch === "*" && next === "/") {
2815
+ inBlock = false;
2816
+ i++;
2817
+ } else if (ch === "\n") {
2818
+ out += ch;
2819
+ }
2820
+ continue;
2821
+ }
2822
+ if (quote !== null) {
2823
+ out += ch;
2824
+ if (ch === "\\") {
2825
+ if (next !== void 0) {
2826
+ out += next;
2827
+ i++;
2828
+ }
2829
+ } else if (ch === quote) {
2830
+ quote = null;
2831
+ }
2832
+ continue;
2833
+ }
2834
+ if (ch === '"' || ch === "'" || ch === "`") {
2835
+ quote = ch;
2836
+ out += ch;
2837
+ continue;
2838
+ }
2839
+ if (ch === "/" && next === "/") {
2840
+ inLine = true;
2841
+ i++;
2842
+ continue;
2843
+ }
2844
+ if (ch === "/" && next === "*") {
2845
+ inBlock = true;
2846
+ i++;
2847
+ continue;
2848
+ }
2849
+ out += ch;
2850
+ }
2851
+ return out;
2852
+ }
2853
+ var MASKED_CHAR = "";
2854
+ function maskStringContents(source) {
2855
+ let out = "";
2856
+ let quote = null;
2857
+ for (let i = 0; i < source.length; i++) {
2858
+ const ch = source[i];
2859
+ if (quote === null) {
2860
+ out += ch;
2861
+ if (ch === '"' || ch === "'" || ch === "`") quote = ch;
2862
+ continue;
2863
+ }
2864
+ if (ch === "\\") {
2865
+ out += MASKED_CHAR;
2866
+ if (i + 1 < source.length) {
2867
+ out += source[i + 1] === "\n" ? "\n" : MASKED_CHAR;
2868
+ i++;
2869
+ }
2870
+ continue;
2871
+ }
2872
+ if (ch === quote) {
2873
+ out += ch;
2874
+ quote = null;
2875
+ continue;
2876
+ }
2877
+ out += ch === "\n" ? "\n" : MASKED_CHAR;
2878
+ }
2879
+ return out;
2880
+ }
2881
+ var GUARD_FILENAMES = ["guard.ts", "guard.js", "guard.mjs", "guard.cjs"];
2882
+ function moduleKindOf(filePath, packageIsModule) {
2883
+ if (/\.(?:ts|tsx|mts|cts|jsx)$/.test(filePath)) return "ts";
2884
+ if (/\.mjs$/.test(filePath)) return "esm";
2885
+ if (/\.cjs$/.test(filePath)) return "cjs";
2886
+ return packageIsModule ? "esm" : "cjs";
2887
+ }
2888
+ function resolvesToGuardModule(specifier, query) {
2889
+ const resolved = resolve(dirname4(query.fromFile), specifier);
2890
+ if (withoutModuleExtension(resolved) !== join9(query.guardDir, "guard")) return false;
2891
+ const existing = GUARD_FILENAMES.filter((name) => existsSync7(join9(query.guardDir, name)));
2892
+ if (existing.length === 0) return false;
2893
+ const extension = /(\.[A-Za-z0-9]+)$/.exec(specifier)?.[1] ?? "";
2894
+ if (!query.strictExtensions) return true;
2895
+ if (extension === "") {
2896
+ if (query.kind === "esm") return false;
2897
+ if (query.kind === "cjs") return existing.includes("guard.js");
2898
+ return true;
2899
+ }
2900
+ if (existsSync7(resolved)) return true;
2901
+ return query.kind === "ts" && extension === ".js" && existing.includes("guard.ts");
2902
+ }
2903
+ function withoutModuleExtension(filePath) {
2904
+ return filePath.replace(/\.(?:ts|tsx|js|jsx|mjs|cjs)$/, "");
2905
+ }
2301
2906
 
2302
2907
  // src/protect/install/generic.ts
2303
- import { readFileSync as readFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync4, copyFileSync as copyFileSync4, readdirSync as readdirSync3, lstatSync as lstatSync2 } from "fs";
2304
- import { join as join9 } from "path";
2305
- var GUARD_MARKER = "patchstack/guard";
2908
+ function packageIsEsm(cwd) {
2909
+ try {
2910
+ return JSON.parse(read(join10(cwd, "package.json"))).type === "module";
2911
+ } catch {
2912
+ return false;
2913
+ }
2914
+ }
2306
2915
  function genericDir(cwd) {
2307
- return existsSync7(join9(cwd, "src")) ? "src/patchstack" : "patchstack";
2916
+ return existsSync8(join10(cwd, "src")) ? "src/patchstack" : "patchstack";
2917
+ }
2918
+ function hasTypeScript(cwd) {
2919
+ return existsSync8(join10(cwd, "tsconfig.json")) || candidateEntries(cwd).some((entry) => /\.tsx?$/.test(entry));
2308
2920
  }
2309
- function scaffoldGeneric(cwd, opts, guardTemplate = "generic-guard.ts", guardFile = "guard.ts") {
2921
+ function genericGuardTarget(cwd) {
2922
+ if (hasTypeScript(cwd)) {
2923
+ return { template: "generic-guard.ts", file: "guard.ts", specifier: "guard" };
2924
+ }
2925
+ if (packageIsEsm(cwd)) {
2926
+ return { template: "generic-guard.js", file: "guard.js", specifier: "guard.js" };
2927
+ }
2928
+ return { template: "generic-guard.cjs", file: "guard.cjs", specifier: "guard.cjs" };
2929
+ }
2930
+ function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
2931
+ const target = genericGuardTarget(cwd);
2932
+ guardTemplate ??= target.template;
2933
+ guardFile ??= target.file;
2310
2934
  const templates = templatesDir();
2311
2935
  const dir = genericDir(cwd);
2312
- const dst = join9(cwd, dir);
2936
+ const dst = join10(cwd, dir);
2313
2937
  mkdirSync4(dst, { recursive: true });
2314
- copyFileSync4(join9(templates, guardTemplate), join9(dst, guardFile));
2938
+ copyFileSync4(join10(templates, guardTemplate), join10(dst, guardFile));
2315
2939
  const guardRel = `${dir}/${guardFile}`;
2316
2940
  const changed = [guardRel];
2317
2941
  if (!opts.demo) bakeSiteUuid(cwd, guardRel);
2318
- const rulesDst = join9(dst, "rules.json");
2319
- if (opts.demo || !existsSync7(rulesDst)) {
2320
- copyFileSync4(join9(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
2942
+ const rulesDst = join10(dst, "rules.json");
2943
+ if (opts.demo || !existsSync8(rulesDst)) {
2944
+ copyFileSync4(join10(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
2321
2945
  changed.push(`${dir}/rules.json`);
2322
2946
  }
2323
2947
  return { changed, dir };
@@ -2338,17 +2962,17 @@ function candidateEntries(cwd) {
2338
2962
  "app.ts",
2339
2963
  "app.js"
2340
2964
  ];
2341
- const hits = names.filter((n) => existsSync7(join9(cwd, n)));
2965
+ const hits = names.filter((n) => existsSync8(join10(cwd, n)));
2342
2966
  try {
2343
- const pkg = JSON.parse(read(join9(cwd, "package.json")));
2344
- if (typeof pkg.main === "string" && existsSync7(join9(cwd, pkg.main))) hits.push(pkg.main);
2967
+ const pkg = JSON.parse(read(join10(cwd, "package.json")));
2968
+ if (typeof pkg.main === "string" && existsSync8(join10(cwd, pkg.main))) hits.push(pkg.main);
2345
2969
  } catch {
2346
2970
  }
2347
2971
  return [...new Set(hits)];
2348
2972
  }
2349
2973
  function usesExpress(cwd) {
2350
2974
  try {
2351
- const pkg = JSON.parse(read(join9(cwd, "package.json")));
2975
+ const pkg = JSON.parse(read(join10(cwd, "package.json")));
2352
2976
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.express);
2353
2977
  } catch {
2354
2978
  return false;
@@ -2357,34 +2981,140 @@ function usesExpress(cwd) {
2357
2981
  function wiringPlan(cwd, dir) {
2358
2982
  const entries = candidateEntries(cwd);
2359
2983
  const express = usesExpress(cwd);
2984
+ const target = genericGuardTarget(cwd);
2360
2985
  const lines = [
2361
- `no built-in adapter matched this stack \u2014 scaffolded a generic guard at ${dir}/guard.ts + ${dir}/rules.json.`,
2986
+ `no built-in adapter matched this stack \u2014 scaffolded a generic guard at ${dir}/${target.file} + ${dir}/rules.json.`,
2362
2987
  "Finish by wiring it into your server (pick the one that fits):",
2363
- ` \u2022 Web-Fetch entry: export default { fetch: protectFetch(yourHandler) } // import from "${dir}/guard"`,
2364
- ` \u2022 Node / Express: app.use(patchstackMiddleware) // add before your routes`,
2988
+ // The specifier is the one this project's loader resolves — a bare path is right for a compiler and
2989
+ // wrong for `require()`, which never guesses `.cjs`.
2990
+ ` \u2022 Web-Fetch entry: export default { fetch: protectFetch(yourHandler) } // import from "${dir}/${target.specifier}"`,
2991
+ ` \u2022 Node / Connect: app.use(patchstackMiddleware) // before any body parser`,
2365
2992
  entries.length ? `Likely server ${entries.length === 1 ? "entry" : "entries"}: ${entries.join(", ")}${express ? " (Express detected)" : ""}.` : "Could not locate a server entry \u2014 wire it wherever requests enter your app.",
2366
2993
  "Then confirm it is hooked up: npx patchstack-connect protect --check"
2367
2994
  ];
2368
2995
  return lines.join("\n");
2369
2996
  }
2997
+ var GUARD_EXPORTS = ["protectFetch", "patchstackMiddleware"];
2998
+ function importsAndUsesGuard(source, sourcePath, guardDir, packageIsModule) {
2999
+ const code = stripComments(source);
3000
+ const masked = maskStringContents(code);
3001
+ const declarations = guardImportDeclarations(code, masked, {
3002
+ fromFile: sourcePath,
3003
+ guardDir,
3004
+ kind: moduleKindOf(sourcePath, packageIsModule),
3005
+ // The scaffold now matches the project's module format and the printed plan names the specifier that
3006
+ // format resolves, so there is nothing left to excuse: a specifier this file's loader would not find is
3007
+ // not a wired guard here either.
3008
+ strictExtensions: true
3009
+ });
3010
+ if (declarations.length === 0) return false;
3011
+ const direct = /* @__PURE__ */ new Set();
3012
+ const namespaces = /* @__PURE__ */ new Set();
3013
+ for (const declaration of declarations) {
3014
+ const bound = boundNames(declaration.text);
3015
+ for (const name of bound.direct) direct.add(name);
3016
+ for (const name of bound.namespaces) namespaces.add(name);
3017
+ }
3018
+ if (direct.size === 0 && namespaces.size === 0) return false;
3019
+ let body = masked;
3020
+ for (const declaration of declarations) {
3021
+ body = body.slice(0, declaration.start) + " ".repeat(declaration.text.length) + body.slice(declaration.start + declaration.text.length);
3022
+ }
3023
+ const members = GUARD_EXPORTS.map(escapeForRegExp).join("|");
3024
+ return [...direct].some((name) => new RegExp(`\\b${escapeForRegExp(name)}\\s*[(),]`).test(body)) || [...namespaces].some(
3025
+ (name) => new RegExp(`\\b${escapeForRegExp(name)}\\s*\\.\\s*(?:${members})\\s*[(),]`).test(body)
3026
+ );
3027
+ }
3028
+ function guardImportDeclarations(code, masked, query) {
3029
+ const pattern = /import\s+[^;]*?from\s*(['"`])[^'"`]*\1|(?:const|let|var)\s+[^;=]+=\s*require\(\s*(['"`])[^'"`]*\2\s*\)/g;
3030
+ const found = [];
3031
+ for (const match of masked.matchAll(pattern)) {
3032
+ const start = match.index ?? -1;
3033
+ if (start < 0) continue;
3034
+ const text = code.slice(start, start + match[0].length);
3035
+ const specifier = specifierOf(text);
3036
+ if (specifier !== null && resolvesToGuardModule(specifier, query)) found.push({ text, start });
3037
+ }
3038
+ return found;
3039
+ }
3040
+ function specifierOf(declaration) {
3041
+ return /(['"`])([^'"`]*)\1/.exec(declaration)?.[2] ?? null;
3042
+ }
3043
+ function boundNames(declaration) {
3044
+ const direct = [];
3045
+ const namespaces = [];
3046
+ const clause = /^import\s+([^]*?)\s+from\b/.exec(declaration)?.[1] ?? /^(?:const|let|var)\s+([^]*?)=\s*require\b/.exec(declaration)?.[1] ?? "";
3047
+ const braced = /\{([^}]*)\}/.exec(clause)?.[1];
3048
+ if (braced !== void 0) {
3049
+ for (const part of braced.split(",")) {
3050
+ const [exported, local] = splitBinding(part);
3051
+ if (!isGuardExport(exported)) continue;
3052
+ if (/^[A-Za-z_$][\w$]*$/.test(local)) direct.push(local);
3053
+ }
3054
+ }
3055
+ const bare = clause.replace(/\{[^}]*\}/g, "").replace(/^\*\s*as\s*/, "").replace(/,/g, " ").trim();
3056
+ for (const token of bare.split(/\s+/)) {
3057
+ if (/^[A-Za-z_$][\w$]*$/.test(token) && token !== "as") namespaces.push(token);
3058
+ }
3059
+ return { direct, namespaces };
3060
+ }
3061
+ function splitBinding(part) {
3062
+ const separator = part.includes(" as ") ? " as " : part.includes(":") ? ":" : null;
3063
+ if (separator === null) {
3064
+ const name = part.trim();
3065
+ return [name, name];
3066
+ }
3067
+ const [exported, ...rest] = part.split(separator);
3068
+ return [(exported ?? "").trim(), (rest.join(separator) ?? "").trim()];
3069
+ }
3070
+ function isGuardExport(name) {
3071
+ return GUARD_EXPORTS.includes(name);
3072
+ }
3073
+ function escapeForRegExp(value) {
3074
+ return value.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
3075
+ }
2370
3076
  function genericVerify(cwd) {
2371
3077
  const dir = genericDir(cwd);
2372
- const scaffolded = existsSync7(join9(cwd, dir, "guard.ts"));
2373
- const imported = scaffolded && guardIsImported(cwd, join9(cwd, dir, "guard.ts"));
3078
+ const target = genericGuardTarget(cwd);
3079
+ const present = GUARD_FILENAMES.find((name) => existsSync8(join10(cwd, dir, name)));
3080
+ const scaffolded = present !== void 0;
3081
+ const imported = scaffolded && guardIsImported(cwd, join10(cwd, dir, present));
2374
3082
  return {
2375
3083
  wired: scaffolded && imported,
2376
3084
  checks: [
2377
3085
  { label: "generic guard scaffolded", ok: scaffolded, hint: "run `patchstack-connect protect`" },
2378
3086
  {
2379
- label: "guard imported into a server entry",
3087
+ label: "guard imported and called in a server entry",
2380
3088
  ok: imported,
2381
- hint: `import { protectFetch } (or patchstackMiddleware) from "${dir}/guard" and wire it into your request path`
3089
+ hint: `import { protectFetch } (or patchstackMiddleware) from "${dir}/${present ?? target.specifier}" and wire it into your request path \u2014 an import that is never called screens nothing`
2382
3090
  }
2383
3091
  ]
2384
3092
  };
2385
3093
  }
3094
+ var NON_RUNTIME_DIRS = /* @__PURE__ */ new Set([
3095
+ "node_modules",
3096
+ "patchstack",
3097
+ "dist",
3098
+ "build",
3099
+ "out",
3100
+ "coverage",
3101
+ ".next",
3102
+ ".output",
3103
+ ".svelte-kit",
3104
+ "test",
3105
+ "tests",
3106
+ "__tests__",
3107
+ "e2e",
3108
+ "examples",
3109
+ "fixtures",
3110
+ "__fixtures__",
3111
+ "stories",
3112
+ "__mocks__"
3113
+ ]);
2386
3114
  function guardIsImported(cwd, guardPath) {
2387
- const root = existsSync7(join9(cwd, "src")) ? join9(cwd, "src") : cwd;
3115
+ const guardDir = dirname5(guardPath);
3116
+ const packageIsModule = packageIsEsm(cwd);
3117
+ const root = existsSync8(join10(cwd, "src")) ? join10(cwd, "src") : cwd;
2388
3118
  let found = false;
2389
3119
  const walk2 = (d, depth) => {
2390
3120
  if (found || depth > 8) return;
@@ -2396,8 +3126,8 @@ function guardIsImported(cwd, guardPath) {
2396
3126
  }
2397
3127
  for (const name of entries) {
2398
3128
  if (found) return;
2399
- if (name === "node_modules" || name === "patchstack" || name.startsWith(".")) continue;
2400
- const p = join9(d, name);
3129
+ if (NON_RUNTIME_DIRS.has(name) || name.startsWith(".")) continue;
3130
+ const p = join10(d, name);
2401
3131
  let st;
2402
3132
  try {
2403
3133
  st = lstatSync2(p);
@@ -2408,7 +3138,7 @@ function guardIsImported(cwd, guardPath) {
2408
3138
  if (st.isDirectory()) walk2(p, depth + 1);
2409
3139
  else if (/\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(name) && p !== guardPath) {
2410
3140
  try {
2411
- if (readFileSync4(p, "utf8").includes(GUARD_MARKER)) found = true;
3141
+ if (importsAndUsesGuard(readFileSync4(p, "utf8"), p, guardDir, packageIsModule)) found = true;
2412
3142
  } catch {
2413
3143
  }
2414
3144
  }
@@ -2419,6 +3149,15 @@ function guardIsImported(cwd, guardPath) {
2419
3149
  }
2420
3150
 
2421
3151
  // src/protect/install/register.ts
3152
+ var routeRegistration = (appVar) => new RegExp(
3153
+ `^\\s*${appVar}\\.(?:get|post|put|patch|delete|head|options|all|route)\\(|^\\s*${appVar}\\.use\\(\\s*['"\`]`,
3154
+ "m"
3155
+ );
3156
+ function routesBefore(source, appVar, guardIndex) {
3157
+ if (guardIndex < 0) return [];
3158
+ const pattern = routeRegistration(appVar);
3159
+ return source.split("\n").map((line, index) => ({ line, index })).filter(({ line, index }) => index < guardIndex && pattern.test(line)).map(({ index }) => index + 1);
3160
+ }
2422
3161
  var REGION = "// #region patchstack (managed by patchstack-connect protect \u2014 do not edit)";
2423
3162
  function guardTarget(cwd, entryRel, spec) {
2424
3163
  if (/\.cjs$/.test(entryRel) || /\.js$/.test(entryRel) && packageType(cwd) !== "module") {
@@ -2447,11 +3186,48 @@ function guardTarget(cwd, entryRel, spec) {
2447
3186
  }
2448
3187
  function packageType(cwd) {
2449
3188
  try {
2450
- return JSON.parse(read(join10(cwd, "package.json"))).type;
3189
+ return JSON.parse(read(join11(cwd, "package.json"))).type;
2451
3190
  } catch {
2452
3191
  return void 0;
2453
3192
  }
2454
3193
  }
3194
+ function wiringState(source, spec, appVar, fileRel, guard) {
3195
+ const code = stripComments(source);
3196
+ const masked = maskStringContents(code);
3197
+ const lines = code.split("\n");
3198
+ const maskedLines = masked.split("\n");
3199
+ const call = spec.call(appVar);
3200
+ const callIndex = maskedLines.findIndex((line) => line.includes(call));
3201
+ const importIndex = maskedLines.findIndex(
3202
+ (line, index) => bindsGuard(lines[index] ?? "", line, spec.importName, call, fileRel, guard)
3203
+ );
3204
+ const appIndex = maskedLines.findIndex((line) => spec.appRe.test(line));
3205
+ const anchor = spec.callAfter ? spec.callAfter(appVar) : null;
3206
+ const anchorIndex = anchor ? maskedLines.findIndex((line) => anchor.test(line)) : -1;
3207
+ return {
3208
+ importAtTopLevel: importIndex !== -1 && isTopLevelLine(code, importIndex),
3209
+ callInAppScope: callIndex !== -1 && inSameBlockAfter(code, appIndex, callIndex),
3210
+ ordered: anchor ? anchorIndex !== -1 && callIndex > anchorIndex : callIndex !== -1,
3211
+ callIndex,
3212
+ appIndex
3213
+ };
3214
+ }
3215
+ function bindsGuard(line, maskedLine, importName, call, fileRel, guard) {
3216
+ if (!maskedLine.includes(importName) || maskedLine.includes(call)) return false;
3217
+ if (/^\s*export\b[^=]*\bfrom\b/.test(maskedLine)) return false;
3218
+ if (!/^\s*(?:import\b|(?:export\s+)?(?:const|let|var)\s+[^=]+=\s*require\()/.test(maskedLine)) return false;
3219
+ const specifier = /(['"`])([^'"`]*)\1/.exec(line)?.[2];
3220
+ if (specifier === void 0 || specifier === "") return false;
3221
+ return namesGuardModule(specifier, fileRel, guard);
3222
+ }
3223
+ function namesGuardModule(specifier, fileRel, guard) {
3224
+ return resolvesToGuardModule(specifier, {
3225
+ fromFile: join11(guard.cwd, fileRel),
3226
+ guardDir: resolve2(join11(guard.cwd, guard.guardDir)),
3227
+ kind: moduleKindOf(fileRel, packageType(guard.cwd) === "module"),
3228
+ strictExtensions: true
3229
+ });
3230
+ }
2455
3231
  function wireRegister(cwd, opts, spec) {
2456
3232
  const entry = findAppInstance(cwd, spec.appRe);
2457
3233
  if (!entry) {
@@ -2459,14 +3235,27 @@ function wireRegister(cwd, opts, spec) {
2459
3235
  log(`${spec.label} not located \u2014 scaffolded guard; ${spec.manualHint}`);
2460
3236
  return { ok: true, changed: changed2 };
2461
3237
  }
3238
+ if (entry.others.length > 0) {
3239
+ log(
3240
+ `${entry.others.map((other) => other.relPath).join(", ")} also start${entry.others.length === 1 ? "s" : ""} a server \u2014 wired ${entry.relPath} only; ${spec.manualHint} there too`
3241
+ );
3242
+ }
2462
3243
  const target = guardTarget(cwd, entry.relPath, spec);
2463
3244
  const { changed, dir } = scaffoldGeneric(cwd, opts, target.template, target.file);
2464
- const p = join10(cwd, entry.relPath);
3245
+ const p = join11(cwd, entry.relPath);
2465
3246
  const s = read(p);
2466
- if (s.includes(spec.importName)) {
2467
- log(`${entry.relPath} already wired`);
3247
+ const state = wiringState(s, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir });
3248
+ if (state.importAtTopLevel && state.callInAppScope) {
3249
+ log(
3250
+ state.ordered ? `${entry.relPath} already wired` : `${entry.relPath} has the guard registered before the body parser, where it cannot read the body \u2014 move \`${spec.call(entry.appVar).trim()}\` below the parser`
3251
+ );
2468
3252
  return { ok: true, changed };
2469
3253
  }
3254
+ if (state.importAtTopLevel || state.callInAppScope) {
3255
+ log(
3256
+ `${entry.relPath} has the guard ${state.importAtTopLevel ? "imported but not registered" : "registered but not imported"} \u2014 completing it`
3257
+ );
3258
+ }
2470
3259
  const sourceLines = s.split("\n");
2471
3260
  const sourceAppIdx = sourceLines.findIndex((line) => spec.appRe.test(line));
2472
3261
  const requiredAnchor = spec.callAfter?.(entry.appVar);
@@ -2479,40 +3268,85 @@ function wireRegister(cwd, opts, spec) {
2479
3268
  importSpecifier(entry.relPath, `${dir}/${target.file}`, target.preserveExtension)
2480
3269
  );
2481
3270
  const lines = s.split("\n");
2482
- let lastImport = -1;
2483
- for (let i = 0; i < lines.length; i++) {
2484
- if (/^\s*(?:import\b|(?:const|let|var)\s+.+?=\s*require\()/.test(lines[i] ?? "")) lastImport = i;
3271
+ if (!state.importAtTopLevel) {
3272
+ const lastImport = lastTopLevelImportLine(s);
3273
+ const importIdx = lastImport === -1 ? firstStatementLine(lines) : lastImport + 1;
3274
+ lines.splice(importIdx, 0, importLine);
2485
3275
  }
2486
- lines.splice(lastImport + 1, 0, importLine);
2487
3276
  const appIdx = lines.findIndex((l) => spec.appRe.test(l));
2488
- if (appIdx !== -1) {
3277
+ if (appIdx !== -1 && !state.callInAppScope) {
2489
3278
  const preferred = spec.callAfter?.(entry.appVar);
2490
3279
  const preferredIdx = preferred ? lines.findIndex((line, index) => index > appIdx && preferred.test(line)) : -1;
2491
3280
  const callIdx = preferredIdx === -1 ? appIdx : preferredIdx;
2492
3281
  lines.splice(callIdx + 1, 0, REGION, spec.call(entry.appVar), "// #endregion patchstack");
2493
3282
  }
2494
- writeFileSync5(p, lines.join("\n"));
3283
+ const patched = lines.join("\n");
3284
+ writeFileSync5(p, patched);
3285
+ if (spec.callAfter) {
3286
+ const guardIndex = lines.findIndex((line) => line.includes(spec.call(entry.appVar)));
3287
+ const early = routesBefore(patched, entry.appVar, guardIndex);
3288
+ if (early.length > 0) {
3289
+ log(
3290
+ `${entry.relPath}: ${early.length} route${early.length === 1 ? "" : "s"} registered before the guard (line${early.length === 1 ? "" : "s"} ${early.join(", ")}) \u2014 those are not screened. Move them below the guard, or move your body parser above them.`
3291
+ );
3292
+ }
3293
+ }
3294
+ const parsed = parses(p);
3295
+ if (parsed === false) {
3296
+ writeFileSync5(p, s);
3297
+ log(`${entry.relPath} would not parse after patching \u2014 reverted; ${spec.manualHint}`);
3298
+ return { ok: true, changed };
3299
+ }
2495
3300
  changed.push(entry.relPath);
2496
3301
  log(`patched ${entry.relPath} (${spec.call(entry.appVar).trim()})`);
2497
3302
  return { ok: true, changed: [...new Set(changed)] };
2498
3303
  }
3304
+ function firstStatementLine(lines) {
3305
+ let i = 0;
3306
+ if ((lines[0] ?? "").startsWith("#!")) i = 1;
3307
+ while (i < lines.length) {
3308
+ const line = (lines[i] ?? "").trim();
3309
+ if (line !== "" && !line.startsWith("//") && !line.startsWith("/*") && !line.startsWith("*")) break;
3310
+ i++;
3311
+ }
3312
+ return i;
3313
+ }
3314
+ function serverIsGuarded(cwd, spec, server, guardDir) {
3315
+ const source = read(join11(cwd, server.relPath));
3316
+ const state = wiringState(source, spec, server.appVar, server.relPath, { cwd, guardDir });
3317
+ if (!state.importAtTopLevel || !state.callInAppScope || !state.ordered) return false;
3318
+ return !spec.callAfter || routesBefore(source, server.appVar, state.callIndex).length === 0;
3319
+ }
2499
3320
  function verifyRegister(cwd, spec) {
2500
- const dir = existsSync8(join10(cwd, "src")) ? "src/patchstack" : "patchstack";
3321
+ const dir = existsSync9(join11(cwd, "src")) ? "src/patchstack" : "patchstack";
2501
3322
  const entry = findAppInstance(cwd, spec.appRe);
2502
3323
  const target = entry ? guardTarget(cwd, entry.relPath, spec) : null;
2503
- const scaffolded = target ? existsSync8(join10(cwd, dir, target.file)) : false;
2504
- const entrySource = entry ? read(join10(cwd, entry.relPath)) : "";
2505
- const entryLines = entrySource.split("\n");
2506
- const anchor = entry && spec.callAfter ? spec.callAfter(entry.appVar) : null;
2507
- const anchorIndex = anchor ? entryLines.findIndex((line) => anchor.test(line)) : -1;
2508
- const callIndex = entry ? entryLines.findIndex((line) => line.includes(spec.call(entry.appVar))) : -1;
2509
- const ordered = anchor ? anchorIndex !== -1 && callIndex > anchorIndex : callIndex !== -1;
2510
- const wired = entrySource.includes(spec.importName) && ordered;
3324
+ const scaffolded = target ? existsSync9(join11(cwd, dir, target.file)) : false;
3325
+ const entrySource = entry ? read(join11(cwd, entry.relPath)) : "";
3326
+ const state = entry ? wiringState(entrySource, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir }) : { importAtTopLevel: false, callInAppScope: false, ordered: false, callIndex: -1, appIndex: -1 };
3327
+ const wired = state.importAtTopLevel && state.callInAppScope && state.ordered;
3328
+ const early = spec.callAfter && entry ? routesBefore(entrySource, entry.appVar, state.callIndex) : [];
3329
+ const noEarlyRoutes = early.length === 0;
3330
+ const unguarded = (entry?.others ?? []).filter((other) => !serverIsGuarded(cwd, spec, other, dir)).map((other) => other.relPath);
2511
3331
  return {
2512
- wired: scaffolded && wired,
3332
+ wired: scaffolded && wired && noEarlyRoutes && unguarded.length === 0,
2513
3333
  checks: [
2514
3334
  { label: `${spec.label} guard scaffolded`, ok: scaffolded, hint: "run `patchstack-connect protect`" },
2515
- { label: `guard registered on the ${spec.label}`, ok: wired, hint: spec.manualHint }
3335
+ { label: `guard registered on the ${spec.label}`, ok: wired, hint: spec.manualHint },
3336
+ ...entry && entry.others.length > 0 ? [
3337
+ {
3338
+ label: "every server in this project has a guard",
3339
+ ok: unguarded.length === 0,
3340
+ hint: unguarded.length === 0 ? "nothing to do" : `${unguarded.join(", ")} start${unguarded.length === 1 ? "s" : ""} a server without one \u2014 ${spec.manualHint} there`
3341
+ }
3342
+ ] : [],
3343
+ ...spec.callAfter ? [
3344
+ {
3345
+ label: "every route registered after the guard",
3346
+ ok: noEarlyRoutes,
3347
+ hint: noEarlyRoutes ? "nothing to do" : `move the route${early.length === 1 ? "" : "s"} on line${early.length === 1 ? "" : "s"} ${early.join(", ")} below the guard, or move your body parser above them`
3348
+ }
3349
+ ] : []
2516
3350
  ]
2517
3351
  };
2518
3352
  }
@@ -2609,18 +3443,35 @@ function runProtect(cwd, opts = {}) {
2609
3443
  return { status: "scaffolded", adapter: "generic", changed: [], plan: "" };
2610
3444
  }
2611
3445
  }
3446
+ var RUNTIMES_WITHOUT_CONFIG_FILE = /* @__PURE__ */ new Set(["next", "sveltekit", "astro", "nuxt", "generic"]);
3447
+ function credentialNote(cwd, adapterName) {
3448
+ if (!RUNTIMES_WITHOUT_CONFIG_FILE.has(adapterName)) return [];
3449
+ const localHint = hasResolvableCredential(cwd) ? "a credential is configured here; set the same one in your deployment" : "no credential found here either \u2014 run `npx @patchstack/connect scan` to provision one";
3450
+ return [
3451
+ {
3452
+ label: "live rule updates need PATCHSTACK_API_KEY in the deployment environment",
3453
+ ok: true,
3454
+ unverifiable: true,
3455
+ hint: `this runtime cannot read .patchstackrc.local.json \u2014 ${localHint}. Without it the guard keeps running on the rules it shipped with.`
3456
+ }
3457
+ ];
3458
+ }
2612
3459
  function runVerify(cwd) {
2613
3460
  try {
2614
3461
  const adapter = ADAPTERS.find((a) => a.detect(cwd));
2615
- if (adapter) return { stack: adapter.label, ...adapter.verify(cwd) };
2616
- return { stack: "generic", ...genericVerify(cwd) };
3462
+ if (adapter) {
3463
+ const result = adapter.verify(cwd);
3464
+ return { stack: adapter.label, ...result, checks: [...result.checks, ...credentialNote(cwd, adapter.name)] };
3465
+ }
3466
+ const generic = genericVerify(cwd);
3467
+ return { stack: "generic", ...generic, checks: [...generic.checks, ...credentialNote(cwd, "generic")] };
2617
3468
  } catch (err) {
2618
3469
  return { stack: "unknown", wired: false, checks: [{ label: "verification failed", ok: false, hint: String(err?.message ?? err) }] };
2619
3470
  }
2620
3471
  }
2621
3472
 
2622
3473
  // src/widget.ts
2623
- import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
3474
+ import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
2624
3475
  import path8 from "path";
2625
3476
  var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
2626
3477
  var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
@@ -2659,7 +3510,7 @@ ${indent}</body>`;
2659
3510
  }
2660
3511
  function findSourceShell(cwd) {
2661
3512
  for (const candidate of SOURCE_SHELL_CANDIDATES) {
2662
- if (existsSync9(path8.join(cwd, candidate))) {
3513
+ if (existsSync10(path8.join(cwd, candidate))) {
2663
3514
  return candidate;
2664
3515
  }
2665
3516
  }
@@ -2753,7 +3604,7 @@ function detectPackageManager(cwd) {
2753
3604
  return declared;
2754
3605
  }
2755
3606
  for (const { filename, pm } of PM_BY_LOCKFILE) {
2756
- if (existsSync10(path9.join(cwd, filename))) {
3607
+ if (existsSync11(path9.join(cwd, filename))) {
2757
3608
  return pm;
2758
3609
  }
2759
3610
  }
@@ -2850,7 +3701,7 @@ function resolveWidgetFileHint(cwd, framework) {
2850
3701
  ...GENERIC_WIDGET_FILES
2851
3702
  ];
2852
3703
  for (const candidate of candidates) {
2853
- if (existsSync10(path9.join(cwd, candidate))) {
3704
+ if (existsSync11(path9.join(cwd, candidate))) {
2854
3705
  return candidate;
2855
3706
  }
2856
3707
  }
@@ -3087,7 +3938,7 @@ function renderGuideChecklist(state, useColor) {
3087
3938
  done(
3088
3939
  paint(
3089
3940
  ANSI.bold,
3090
- "All setup steps complete. Commit .patchstackrc.json, package.json, the runtime guard changes, and the file carrying the widget snippet."
3941
+ "All setup steps complete. Commit .patchstackrc.json, package.json, the runtime guard changes, and the file carrying the widget snippet. Never commit .patchstackrc.local.json \u2014 it holds the API key, and setup has already added it to .gitignore."
3091
3942
  )
3092
3943
  )
3093
3944
  );
@@ -3230,10 +4081,10 @@ async function login(config, onPrompt, deps = {}) {
3230
4081
  // src/map/ts-loader.ts
3231
4082
  import { createRequire } from "module";
3232
4083
  import { pathToFileURL } from "url";
3233
- import { join as join11 } from "path";
4084
+ import { join as join12 } from "path";
3234
4085
  async function loadTypeScript(cwd) {
3235
4086
  try {
3236
- const req = createRequire(pathToFileURL(join11(cwd, "package.json")));
4087
+ const req = createRequire(pathToFileURL(join12(cwd, "package.json")));
3237
4088
  const resolved = req.resolve("typescript");
3238
4089
  const mod = await import(pathToFileURL(resolved).href);
3239
4090
  return mod.default ?? mod;
@@ -3505,7 +4356,7 @@ function npmPackageOf(spec) {
3505
4356
 
3506
4357
  // src/map/sources.ts
3507
4358
  import { readFileSync as readFileSync8, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
3508
- import { join as join12, relative as relative2, isAbsolute } from "path";
4359
+ import { join as join13, relative as relative2, isAbsolute } from "path";
3509
4360
 
3510
4361
  // src/map/routes.ts
3511
4362
  var ROUTE_REGISTER_NAMES = ["get", "post", "put", "patch", "delete", "options", "all", "head", "use"];
@@ -3589,7 +4440,7 @@ function hasEntrySignal(text) {
3589
4440
  }
3590
4441
  function detectFramework(cwd) {
3591
4442
  try {
3592
- const pkg = JSON.parse(readFileSync8(join12(cwd, "package.json"), "utf8"));
4443
+ const pkg = JSON.parse(readFileSync8(join13(cwd, "package.json"), "utf8"));
3593
4444
  const d = { ...pkg.dependencies, ...pkg.devDependencies };
3594
4445
  if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
3595
4446
  if (d["next"]) return "next";
@@ -3601,11 +4452,11 @@ function detectFramework(cwd) {
3601
4452
  } catch {
3602
4453
  }
3603
4454
  try {
3604
- if (statSync3(join12(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
4455
+ if (statSync3(join13(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
3605
4456
  } catch {
3606
4457
  }
3607
4458
  try {
3608
- if (statSync3(join12(cwd, "functions")).isDirectory()) return "deno-functions";
4459
+ if (statSync3(join13(cwd, "functions")).isDirectory()) return "deno-functions";
3609
4460
  } catch {
3610
4461
  }
3611
4462
  return "unknown";
@@ -3658,7 +4509,7 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
3658
4509
  entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
3659
4510
  for (const e of entries) {
3660
4511
  if (SKIP_DIRS2.has(e.name) || e.name.startsWith(".") && e.name !== ".") continue;
3661
- const full = join12(dir, e.name);
4512
+ const full = join13(dir, e.name);
3662
4513
  if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats);
3663
4514
  else if (e.isSymbolicLink()) {
3664
4515
  let st, real;
@@ -3728,7 +4579,7 @@ function detectDeploymentShapes(cwd, opts = {}) {
3728
4579
  const found = [];
3729
4580
  for (const candidate of DEPLOYMENT_SHAPES) {
3730
4581
  for (const file of candidate.files ?? []) {
3731
- const full = join12(cwd, file);
4582
+ const full = join13(cwd, file);
3732
4583
  try {
3733
4584
  if (statSync3(full).isFile() && inProject(full)) {
3734
4585
  found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
@@ -3738,7 +4589,7 @@ function detectDeploymentShapes(cwd, opts = {}) {
3738
4589
  }
3739
4590
  }
3740
4591
  for (const dir of candidate.dirs ?? []) {
3741
- const full = join12(cwd, dir);
4592
+ const full = join13(cwd, dir);
3742
4593
  try {
3743
4594
  if (!statSync3(full).isDirectory() || !inProject(full)) continue;
3744
4595
  } catch {
@@ -3757,7 +4608,7 @@ function holdsFunctionEntry(dir) {
3757
4608
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
3758
4609
  if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
3759
4610
  try {
3760
- for (const nested of readdirSync5(join12(dir, entry.name), { withFileTypes: true })) {
4611
+ for (const nested of readdirSync5(join13(dir, entry.name), { withFileTypes: true })) {
3761
4612
  if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
3762
4613
  }
3763
4614
  } catch {
@@ -3773,7 +4624,7 @@ function holdsSourceFile(dir) {
3773
4624
  if (entry.isFile() && isSourceFile(entry.name)) return true;
3774
4625
  if (entry.isDirectory()) {
3775
4626
  try {
3776
- for (const nested of readdirSync5(join12(dir, entry.name), { withFileTypes: true })) {
4627
+ for (const nested of readdirSync5(join13(dir, entry.name), { withFileTypes: true })) {
3777
4628
  if (nested.isFile() && isSourceFile(nested.name)) return true;
3778
4629
  }
3779
4630
  } catch {
@@ -3787,7 +4638,7 @@ function holdsSourceFile(dir) {
3787
4638
 
3788
4639
  // src/map/surface.ts
3789
4640
  import { readFileSync as readFileSync9 } from "fs";
3790
- import { join as join13 } from "path";
4641
+ import { join as join14 } from "path";
3791
4642
  var STATIC_GENERATORS = [
3792
4643
  { dep: "vite", label: "vite" },
3793
4644
  { dep: "react-scripts", label: "create-react-app" },
@@ -3825,7 +4676,7 @@ var SHAPE_SIGNAL = {
3825
4676
  };
3826
4677
  function readManifest(cwd) {
3827
4678
  try {
3828
- const parsed = JSON.parse(readFileSync9(join13(cwd, "package.json"), "utf8"));
4679
+ const parsed = JSON.parse(readFileSync9(join14(cwd, "package.json"), "utf8"));
3829
4680
  return typeof parsed === "object" && parsed !== null ? parsed : null;
3830
4681
  } catch {
3831
4682
  return null;
@@ -3838,7 +4689,7 @@ function nextExportsStatically(cwd, manifest, ts) {
3838
4689
  for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
3839
4690
  let text;
3840
4691
  try {
3841
- text = readFileSync9(join13(cwd, file), "utf8");
4692
+ text = readFileSync9(join14(cwd, file), "utf8");
3842
4693
  } catch {
3843
4694
  continue;
3844
4695
  }
@@ -4239,7 +5090,7 @@ function dedupeSinks(sinks, owner) {
4239
5090
 
4240
5091
  // src/map/module-graph.ts
4241
5092
  import { readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
4242
- import { dirname as dirname4, join as join14, relative as relative3, resolve as resolvePath } from "path";
5093
+ import { dirname as dirname7, join as join15, relative as relative3, resolve as resolvePath } from "path";
4243
5094
  var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
4244
5095
  function createModuleGraph(ts, opts) {
4245
5096
  const cache = /* @__PURE__ */ new Map();
@@ -4307,7 +5158,7 @@ function createModuleGraph(ts, opts) {
4307
5158
  }
4308
5159
  function resolveRelativeModule(fromFile, spec) {
4309
5160
  if (!spec.startsWith(".")) return void 0;
4310
- const dir = dirname4(fromFile);
5161
+ const dir = dirname7(fromFile);
4311
5162
  const base = resolvePath(dir, spec);
4312
5163
  const candidates = [];
4313
5164
  const jsLike = /\.(js|jsx|mjs|cjs)$/.exec(base);
@@ -4317,7 +5168,7 @@ function resolveRelativeModule(fromFile, spec) {
4317
5168
  }
4318
5169
  candidates.push(base);
4319
5170
  for (const e of RESOLVE_EXTS) candidates.push(base + e);
4320
- for (const e of RESOLVE_EXTS) candidates.push(join14(base, "index" + e));
5171
+ for (const e of RESOLVE_EXTS) candidates.push(join15(base, "index" + e));
4321
5172
  for (const c of candidates) {
4322
5173
  try {
4323
5174
  if (statSync4(c).isFile()) return c;
@@ -4990,7 +5841,7 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
4990
5841
 
4991
5842
  // src/map/imports.ts
4992
5843
  import { readFileSync as readFileSync11 } from "fs";
4993
- import { join as join15 } from "path";
5844
+ import { join as join16 } from "path";
4994
5845
  var MAX_SITES = 5;
4995
5846
  var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
4996
5847
  function isNpmPackageName(pkg) {
@@ -5001,7 +5852,7 @@ function readPathAliases(cwd) {
5001
5852
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
5002
5853
  let paths;
5003
5854
  try {
5004
- const raw = readFileSync11(join15(cwd, name), "utf8");
5855
+ const raw = readFileSync11(join16(cwd, name), "utf8");
5005
5856
  const parsed = JSON.parse(stripJsonComments(raw));
5006
5857
  paths = parsed.compilerOptions?.paths;
5007
5858
  } catch {
@@ -5714,7 +6565,9 @@ Usage:
5714
6565
  print the full setup guide. --full prints the
5715
6566
  guide even when setup is complete
5716
6567
  patchstack-connect login [--wait] Recover this site's credential when
5717
- .patchstackrc.json has been lost. Prints a link
6568
+ .patchstackrc.local.json has been lost. Needs the
6569
+ site UUID, which lives in the committed
6570
+ .patchstackrc.json. Prints a link
5718
6571
  for the site's OWNER to approve in the dashboard.
5719
6572
  In a terminal it then waits. When the output is
5720
6573
  piped or captured \u2014 an assistant running it \u2014 it
@@ -5755,7 +6608,8 @@ Environment:
5755
6608
  PATCHSTACK_MODE (protect) Runtime guard mode: block (default) | dry-run
5756
6609
  PATCHSTACK_ROUTE_WAF (protect) Set to 1 to also screen every request at the route level (opt-in)
5757
6610
 
5758
- Precedence: CLI flag > environment variable > .patchstackrc.json.
6611
+ Precedence, site UUID and settings: CLI flag > environment variable > .patchstackrc.json (committed).
6612
+ Precedence, API key: environment variable > .patchstackrc.local.json (git-ignored) > .patchstackrc.json.
5759
6613
 
5760
6614
  Examples:
5761
6615
  npx @patchstack/connect setup
@@ -5828,8 +6682,13 @@ async function runLogin(args) {
5828
6682
  cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
5829
6683
  cliEndpoint: getStringFlag(args.flags, "endpoint")
5830
6684
  });
5831
- const approved = () => {
5832
- console.log("\n \u2713 Credential restored and saved to .patchstackrc.json.");
6685
+ const approved = async () => {
6686
+ const ignore = await secretFileIgnored(process.cwd());
6687
+ console.log(`
6688
+ \u2713 Credential restored and saved to ${SECRET_CONFIG_FILENAME}.`);
6689
+ console.log(
6690
+ ignore.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${ignore.reason ?? "unknown reason"}. Add it to .gitignore yourself before committing.`
6691
+ );
5833
6692
  console.log(" The previous credential no longer works. Update it anywhere else it was set:");
5834
6693
  console.log(" CI secrets, hosting env vars, preview environments, other checkouts.\n");
5835
6694
  return 0;
@@ -5841,7 +6700,7 @@ async function runLogin(args) {
5841
6700
  return 1;
5842
6701
  }
5843
6702
  const resumed = await waitForApproval(config, pending);
5844
- if (resumed.status === "approved") return approved();
6703
+ if (resumed.status === "approved") return await approved();
5845
6704
  console.error(`
5846
6705
  ${resumed.message ?? "Login failed."}
5847
6706
  `);
@@ -5860,7 +6719,7 @@ async function runLogin(args) {
5860
6719
  const existing = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
5861
6720
  if (existing !== null && Date.now() < existing.expiresAt) {
5862
6721
  const outcome = await redeemIfApproved(config, existing);
5863
- if (outcome === "approved") return approved();
6722
+ if (outcome === "approved") return await approved();
5864
6723
  if (outcome === "pending") {
5865
6724
  const secondsLeft = Math.round((existing.expiresAt - Date.now()) / 1e3);
5866
6725
  console.log(`
@@ -5887,7 +6746,7 @@ async function runLogin(args) {
5887
6746
  prompt(userCode, verificationUri);
5888
6747
  console.log(" Waiting for approval (the code expires in 10 minutes)\u2026");
5889
6748
  });
5890
- if (result.status === "approved") return approved();
6749
+ if (result.status === "approved") return await approved();
5891
6750
  console.error(`
5892
6751
  ${result.message ?? "Login failed."}
5893
6752
  `);
@@ -6022,8 +6881,17 @@ async function runScan(args, options = {}) {
6022
6881
  console.log(`Provisioned site ${response.uuid}. Saved UUID to ${target}.`);
6023
6882
  }
6024
6883
  if (typeof response.api_key === "string" && response.api_key.length > 0) {
6025
- const target = await persistApiKey(process.cwd(), response.api_key);
6026
- console.log(`Saved API key to ${target} (authenticates Pulse ingest and block-log reporting; keep out of the public widget).`);
6884
+ const hadCredentialInConfig = await credentialInCommittedConfig(process.cwd());
6885
+ const saved = await persistApiKey(process.cwd(), response.api_key);
6886
+ console.log(`Saved API key to ${saved.path}. Do not commit it.`);
6887
+ console.log(
6888
+ saved.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${saved.reason ?? "unknown reason"}. Add \`${SECRET_CONFIG_FILENAME}\` to .gitignore yourself before committing.`
6889
+ );
6890
+ if (hadCredentialInConfig) {
6891
+ console.log(
6892
+ "A credential was also present in .patchstackrc.json and has been removed from it. If that file was ever committed, rotate the credential from the dashboard."
6893
+ );
6894
+ }
6027
6895
  }
6028
6896
  if (response.stored) {
6029
6897
  console.log(`Stored manifest #${response.manifest_id} (checksum ${response.checksum}).`);
@@ -6134,9 +7002,14 @@ async function runProtectCommand(args) {
6134
7002
  const report = runVerify(process.cwd());
6135
7003
  console.log(`patchstack protect --check (${report.stack}):`);
6136
7004
  for (const c of report.checks) {
6137
- console.log(` ${c.ok ? "\u2713" : "\u2717"} ${c.label}${!c.ok && c.hint ? ` \u2014 ${c.hint}` : ""}`);
7005
+ const mark = c.unverifiable ? "?" : c.ok ? "\u2713" : "\u2717";
7006
+ const note = c.unverifiable || !c.ok ? c.hint : void 0;
7007
+ console.log(` ${mark} ${c.label}${note ? ` \u2014 ${note}` : ""}`);
6138
7008
  }
6139
7009
  console.log(report.wired ? "guard is wired \u2713" : "guard is NOT fully wired \u2717");
7010
+ if (report.checks.some((c) => c.unverifiable)) {
7011
+ console.log("One or more checks could not be answered from here \u2014 see the `?` lines above.");
7012
+ }
6140
7013
  return report.wired ? 0 : 1;
6141
7014
  }
6142
7015
  const demo = args.flags.get("demo") === true;