@patchstack/connect 0.4.1 → 0.5.0

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 (34) hide show
  1. package/AGENT-INSTALL.md +21 -4
  2. package/README.md +10 -2
  3. package/dist/cli.js +277 -56
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.cjs +221 -18
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +37 -0
  8. package/dist/index.d.ts +37 -0
  9. package/dist/index.js +218 -15
  10. package/dist/index.js.map +1 -1
  11. package/dist/protect/templates/astro-middleware.ts +22 -1
  12. package/dist/protect/templates/express-guard.cjs +54 -4
  13. package/dist/protect/templates/express-guard.js +54 -4
  14. package/dist/protect/templates/express-guard.ts +43 -5
  15. package/dist/protect/templates/fastify-plugin.cjs +41 -3
  16. package/dist/protect/templates/fastify-plugin.js +41 -3
  17. package/dist/protect/templates/fastify-plugin.ts +30 -2
  18. package/dist/protect/templates/generic-guard.cjs +58 -5
  19. package/dist/protect/templates/generic-guard.js +58 -5
  20. package/dist/protect/templates/generic-guard.ts +47 -4
  21. package/dist/protect/templates/guard.ts +42 -8
  22. package/dist/protect/templates/next-middleware.ts +22 -1
  23. package/dist/protect/templates/nuxt-middleware.ts +22 -1
  24. package/dist/protect/templates/sveltekit-hooks.ts +22 -1
  25. package/dist/protect.cjs +341 -94
  26. package/dist/protect.cjs.map +1 -1
  27. package/dist/protect.edge.js +56 -24
  28. package/dist/protect.edge.js.map +1 -1
  29. package/dist/protect.js +57 -25
  30. package/dist/protect.js.map +1 -1
  31. package/dist/{refresh-manifest-SWCPQ52Z.js → refresh-manifest-LDS35UKV.js} +234 -33
  32. package/dist/refresh-manifest-LDS35UKV.js.map +1 -0
  33. package/package.json +4 -4
  34. package/dist/refresh-manifest-SWCPQ52Z.js.map +0 -1
package/dist/protect.cjs CHANGED
@@ -38,8 +38,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
38
38
  // src/pulse-token.ts
39
39
  function buildTokenUrl(manifestEndpoint) {
40
40
  const url = new URL(manifestEndpoint);
41
- const path7 = url.pathname.replace(/\/$/, "");
42
- url.pathname = path7.endsWith("/manifest") ? `${path7.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
41
+ const path8 = url.pathname.replace(/\/$/, "");
42
+ url.pathname = path8.endsWith("/manifest") ? `${path8.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
43
43
  url.search = "";
44
44
  url.hash = "";
45
45
  return url.toString();
@@ -159,6 +159,14 @@ function authFailureMessage(status, config) {
159
159
  }
160
160
  return null;
161
161
  }
162
+ function buildManifestBody(config, payload) {
163
+ return {
164
+ ...payload,
165
+ environment: config.environment,
166
+ ...typeof config.siteUrl === "string" && config.siteUrl !== "" ? { url: config.siteUrl } : {},
167
+ ...typeof config.siteName === "string" && config.siteName !== "" ? { name: config.siteName } : {}
168
+ };
169
+ }
162
170
  async function postManifest(config, payload) {
163
171
  const url = buildEndpointUrl(config.endpoint, config.siteUuid);
164
172
  const timeoutMs = config.timeoutMs;
@@ -171,7 +179,7 @@ async function postManifest(config, payload) {
171
179
  Accept: "application/json",
172
180
  "User-Agent": "@patchstack/connect"
173
181
  },
174
- body: JSON.stringify({ ...payload, environment: config.environment }),
182
+ body: JSON.stringify(buildManifestBody(config, payload)),
175
183
  signal: AbortSignal.timeout(timeoutMs)
176
184
  });
177
185
  } catch (cause) {
@@ -239,7 +247,207 @@ var init_client = __esm({
239
247
  }
240
248
  });
241
249
 
250
+ // src/site-url.ts
251
+ function isUnpublishableHost(hostname) {
252
+ const host = hostname.toLowerCase().replace(/\.+$/, "");
253
+ if (host === "") return true;
254
+ if (host.startsWith("[")) return true;
255
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
256
+ if (!host.includes(".")) return true;
257
+ return RESERVED_SUFFIXES.some((suffix) => host.endsWith(suffix));
258
+ }
259
+ function normaliseSiteUrl(value) {
260
+ const trimmed = (value ?? "").trim();
261
+ if (trimmed === "") return null;
262
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
263
+ let parsed;
264
+ try {
265
+ parsed = new URL(withScheme);
266
+ } catch {
267
+ return null;
268
+ }
269
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
270
+ if (parsed.hostname === "" || isUnpublishableHost(parsed.hostname)) return null;
271
+ const origin = `${parsed.protocol}//${parsed.host}`;
272
+ return origin.length <= 191 ? origin : null;
273
+ }
274
+ function detectSiteUrl(env = process.env) {
275
+ for (const source of PRODUCTION_URL_SOURCES) {
276
+ const url = normaliseSiteUrl(source.read(env));
277
+ if (url !== null) {
278
+ return { url, platform: source.platform };
279
+ }
280
+ }
281
+ return null;
282
+ }
283
+ var RESERVED_SUFFIXES, PRODUCTION_URL_SOURCES;
284
+ var init_site_url = __esm({
285
+ "src/site-url.ts"() {
286
+ "use strict";
287
+ RESERVED_SUFFIXES = [
288
+ ".local",
289
+ ".localhost",
290
+ ".localdomain",
291
+ ".internal",
292
+ ".intranet",
293
+ ".invalid",
294
+ ".test",
295
+ ".example",
296
+ ".home.arpa",
297
+ ".in-addr.arpa",
298
+ ".ip6.arpa"
299
+ ];
300
+ PRODUCTION_URL_SOURCES = [
301
+ {
302
+ // VERCEL_URL is per-deployment and changes every build, so it is deliberately not read here.
303
+ platform: "vercel",
304
+ read: (env) => env.VERCEL_ENV === "production" ? env.VERCEL_PROJECT_PRODUCTION_URL : void 0
305
+ },
306
+ {
307
+ // `URL` is generic enough to belong to anything, so it counts only alongside Netlify's own markers.
308
+ platform: "netlify",
309
+ read: (env) => env.NETLIFY === "true" && env.CONTEXT === "production" ? env.URL : void 0
310
+ },
311
+ {
312
+ platform: "render",
313
+ read: (env) => env.RENDER === "true" && env.IS_PULL_REQUEST !== "true" ? env.RENDER_EXTERNAL_URL : void 0
314
+ },
315
+ {
316
+ platform: "railway",
317
+ read: (env) => env.RAILWAY_ENVIRONMENT_NAME === "production" ? env.RAILWAY_PUBLIC_DOMAIN : void 0
318
+ }
319
+ ];
320
+ }
321
+ });
322
+
323
+ // src/site-name.ts
324
+ function normaliseSiteName(value) {
325
+ const collapsed = (value ?? "").replace(/\s+/g, " ").trim();
326
+ if (collapsed === "") return null;
327
+ return collapsed.slice(0, NAME_MAX_LENGTH);
328
+ }
329
+ function decodeEntities(text) {
330
+ return text.replace(/&([a-z]+);/gi, (whole, name) => NAMED_ENTITIES2[name.toLowerCase()] ?? whole);
331
+ }
332
+ function nameFromHtmlTitle(html) {
333
+ const match = /<title\b[^>]*>([\s\S]*?)<\/title>/i.exec(html);
334
+ if (match === null) return null;
335
+ const name = normaliseSiteName(decodeEntities(match[1] ?? ""));
336
+ if (name === null || TEMPLATE_TITLES.has(name.toLowerCase())) return null;
337
+ return name;
338
+ }
339
+ function nameFromPackageName(raw) {
340
+ if (typeof raw !== "string") return null;
341
+ const unscoped = raw.trim().replace(/^@[^/]+\//, "");
342
+ if (unscoped === "" || TEMPLATE_PACKAGE_NAMES.has(unscoped.toLowerCase())) return null;
343
+ const words = unscoped.split(/[-_.\s]+/).filter((word) => word !== "").map((word) => word === word.toUpperCase() ? word : word.charAt(0).toUpperCase() + word.slice(1));
344
+ return normaliseSiteName(words.join(" "));
345
+ }
346
+ async function readIfPresent(file) {
347
+ try {
348
+ return await (0, import_promises.readFile)(file, "utf8");
349
+ } catch {
350
+ return null;
351
+ }
352
+ }
353
+ async function detectSiteName(cwd) {
354
+ for (const shell of ["index.html", import_node_path.default.join("public", "index.html")]) {
355
+ const html = await readIfPresent(import_node_path.default.join(cwd, shell));
356
+ if (html === null) continue;
357
+ const name = nameFromHtmlTitle(html);
358
+ if (name !== null) return { name, source: "index.html" };
359
+ }
360
+ const manifest = await readIfPresent(import_node_path.default.join(cwd, "package.json"));
361
+ if (manifest !== null) {
362
+ try {
363
+ const parsed = JSON.parse(manifest);
364
+ const name = nameFromPackageName(
365
+ typeof parsed === "object" && parsed !== null ? parsed.name : void 0
366
+ );
367
+ if (name !== null) return { name, source: "package.json" };
368
+ } catch {
369
+ }
370
+ }
371
+ return null;
372
+ }
373
+ var import_promises, import_node_path, NAME_MAX_LENGTH, TEMPLATE_PACKAGE_NAMES, TEMPLATE_TITLES, NAMED_ENTITIES2;
374
+ var init_site_name = __esm({
375
+ "src/site-name.ts"() {
376
+ "use strict";
377
+ import_promises = require("fs/promises");
378
+ import_node_path = __toESM(require("path"), 1);
379
+ NAME_MAX_LENGTH = 191;
380
+ TEMPLATE_PACKAGE_NAMES = /* @__PURE__ */ new Set([
381
+ // Lovable
382
+ "vite_react_shadcn_ts",
383
+ // Bolt
384
+ "vite-react-typescript-starter",
385
+ // Scaffolders and the names people accept from them
386
+ "vite-project",
387
+ "my-app",
388
+ "my-project",
389
+ "my-v0-project",
390
+ "app",
391
+ "frontend",
392
+ "web",
393
+ "client",
394
+ "project",
395
+ "template",
396
+ "example",
397
+ "react-app",
398
+ "next-app",
399
+ "nextjs",
400
+ "starter",
401
+ "test"
402
+ ]);
403
+ TEMPLATE_TITLES = /* @__PURE__ */ new Set([
404
+ "vite + react",
405
+ "vite + react + ts",
406
+ "vite + vue",
407
+ "vite + vue + ts",
408
+ "vite app",
409
+ "react app",
410
+ "create next app",
411
+ "document",
412
+ "untitled",
413
+ "index",
414
+ "home",
415
+ "app"
416
+ ]);
417
+ NAMED_ENTITIES2 = {
418
+ amp: "&",
419
+ lt: "<",
420
+ gt: ">",
421
+ quot: '"',
422
+ apos: "'",
423
+ nbsp: " "
424
+ };
425
+ }
426
+ });
427
+
242
428
  // src/config.ts
429
+ async function resolveSiteIdentity(cwd, fromEnv, fromFile) {
430
+ const configuredUrl = stated(fromEnv.url) ?? stated(fromFile.url);
431
+ const configuredName = stated(fromEnv.name) ?? stated(fromFile.name);
432
+ let url;
433
+ if (configuredUrl === null) {
434
+ url = detectSiteUrl(process.env)?.url ?? null;
435
+ } else {
436
+ url = normaliseSiteUrl(configuredUrl);
437
+ if (url === null) {
438
+ throw new PatchstackError(
439
+ `Site URL "${configuredUrl}" is not an address the public can reach this site at. Set "url" in .patchstackrc.json (or PATCHSTACK_SITE_URL) to the http(s) address visitors use \u2014 a hostname, not an IP address \u2014 or remove it to let the build environment answer.`,
440
+ "CONFIG_INVALID"
441
+ );
442
+ }
443
+ }
444
+ const name = configuredName === null ? (await detectSiteName(cwd))?.name ?? null : normaliseSiteName(configuredName);
445
+ return { url, name };
446
+ }
447
+ function stated(value) {
448
+ const trimmed = (value ?? "").trim();
449
+ return trimmed === "" ? null : trimmed;
450
+ }
243
451
  async function resolveConfig(options) {
244
452
  const fromFile = await readConfigFile(options.cwd);
245
453
  const fromSecretFile = await readSecretFile(options.cwd);
@@ -269,10 +477,13 @@ async function resolveConfig(options) {
269
477
  }
270
478
  const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
271
479
  const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
480
+ const identity = options.detectSiteIdentity === true ? await resolveSiteIdentity(options.cwd, fromEnv, fromFile) : { url: null, name: null };
272
481
  return {
273
482
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
274
483
  apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
275
484
  pulseAuth: pulseAuthRaw === null || pulseAuthRaw.length === 0 ? null : pulseAuthRaw,
485
+ siteUrl: identity.url,
486
+ siteName: identity.name,
276
487
  endpoint,
277
488
  timeoutMs,
278
489
  environment,
@@ -287,10 +498,10 @@ async function readConfigFile(cwd) {
287
498
  return readJsonFile(cwd, CONFIG_FILENAME);
288
499
  }
289
500
  async function readJsonFile(cwd, filename) {
290
- const target = import_node_path.default.join(cwd, filename);
501
+ const target = import_node_path2.default.join(cwd, filename);
291
502
  let raw;
292
503
  try {
293
- raw = await (0, import_promises.readFile)(target, "utf8");
504
+ raw = await (0, import_promises2.readFile)(target, "utf8");
294
505
  } catch (err) {
295
506
  if (err.code === "ENOENT") {
296
507
  return {};
@@ -327,6 +538,8 @@ function readEnv() {
327
538
  const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
328
539
  return {
329
540
  siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
541
+ url: process.env.PATCHSTACK_SITE_URL ?? void 0,
542
+ name: process.env.PATCHSTACK_SITE_NAME ?? void 0,
330
543
  apiKey: process.env.PATCHSTACK_API_KEY ?? void 0,
331
544
  pulseAuth: process.env.PATCHSTACK_PULSE_AUTH ?? void 0,
332
545
  endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
@@ -340,14 +553,16 @@ function isUuid(value) {
340
553
  function isEnvironment(value) {
341
554
  return value === "production" || value === "sandbox";
342
555
  }
343
- var import_promises, import_node_path, CONFIG_FILENAME, SECRET_FILENAME, DEFAULT_ENVIRONMENT;
556
+ var import_promises2, import_node_path2, CONFIG_FILENAME, SECRET_FILENAME, DEFAULT_ENVIRONMENT;
344
557
  var init_config = __esm({
345
558
  "src/config.ts"() {
346
559
  "use strict";
347
- import_promises = require("fs/promises");
348
- import_node_path = __toESM(require("path"), 1);
560
+ import_promises2 = require("fs/promises");
561
+ import_node_path2 = __toESM(require("path"), 1);
349
562
  init_types();
350
563
  init_client();
564
+ init_site_url();
565
+ init_site_name();
351
566
  CONFIG_FILENAME = ".patchstackrc.json";
352
567
  SECRET_FILENAME = ".patchstackrc.local.json";
353
568
  DEFAULT_ENVIRONMENT = "production";
@@ -358,7 +573,7 @@ var init_config = __esm({
358
573
  async function readDeclaredDependencyNames(cwd) {
359
574
  let raw;
360
575
  try {
361
- raw = await (0, import_promises2.readFile)(import_node_path2.default.join(cwd, "package.json"), "utf8");
576
+ raw = await (0, import_promises3.readFile)(import_node_path3.default.join(cwd, "package.json"), "utf8");
362
577
  } catch {
363
578
  return [];
364
579
  }
@@ -407,12 +622,12 @@ function disagreements(packages, other) {
407
622
  }
408
623
  return conflicts;
409
624
  }
410
- var import_promises2, import_node_path2;
625
+ var import_promises3, import_node_path3;
411
626
  var init_consistency = __esm({
412
627
  "src/parsers/consistency.ts"() {
413
628
  "use strict";
414
- import_promises2 = require("fs/promises");
415
- import_node_path2 = __toESM(require("path"), 1);
629
+ import_promises3 = require("fs/promises");
630
+ import_node_path3 = __toESM(require("path"), 1);
416
631
  }
417
632
  });
418
633
 
@@ -443,7 +658,7 @@ var init_report = __esm({
443
658
  async function parseBunLockfile(lockfilePath, report) {
444
659
  let raw;
445
660
  try {
446
- raw = await (0, import_promises3.readFile)(lockfilePath, "utf8");
661
+ raw = await (0, import_promises4.readFile)(lockfilePath, "utf8");
447
662
  } catch (cause) {
448
663
  throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
449
664
  }
@@ -557,11 +772,11 @@ function stripTrailingCommas(source) {
557
772
  }
558
773
  return out.join("");
559
774
  }
560
- var import_promises3, NON_REGISTRY_VERSION;
775
+ var import_promises4, NON_REGISTRY_VERSION;
561
776
  var init_bun = __esm({
562
777
  "src/parsers/bun.ts"() {
563
778
  "use strict";
564
- import_promises3 = require("fs/promises");
779
+ import_promises4 = require("fs/promises");
565
780
  init_types();
566
781
  init_report();
567
782
  NON_REGISTRY_VERSION = /^[a-z][a-z0-9+.-]*:|\//i;
@@ -572,7 +787,7 @@ var init_bun = __esm({
572
787
  async function parseNpmLockfile(lockfilePath) {
573
788
  let raw;
574
789
  try {
575
- raw = await (0, import_promises4.readFile)(lockfilePath, "utf8");
790
+ raw = await (0, import_promises5.readFile)(lockfilePath, "utf8");
576
791
  } catch (cause) {
577
792
  throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
578
793
  }
@@ -630,7 +845,7 @@ function extractFromV1(deps, acc = [], depth = 0) {
630
845
  return acc;
631
846
  }
632
847
  function extractNameFromPath(pkgPath) {
633
- const segments = pkgPath.split("node_modules" + import_node_path3.default.sep === pkgPath ? import_node_path3.default.sep : "/");
848
+ const segments = pkgPath.split("node_modules" + import_node_path4.default.sep === pkgPath ? import_node_path4.default.sep : "/");
634
849
  const parts = pkgPath.split("/");
635
850
  const nmIndex = parts.lastIndexOf("node_modules");
636
851
  if (nmIndex === -1 || nmIndex >= parts.length - 1) {
@@ -651,21 +866,21 @@ function isDirectV2(pkgPath) {
651
866
  const nmCount = parts.filter((p) => p === "node_modules").length;
652
867
  return nmCount === 1;
653
868
  }
654
- var import_promises4, import_node_path3;
869
+ var import_promises5, import_node_path4;
655
870
  var init_npm = __esm({
656
871
  "src/parsers/npm.ts"() {
657
872
  "use strict";
658
- import_promises4 = require("fs/promises");
659
- import_node_path3 = __toESM(require("path"), 1);
873
+ import_promises5 = require("fs/promises");
874
+ import_node_path4 = __toESM(require("path"), 1);
660
875
  init_types();
661
876
  }
662
877
  });
663
878
 
664
879
  // src/parsers/node_modules.ts
665
880
  async function walkNodeModules(cwd) {
666
- const root = import_node_path4.default.join(cwd, "node_modules");
881
+ const root = import_node_path5.default.join(cwd, "node_modules");
667
882
  try {
668
- const info = await (0, import_promises5.stat)(root);
883
+ const info = await (0, import_promises6.stat)(root);
669
884
  if (!info.isDirectory()) {
670
885
  throw new PatchstackError(
671
886
  `${root} exists but is not a directory.`,
@@ -684,7 +899,7 @@ async function walkNodeModules(cwd) {
684
899
  }
685
900
  const seen = /* @__PURE__ */ new Map();
686
901
  try {
687
- seen.set(await (0, import_promises5.realpath)(cwd), -1);
902
+ seen.set(await (0, import_promises6.realpath)(cwd), -1);
688
903
  } catch {
689
904
  }
690
905
  return dedupeByShallowest(await walk(root, 0, seen, cwd));
@@ -700,26 +915,26 @@ function dedupeByShallowest(found) {
700
915
  async function walk(dir, depth, seen, root) {
701
916
  let names;
702
917
  try {
703
- names = await (0, import_promises5.readdir)(dir);
918
+ names = await (0, import_promises6.readdir)(dir);
704
919
  } catch {
705
920
  return [];
706
921
  }
707
922
  const perEntry = await mapLimit(names, async (name) => {
708
923
  if (name.startsWith(".") && !PACKAGE_STORE_DIRS.has(name)) return [];
709
- const fullPath = import_node_path4.default.join(dir, name);
924
+ const fullPath = import_node_path5.default.join(dir, name);
710
925
  const resolved = await claim(fullPath, seen, depth);
711
926
  if (resolved === null) return [];
712
927
  if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen, root);
713
928
  if (name.startsWith("@")) {
714
929
  let subNames;
715
930
  try {
716
- subNames = await (0, import_promises5.readdir)(fullPath);
931
+ subNames = await (0, import_promises6.readdir)(fullPath);
717
932
  } catch {
718
933
  return [];
719
934
  }
720
935
  const scoped = await mapLimit(subNames, async (sub) => {
721
936
  if (sub.startsWith(".")) return [];
722
- const scopedDir = import_node_path4.default.join(fullPath, sub);
937
+ const scopedDir = import_node_path5.default.join(fullPath, sub);
723
938
  const scopedReal = await claim(scopedDir, seen, depth);
724
939
  if (scopedReal === null) return [];
725
940
  return packageAndNested(scopedDir, scopedReal, depth, seen, root);
@@ -746,13 +961,13 @@ async function mapLimit(items, fn) {
746
961
  await Promise.all(workers);
747
962
  return results;
748
963
  }
749
- function installLocation(root, pkgDir, impl = import_node_path4.default) {
964
+ function installLocation(root, pkgDir, impl = import_node_path5.default) {
750
965
  return impl.relative(root, pkgDir).split(impl.sep).join("/");
751
966
  }
752
967
  async function readPackage(pkgDir, depth, root) {
753
968
  let raw;
754
969
  try {
755
- raw = await (0, import_promises5.readFile)(import_node_path4.default.join(pkgDir, "package.json"), "utf8");
970
+ raw = await (0, import_promises6.readFile)(import_node_path5.default.join(pkgDir, "package.json"), "utf8");
756
971
  } catch {
757
972
  return null;
758
973
  }
@@ -771,7 +986,7 @@ async function readPackage(pkgDir, depth, root) {
771
986
  return { name: parsed.name, version: parsed.version, path: installLocation(root, pkgDir), direct: depth === 0 };
772
987
  }
773
988
  async function walkNested(pkgDir, depth, seen, root) {
774
- const nested = import_node_path4.default.join(pkgDir, "node_modules");
989
+ const nested = import_node_path5.default.join(pkgDir, "node_modules");
775
990
  if (await claim(nested, seen, depth + 1) === null) {
776
991
  return [];
777
992
  }
@@ -780,10 +995,10 @@ async function walkNested(pkgDir, depth, seen, root) {
780
995
  async function claim(dir, seen, depth) {
781
996
  let resolved;
782
997
  try {
783
- const info = await (0, import_promises5.lstat)(dir);
998
+ const info = await (0, import_promises6.lstat)(dir);
784
999
  if (!info.isDirectory() && !info.isSymbolicLink()) return null;
785
- resolved = await (0, import_promises5.realpath)(dir);
786
- const target = await (0, import_promises5.stat)(resolved);
1000
+ resolved = await (0, import_promises6.realpath)(dir);
1001
+ const target = await (0, import_promises6.stat)(resolved);
787
1002
  if (!target.isDirectory()) return null;
788
1003
  } catch {
789
1004
  return null;
@@ -793,12 +1008,12 @@ async function claim(dir, seen, depth) {
793
1008
  seen.set(resolved, depth);
794
1009
  return resolved;
795
1010
  }
796
- var import_promises5, import_node_path4, PACKAGE_STORE_DIRS, CONCURRENCY;
1011
+ var import_promises6, import_node_path5, PACKAGE_STORE_DIRS, CONCURRENCY;
797
1012
  var init_node_modules = __esm({
798
1013
  "src/parsers/node_modules.ts"() {
799
1014
  "use strict";
800
- import_promises5 = require("fs/promises");
801
- import_node_path4 = __toESM(require("path"), 1);
1015
+ import_promises6 = require("fs/promises");
1016
+ import_node_path5 = __toESM(require("path"), 1);
802
1017
  init_types();
803
1018
  PACKAGE_STORE_DIRS = /* @__PURE__ */ new Set([".bun", ".pnpm", ".store"]);
804
1019
  CONCURRENCY = 32;
@@ -809,7 +1024,7 @@ var init_node_modules = __esm({
809
1024
  async function parsePnpmLockfile(lockfilePath, report) {
810
1025
  let raw;
811
1026
  try {
812
- raw = await (0, import_promises6.readFile)(lockfilePath, "utf8");
1027
+ raw = await (0, import_promises7.readFile)(lockfilePath, "utf8");
813
1028
  } catch (cause) {
814
1029
  throw new PatchstackError(
815
1030
  `Could not read lockfile at ${lockfilePath}`,
@@ -1051,11 +1266,11 @@ function extractLeafName(trimmed) {
1051
1266
  }
1052
1267
  return name.length > 0 ? name : null;
1053
1268
  }
1054
- var import_promises6, DEP_SECTIONS;
1269
+ var import_promises7, DEP_SECTIONS;
1055
1270
  var init_pnpm = __esm({
1056
1271
  "src/parsers/pnpm.ts"() {
1057
1272
  "use strict";
1058
- import_promises6 = require("fs/promises");
1273
+ import_promises7 = require("fs/promises");
1059
1274
  init_types();
1060
1275
  init_report();
1061
1276
  DEP_SECTIONS = /* @__PURE__ */ new Set([
@@ -1070,7 +1285,7 @@ var init_pnpm = __esm({
1070
1285
  async function parseYarnLockfile(lockfilePath, report) {
1071
1286
  let raw;
1072
1287
  try {
1073
- raw = await (0, import_promises7.readFile)(lockfilePath, "utf8");
1288
+ raw = await (0, import_promises8.readFile)(lockfilePath, "utf8");
1074
1289
  } catch (cause) {
1075
1290
  throw new PatchstackError(
1076
1291
  `Could not read lockfile at ${lockfilePath}`,
@@ -1085,7 +1300,7 @@ async function parseYarnLockfile(lockfilePath, report) {
1085
1300
  "LOCKFILE_PARSE_ERROR"
1086
1301
  );
1087
1302
  }
1088
- const directNames = await readDirectDepNames(import_node_path5.default.dirname(lockfilePath));
1303
+ const directNames = await readDirectDepNames(import_node_path6.default.dirname(lockfilePath));
1089
1304
  const entries = [];
1090
1305
  const seen = /* @__PURE__ */ new Set();
1091
1306
  for (const block of blocks) {
@@ -1250,7 +1465,7 @@ async function readDirectDepNames(cwd) {
1250
1465
  const names = /* @__PURE__ */ new Set();
1251
1466
  let raw;
1252
1467
  try {
1253
- raw = await (0, import_promises7.readFile)(import_node_path5.default.join(cwd, "package.json"), "utf8");
1468
+ raw = await (0, import_promises8.readFile)(import_node_path6.default.join(cwd, "package.json"), "utf8");
1254
1469
  } catch {
1255
1470
  return names;
1256
1471
  }
@@ -1275,12 +1490,12 @@ async function readDirectDepNames(cwd) {
1275
1490
  }
1276
1491
  return names;
1277
1492
  }
1278
- var import_promises7, import_node_path5, NON_PACKAGE_KEYS, NON_REGISTRY_PROTOCOLS;
1493
+ var import_promises8, import_node_path6, NON_PACKAGE_KEYS, NON_REGISTRY_PROTOCOLS;
1279
1494
  var init_yarn = __esm({
1280
1495
  "src/parsers/yarn.ts"() {
1281
1496
  "use strict";
1282
- import_promises7 = require("fs/promises");
1283
- import_node_path5 = __toESM(require("path"), 1);
1497
+ import_promises8 = require("fs/promises");
1498
+ import_node_path6 = __toESM(require("path"), 1);
1284
1499
  init_types();
1285
1500
  init_report();
1286
1501
  NON_PACKAGE_KEYS = /* @__PURE__ */ new Set(["__metadata"]);
@@ -1292,7 +1507,7 @@ var init_yarn = __esm({
1292
1507
  async function detectLockfile(cwd) {
1293
1508
  const probed = await Promise.all(
1294
1509
  LOCKFILE_CANDIDATES.map(async (candidate) => {
1295
- const filePath = import_node_path6.default.join(cwd, candidate.filename);
1510
+ const filePath = import_node_path7.default.join(cwd, candidate.filename);
1296
1511
  return { ...candidate, filePath, present: await exists(filePath) };
1297
1512
  })
1298
1513
  );
@@ -1403,7 +1618,7 @@ function staleWarning(source, missing) {
1403
1618
  async function presentLockfiles(cwd) {
1404
1619
  const probed = await Promise.all(
1405
1620
  LOCKFILE_CANDIDATES.map(async (candidate) => {
1406
- const filePath = import_node_path6.default.join(cwd, candidate.filename);
1621
+ const filePath = import_node_path7.default.join(cwd, candidate.filename);
1407
1622
  return { ...candidate, filePath, present: await exists(filePath) };
1408
1623
  })
1409
1624
  );
@@ -1427,18 +1642,18 @@ async function runStrategy(detected, cwd, report) {
1427
1642
  }
1428
1643
  async function exists(filePath) {
1429
1644
  try {
1430
- await (0, import_promises8.access)(filePath);
1645
+ await (0, import_promises9.access)(filePath);
1431
1646
  return true;
1432
1647
  } catch {
1433
1648
  return false;
1434
1649
  }
1435
1650
  }
1436
- var import_promises8, import_node_path6, LOCKFILE_CANDIDATES;
1651
+ var import_promises9, import_node_path7, LOCKFILE_CANDIDATES;
1437
1652
  var init_parsers = __esm({
1438
1653
  "src/parsers/index.ts"() {
1439
1654
  "use strict";
1440
- import_promises8 = require("fs/promises");
1441
- import_node_path6 = __toESM(require("path"), 1);
1655
+ import_promises9 = require("fs/promises");
1656
+ import_node_path7 = __toESM(require("path"), 1);
1442
1657
  init_types();
1443
1658
  init_consistency();
1444
1659
  init_bun();
@@ -2879,12 +3094,12 @@ function isRegexForm(pattern) {
2879
3094
  const parts = REGEX_FORM.exec(pattern);
2880
3095
  return parts !== null && REGEX_SYNTAX.test(parts[1]);
2881
3096
  }
2882
- function pathMatches(pattern, path7) {
3097
+ function pathMatches(pattern, path8) {
2883
3098
  if (isRegexForm(pattern)) {
2884
3099
  const re = safeRegExp(pattern);
2885
- return re ? re.test(path7) : false;
3100
+ return re ? re.test(path8) : false;
2886
3101
  }
2887
- const target = foldPathForScope(path7);
3102
+ const target = foldPathForScope(path8);
2888
3103
  const folded = foldPathForScope(pattern);
2889
3104
  if (folded.includes("*")) {
2890
3105
  const trailing = folded.endsWith("/*");
@@ -4426,7 +4641,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
4426
4641
  for (const moduleName of ["node:http", "node:https"]) {
4427
4642
  try {
4428
4643
  const mod = await import(moduleName);
4429
- const restore = patchHttpModule(mod.default ?? mod, block, screen);
4644
+ const restore = patchHttpModule(mod.default ?? mod, block, screen, skip);
4430
4645
  if (restore) restores.push(restore);
4431
4646
  } catch {
4432
4647
  }
@@ -4440,7 +4655,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
4440
4655
  }
4441
4656
  };
4442
4657
  }
4443
- function patchHttpModule(http, block, screen) {
4658
+ function patchHttpModule(http, block, screen, skip) {
4444
4659
  if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
4445
4660
  const originalRequest = http.request;
4446
4661
  const originalGet = http.get;
@@ -4451,7 +4666,7 @@ function patchHttpModule(http, block, screen) {
4451
4666
  }
4452
4667
  if (target && screen && target.host && screen.isIP(target.host) === 0 && !screen.isExempt(target.host)) {
4453
4668
  try {
4454
- args = withScreeningLookup(args, target, block, screen.lookup);
4669
+ args = withScreeningLookup(args, target, block, screen.lookup, skip);
4455
4670
  } catch {
4456
4671
  }
4457
4672
  }
@@ -4483,8 +4698,8 @@ function extractHttpTarget(args) {
4483
4698
  const host = normalizeHost(first.hostname || first.host) || "localhost";
4484
4699
  const protocol = first.protocol || "http:";
4485
4700
  const port = first.port ? `:${first.port}` : "";
4486
- const path7 = first.path || "/";
4487
- return { url: `${protocol}//${host}${port}${path7}`, host, method: first.method || "GET" };
4701
+ const path8 = first.path || "/";
4702
+ return { url: `${protocol}//${host}${port}${path8}`, host, method: first.method || "GET" };
4488
4703
  }
4489
4704
  } catch {
4490
4705
  }
@@ -4504,7 +4719,9 @@ function screenResolved(addresses, target, block) {
4504
4719
  }
4505
4720
  return null;
4506
4721
  }
4507
- function withScreeningLookup(args, target, block, lookup) {
4722
+ function withScreeningLookup(args, target, block, lookup, skip) {
4723
+ const own = callerLookup(args);
4724
+ const resolve = typeof own === "function" ? own : lookup;
4508
4725
  const screeningLookup = (hostname, options, callback) => {
4509
4726
  let opts = options;
4510
4727
  let cb = callback;
@@ -4513,29 +4730,43 @@ function withScreeningLookup(args, target, block, lookup) {
4513
4730
  opts = {};
4514
4731
  }
4515
4732
  if (!opts || typeof opts !== "object") opts = {};
4733
+ const handOver = (list) => {
4734
+ if (opts.all) return cb(null, list);
4735
+ const first = list[0];
4736
+ if (!first) return cb(new Error(`Patchstack: could not resolve ${hostname}`));
4737
+ return cb(null, first.address, first.family);
4738
+ };
4739
+ let answered = false;
4516
4740
  try {
4517
- lookup(hostname, { ...opts, all: true }, (err, addresses) => {
4741
+ resolve(hostname, { ...opts, all: true }, (err, addresses) => {
4742
+ answered = true;
4518
4743
  if (err) return cb(err);
4519
4744
  const list = Array.isArray(addresses) ? addresses : [];
4520
- const blocked = screenResolved(list, target, block);
4745
+ let blocked;
4746
+ try {
4747
+ blocked = screenResolved(list, target, block);
4748
+ } catch {
4749
+ skip("screen-failed", { host: hostname });
4750
+ return handOver(list);
4751
+ }
4521
4752
  if (blocked) {
4522
4753
  return cb(new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host} resolved to ${blocked}`));
4523
4754
  }
4524
- if (opts.all) return cb(null, list);
4525
- const first = list[0];
4526
- if (!first) return cb(new Error(`Patchstack: could not resolve ${hostname}`));
4527
- return cb(null, first.address, first.family);
4755
+ return handOver(list);
4528
4756
  });
4529
- } catch {
4530
- try {
4531
- lookup(hostname, opts, cb);
4532
- } catch {
4533
- cb(new Error(`Patchstack: lookup failed for ${hostname}`));
4534
- }
4757
+ } catch (err) {
4758
+ if (answered) throw err;
4759
+ return cb(err);
4535
4760
  }
4536
4761
  };
4537
4762
  return injectLookupOption(args, screeningLookup);
4538
4763
  }
4764
+ function callerLookup(args) {
4765
+ for (const arg of args) {
4766
+ if (arg && typeof arg === "object" && !(arg instanceof URL) && typeof arg.lookup === "function") return arg.lookup;
4767
+ }
4768
+ return void 0;
4769
+ }
4539
4770
  function injectLookupOption(args, lookup) {
4540
4771
  const first = args[0];
4541
4772
  if (first && typeof first === "object" && !(first instanceof URL)) {
@@ -4833,12 +5064,12 @@ var fsMod;
4833
5064
  async function loadFs() {
4834
5065
  if (fsMod !== void 0) return fsMod;
4835
5066
  try {
4836
- const [fs, path7] = await Promise.all([import("fs"), import("path")]);
5067
+ const [fs, path8] = await Promise.all([import("fs"), import("path")]);
4837
5068
  fsMod = {
4838
5069
  readFileSync: fs.readFileSync,
4839
5070
  writeFileSync: fs.writeFileSync,
4840
5071
  mkdirSync: fs.mkdirSync,
4841
- join: path7.join
5072
+ join: path8.join
4842
5073
  };
4843
5074
  } catch {
4844
5075
  fsMod = null;
@@ -5132,13 +5363,13 @@ var MAX_CAPTURED_VALUE_CHARS = 512;
5132
5363
  var MAX_QUERY_KEYS = 10;
5133
5364
  var MAX_METHOD_CHARS = 16;
5134
5365
  var MAX_BODY_BYTES2 = 64 * 1024;
5135
- function queryKeysOf(path7) {
5136
- if (typeof path7 !== "string") return { keys: [], total: 0 };
5137
- const start = path7.indexOf("?");
5366
+ function queryKeysOf(path8) {
5367
+ if (typeof path8 !== "string") return { keys: [], total: 0 };
5368
+ const start = path8.indexOf("?");
5138
5369
  if (start === -1) return { keys: [], total: 0 };
5139
5370
  let names;
5140
5371
  try {
5141
- names = [...new URLSearchParams(path7.slice(start + 1)).keys()];
5372
+ names = [...new URLSearchParams(path8.slice(start + 1)).keys()];
5142
5373
  } catch {
5143
5374
  return { keys: [], total: 0 };
5144
5375
  }
@@ -5277,10 +5508,10 @@ function revisionOf(rule) {
5277
5508
  if (typeof revision === "number" && Number.isFinite(revision)) return String(revision);
5278
5509
  return null;
5279
5510
  }
5280
- function routeOf(path7) {
5281
- if (typeof path7 !== "string" || path7 === "") return null;
5282
- const cut = path7.search(/[?#]/);
5283
- return cut === -1 ? path7 : path7.slice(0, cut);
5511
+ function routeOf(path8) {
5512
+ if (typeof path8 !== "string" || path8 === "") return null;
5513
+ const cut = path8.search(/[?#]/);
5514
+ return cut === -1 ? path8 : path8.slice(0, cut);
5284
5515
  }
5285
5516
  function createDetectionReporter(opts) {
5286
5517
  const siteUuid = opts.siteUuid ?? process.env?.PATCHSTACK_SITE_UUID;
@@ -6116,8 +6347,17 @@ function createServerFnGuard({ protection }) {
6116
6347
  return { rule: body.rule, message: body.message || "Blocked by Patchstack" };
6117
6348
  };
6118
6349
  }
6350
+ function listOption(value, fallback, name, onError) {
6351
+ if (value === void 0) return fallback;
6352
+ if (Array.isArray(value)) return value;
6353
+ notify(onError, new Error(`patchstack: \`${name}\` has to be an array; the value configured is ignored`), "onError");
6354
+ return fallback;
6355
+ }
6119
6356
  async function createProtection(options = {}) {
6120
6357
  const onError = options.onError;
6358
+ const allowHosts = listOption(options.allowHosts, [], "allowHosts", onError);
6359
+ const configuredResponseRules = listOption(options.responseRules, DEFAULT_RESPONSE_RULES, "responseRules", onError);
6360
+ const configuredEgressRules = listOption(options.egressRules, DEFAULT_EGRESS_RULES, "egressRules", onError);
6121
6361
  const userOnDetect = options.onDetect ?? defaultOnDetect;
6122
6362
  const apiKey = await resolveApiKey(options);
6123
6363
  const firewallLog = apiKey && telemetryEnabled() && options.reportFirewallLog !== false ? createFirewallLogReporter({
@@ -6217,9 +6457,9 @@ async function createProtection(options = {}) {
6217
6457
  const applyBundle = (delivered) => {
6218
6458
  const incoming = delivered.firewall ?? [];
6219
6459
  requestRules = byPhase(incoming, "request");
6220
- responseRules = [...options.responseRules ?? DEFAULT_RESPONSE_RULES, ...byPhase(incoming, "response")];
6460
+ responseRules = [...configuredResponseRules, ...byPhase(incoming, "response")];
6221
6461
  screenCap = responseScreenCap(responseRules);
6222
- egressRules = [...options.egressRules ?? DEFAULT_EGRESS_RULES, ...byPhase(incoming, "egress")];
6462
+ egressRules = [...configuredEgressRules, ...byPhase(incoming, "egress")];
6223
6463
  engine = new RuleEngine({
6224
6464
  firewall: requestRules,
6225
6465
  whitelists: delivered.whitelists,
@@ -6688,7 +6928,7 @@ async function createProtection(options = {}) {
6688
6928
  return origEnd(cb);
6689
6929
  };
6690
6930
  };
6691
- const allow = new Set((options.allowHosts ?? []).map((h) => String(h).toLowerCase()));
6931
+ const allow = new Set(allowHosts.map((h) => String(h).toLowerCase()));
6692
6932
  const egressShouldBlock = (url, host, method) => {
6693
6933
  if (host && allow.has(host.toLowerCase())) return false;
6694
6934
  let egressPath = null;
@@ -6740,10 +6980,17 @@ async function createProtection(options = {}) {
6740
6980
  return { request: requestRules, response: responseRules, egress: egressRules };
6741
6981
  },
6742
6982
  /**
6743
- * Enforcement coverage: how often the guard FAILED OPEN rather than inspecting, keyed
6744
- * `<phase>:<reason>` (e.g. `response:body-cap`, `request:body-cap`, `response:live-stream`,
6745
- * `egress:resolver-failed`). "Always-on" is not "always inspected" — surface this (or pass
6746
- * `onSkip`) so an unscreened path is visible and alertable rather than silent.
6983
+ * Enforcement coverage: how often the guard failed open rather than inspecting, in the cases it can
6984
+ * observe itself — keyed `<phase>:<reason>` (e.g. `response:body-cap`, `request:body-cap`,
6985
+ * `response:live-stream`, `egress:resolver-failed` for a fetch call whose address could not be
6986
+ * resolved). "Always-on" is not "always inspected", so surface this (or pass `onSkip`) rather than
6987
+ * assuming coverage.
6988
+ *
6989
+ * Not a total, in two ways. An outbound call can miss the address check without being counted:
6990
+ * whether it resolved through the screening resolver depends on the agent and the socket pool,
6991
+ * which this guard cannot observe. And a resolver failure on the node path appears nowhere here
6992
+ * because it is not a bypass — that resolver is the connection's, so the call fails instead.
6993
+ * Zero skips means nothing the guard can see was bypassed, not that nothing was.
6747
6994
  */
6748
6995
  coverage() {
6749
6996
  return { skipped: { ...skipCounts } };
@@ -6876,7 +7123,7 @@ async function createProtection(options = {}) {
6876
7123
  // same skip accounting as the request/response phases.
6877
7124
  onSkip: ({ reason, detail }) => recordSkip("egress", reason, detail),
6878
7125
  dnsScreen: options.screenDns !== false,
6879
- allowHosts: options.allowHosts
7126
+ allowHosts
6880
7127
  });
6881
7128
  }
6882
7129
  const cwd = options.cwd ?? (typeof process !== "undefined" ? process.cwd() : void 0);
@@ -7378,20 +7625,20 @@ function requestMetaFromContext(reqCtx) {
7378
7625
  }
7379
7626
  function requestMeta(shaped, request) {
7380
7627
  const client = shaped?._clientIp ?? { ip: null, source: "unavailable" };
7381
- let path7 = null;
7628
+ let path8 = null;
7382
7629
  let method = null;
7383
7630
  let userAgent = null;
7384
7631
  if (request) {
7385
7632
  try {
7386
7633
  const u = new URL(request.url);
7387
- path7 = u.pathname + u.search;
7634
+ path8 = u.pathname + u.search;
7388
7635
  } catch {
7389
- path7 = typeof request.url === "string" ? request.url : null;
7636
+ path8 = typeof request.url === "string" ? request.url : null;
7390
7637
  }
7391
7638
  method = request.method ?? null;
7392
7639
  userAgent = request.headers?.get?.("user-agent") ?? null;
7393
7640
  }
7394
- return { method, path: path7, ip: client.ip, clientIpSource: client.source, userAgent, event: eventFor(request) };
7641
+ return { method, path: path8, ip: client.ip, clientIpSource: client.source, userAgent, event: eventFor(request) };
7395
7642
  }
7396
7643
  function nodeRequestMeta(req, client) {
7397
7644
  if (!req) return {};