@patchstack/connect 0.4.2 → 0.5.1

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 +24 -4
  2. package/README.md +27 -3
  3. package/dist/cli.js +319 -61
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.cjs +230 -20
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +65 -0
  8. package/dist/index.d.ts +65 -0
  9. package/dist/index.js +227 -17
  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 +351 -97
  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-JNCTIAM5.js → refresh-manifest-2MMPQN2A.js} +243 -35
  32. package/dist/refresh-manifest-2MMPQN2A.js.map +1 -0
  33. package/package.json +3 -3
  34. package/dist/refresh-manifest-JNCTIAM5.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();
@@ -142,6 +142,9 @@ var init_types = __esm({
142
142
  });
143
143
 
144
144
  // src/client.ts
145
+ function claimTokenHeader(config) {
146
+ return typeof config.claimToken === "string" && config.claimToken !== "" ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
147
+ }
145
148
  function buildEndpointUrl(base, siteUuid) {
146
149
  const trimmed = base.replace(/\/$/, "");
147
150
  return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
@@ -159,6 +162,14 @@ function authFailureMessage(status, config) {
159
162
  }
160
163
  return null;
161
164
  }
165
+ function buildManifestBody(config, payload) {
166
+ return {
167
+ ...payload,
168
+ environment: config.environment,
169
+ ...typeof config.siteUrl === "string" && config.siteUrl !== "" ? { url: config.siteUrl } : {},
170
+ ...typeof config.siteName === "string" && config.siteName !== "" ? { name: config.siteName } : {}
171
+ };
172
+ }
162
173
  async function postManifest(config, payload) {
163
174
  const url = buildEndpointUrl(config.endpoint, config.siteUuid);
164
175
  const timeoutMs = config.timeoutMs;
@@ -169,9 +180,10 @@ async function postManifest(config, payload) {
169
180
  headers: {
170
181
  "Content-Type": "application/json",
171
182
  Accept: "application/json",
172
- "User-Agent": "@patchstack/connect"
183
+ "User-Agent": "@patchstack/connect",
184
+ ...claimTokenHeader(config)
173
185
  },
174
- body: JSON.stringify({ ...payload, environment: config.environment }),
186
+ body: JSON.stringify(buildManifestBody(config, payload)),
175
187
  signal: AbortSignal.timeout(timeoutMs)
176
188
  });
177
189
  } catch (cause) {
@@ -228,7 +240,7 @@ function isTimeoutError(cause) {
228
240
  }
229
241
  return false;
230
242
  }
231
- var DEFAULT_ENDPOINT, DEFAULT_TIMEOUT_MS;
243
+ var DEFAULT_ENDPOINT, DEFAULT_TIMEOUT_MS, CLAIM_TOKEN_HEADER;
232
244
  var init_client = __esm({
233
245
  "src/client.ts"() {
234
246
  "use strict";
@@ -236,10 +248,211 @@ var init_client = __esm({
236
248
  init_pulse_token();
237
249
  DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
238
250
  DEFAULT_TIMEOUT_MS = 3e4;
251
+ CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
252
+ }
253
+ });
254
+
255
+ // src/site-url.ts
256
+ function isUnpublishableHost(hostname) {
257
+ const host = hostname.toLowerCase().replace(/\.+$/, "");
258
+ if (host === "") return true;
259
+ if (host.startsWith("[")) return true;
260
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
261
+ if (!host.includes(".")) return true;
262
+ return RESERVED_SUFFIXES.some((suffix) => host.endsWith(suffix));
263
+ }
264
+ function normaliseSiteUrl(value) {
265
+ const trimmed = (value ?? "").trim();
266
+ if (trimmed === "") return null;
267
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
268
+ let parsed;
269
+ try {
270
+ parsed = new URL(withScheme);
271
+ } catch {
272
+ return null;
273
+ }
274
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
275
+ if (parsed.hostname === "" || isUnpublishableHost(parsed.hostname)) return null;
276
+ const origin = `${parsed.protocol}//${parsed.host}`;
277
+ return origin.length <= 191 ? origin : null;
278
+ }
279
+ function detectSiteUrl(env = process.env) {
280
+ for (const source of PRODUCTION_URL_SOURCES) {
281
+ const url = normaliseSiteUrl(source.read(env));
282
+ if (url !== null) {
283
+ return { url, platform: source.platform };
284
+ }
285
+ }
286
+ return null;
287
+ }
288
+ var RESERVED_SUFFIXES, PRODUCTION_URL_SOURCES;
289
+ var init_site_url = __esm({
290
+ "src/site-url.ts"() {
291
+ "use strict";
292
+ RESERVED_SUFFIXES = [
293
+ ".local",
294
+ ".localhost",
295
+ ".localdomain",
296
+ ".internal",
297
+ ".intranet",
298
+ ".invalid",
299
+ ".test",
300
+ ".example",
301
+ ".home.arpa",
302
+ ".in-addr.arpa",
303
+ ".ip6.arpa"
304
+ ];
305
+ PRODUCTION_URL_SOURCES = [
306
+ {
307
+ // VERCEL_URL is per-deployment and changes every build, so it is deliberately not read here.
308
+ platform: "vercel",
309
+ read: (env) => env.VERCEL_ENV === "production" ? env.VERCEL_PROJECT_PRODUCTION_URL : void 0
310
+ },
311
+ {
312
+ // `URL` is generic enough to belong to anything, so it counts only alongside Netlify's own markers.
313
+ platform: "netlify",
314
+ read: (env) => env.NETLIFY === "true" && env.CONTEXT === "production" ? env.URL : void 0
315
+ },
316
+ {
317
+ platform: "render",
318
+ read: (env) => env.RENDER === "true" && env.IS_PULL_REQUEST !== "true" ? env.RENDER_EXTERNAL_URL : void 0
319
+ },
320
+ {
321
+ platform: "railway",
322
+ read: (env) => env.RAILWAY_ENVIRONMENT_NAME === "production" ? env.RAILWAY_PUBLIC_DOMAIN : void 0
323
+ }
324
+ ];
325
+ }
326
+ });
327
+
328
+ // src/site-name.ts
329
+ function normaliseSiteName(value) {
330
+ const collapsed = (value ?? "").replace(/\s+/g, " ").trim();
331
+ if (collapsed === "") return null;
332
+ return collapsed.slice(0, NAME_MAX_LENGTH);
333
+ }
334
+ function decodeEntities(text) {
335
+ return text.replace(/&([a-z]+);/gi, (whole, name) => NAMED_ENTITIES2[name.toLowerCase()] ?? whole);
336
+ }
337
+ function nameFromHtmlTitle(html) {
338
+ const match = /<title\b[^>]*>([\s\S]*?)<\/title>/i.exec(html);
339
+ if (match === null) return null;
340
+ const name = normaliseSiteName(decodeEntities(match[1] ?? ""));
341
+ if (name === null || TEMPLATE_TITLES.has(name.toLowerCase())) return null;
342
+ return name;
343
+ }
344
+ function nameFromPackageName(raw) {
345
+ if (typeof raw !== "string") return null;
346
+ const unscoped = raw.trim().replace(/^@[^/]+\//, "");
347
+ if (unscoped === "" || TEMPLATE_PACKAGE_NAMES.has(unscoped.toLowerCase())) return null;
348
+ const words = unscoped.split(/[-_.\s]+/).filter((word) => word !== "").map((word) => word === word.toUpperCase() ? word : word.charAt(0).toUpperCase() + word.slice(1));
349
+ return normaliseSiteName(words.join(" "));
350
+ }
351
+ async function readIfPresent(file) {
352
+ try {
353
+ return await (0, import_promises.readFile)(file, "utf8");
354
+ } catch {
355
+ return null;
356
+ }
357
+ }
358
+ async function detectSiteName(cwd) {
359
+ for (const shell of ["index.html", import_node_path.default.join("public", "index.html")]) {
360
+ const html = await readIfPresent(import_node_path.default.join(cwd, shell));
361
+ if (html === null) continue;
362
+ const name = nameFromHtmlTitle(html);
363
+ if (name !== null) return { name, source: "index.html" };
364
+ }
365
+ const manifest = await readIfPresent(import_node_path.default.join(cwd, "package.json"));
366
+ if (manifest !== null) {
367
+ try {
368
+ const parsed = JSON.parse(manifest);
369
+ const name = nameFromPackageName(
370
+ typeof parsed === "object" && parsed !== null ? parsed.name : void 0
371
+ );
372
+ if (name !== null) return { name, source: "package.json" };
373
+ } catch {
374
+ }
375
+ }
376
+ return null;
377
+ }
378
+ var import_promises, import_node_path, NAME_MAX_LENGTH, TEMPLATE_PACKAGE_NAMES, TEMPLATE_TITLES, NAMED_ENTITIES2;
379
+ var init_site_name = __esm({
380
+ "src/site-name.ts"() {
381
+ "use strict";
382
+ import_promises = require("fs/promises");
383
+ import_node_path = __toESM(require("path"), 1);
384
+ NAME_MAX_LENGTH = 191;
385
+ TEMPLATE_PACKAGE_NAMES = /* @__PURE__ */ new Set([
386
+ // Lovable
387
+ "vite_react_shadcn_ts",
388
+ // Bolt
389
+ "vite-react-typescript-starter",
390
+ // Scaffolders and the names people accept from them
391
+ "vite-project",
392
+ "my-app",
393
+ "my-project",
394
+ "my-v0-project",
395
+ "app",
396
+ "frontend",
397
+ "web",
398
+ "client",
399
+ "project",
400
+ "template",
401
+ "example",
402
+ "react-app",
403
+ "next-app",
404
+ "nextjs",
405
+ "starter",
406
+ "test"
407
+ ]);
408
+ TEMPLATE_TITLES = /* @__PURE__ */ new Set([
409
+ "vite + react",
410
+ "vite + react + ts",
411
+ "vite + vue",
412
+ "vite + vue + ts",
413
+ "vite app",
414
+ "react app",
415
+ "create next app",
416
+ "document",
417
+ "untitled",
418
+ "index",
419
+ "home",
420
+ "app"
421
+ ]);
422
+ NAMED_ENTITIES2 = {
423
+ amp: "&",
424
+ lt: "<",
425
+ gt: ">",
426
+ quot: '"',
427
+ apos: "'",
428
+ nbsp: " "
429
+ };
239
430
  }
240
431
  });
241
432
 
242
433
  // src/config.ts
434
+ async function resolveSiteIdentity(cwd, fromEnv, fromFile) {
435
+ const configuredUrl = stated(fromEnv.url) ?? stated(fromFile.url);
436
+ const configuredName = stated(fromEnv.name) ?? stated(fromFile.name);
437
+ let url;
438
+ if (configuredUrl === null) {
439
+ url = detectSiteUrl(process.env)?.url ?? null;
440
+ } else {
441
+ url = normaliseSiteUrl(configuredUrl);
442
+ if (url === null) {
443
+ throw new PatchstackError(
444
+ `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.`,
445
+ "CONFIG_INVALID"
446
+ );
447
+ }
448
+ }
449
+ const name = configuredName === null ? (await detectSiteName(cwd))?.name ?? null : normaliseSiteName(configuredName);
450
+ return { url, name };
451
+ }
452
+ function stated(value) {
453
+ const trimmed = (value ?? "").trim();
454
+ return trimmed === "" ? null : trimmed;
455
+ }
243
456
  async function resolveConfig(options) {
244
457
  const fromFile = await readConfigFile(options.cwd);
245
458
  const fromSecretFile = await readSecretFile(options.cwd);
@@ -269,14 +482,19 @@ async function resolveConfig(options) {
269
482
  }
270
483
  const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
271
484
  const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
485
+ const identity = options.detectSiteIdentity === true ? await resolveSiteIdentity(options.cwd, fromEnv, fromFile) : { url: null, name: null };
486
+ const claimToken = stated(options.cliClaimToken) ?? stated(process.env.PATCHSTACK_CLAIM_TOKEN);
272
487
  return {
273
488
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
274
489
  apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
275
490
  pulseAuth: pulseAuthRaw === null || pulseAuthRaw.length === 0 ? null : pulseAuthRaw,
491
+ siteUrl: identity.url,
492
+ siteName: identity.name,
276
493
  endpoint,
277
494
  timeoutMs,
278
495
  environment,
279
- widget: fromFile.widget !== false
496
+ widget: fromFile.widget !== false,
497
+ claimToken
280
498
  };
281
499
  }
282
500
  async function readSecretFile(cwd) {
@@ -287,10 +505,10 @@ async function readConfigFile(cwd) {
287
505
  return readJsonFile(cwd, CONFIG_FILENAME);
288
506
  }
289
507
  async function readJsonFile(cwd, filename) {
290
- const target = import_node_path.default.join(cwd, filename);
508
+ const target = import_node_path2.default.join(cwd, filename);
291
509
  let raw;
292
510
  try {
293
- raw = await (0, import_promises.readFile)(target, "utf8");
511
+ raw = await (0, import_promises2.readFile)(target, "utf8");
294
512
  } catch (err) {
295
513
  if (err.code === "ENOENT") {
296
514
  return {};
@@ -327,6 +545,8 @@ function readEnv() {
327
545
  const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
328
546
  return {
329
547
  siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
548
+ url: process.env.PATCHSTACK_SITE_URL ?? void 0,
549
+ name: process.env.PATCHSTACK_SITE_NAME ?? void 0,
330
550
  apiKey: process.env.PATCHSTACK_API_KEY ?? void 0,
331
551
  pulseAuth: process.env.PATCHSTACK_PULSE_AUTH ?? void 0,
332
552
  endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
@@ -340,14 +560,16 @@ function isUuid(value) {
340
560
  function isEnvironment(value) {
341
561
  return value === "production" || value === "sandbox";
342
562
  }
343
- var import_promises, import_node_path, CONFIG_FILENAME, SECRET_FILENAME, DEFAULT_ENVIRONMENT;
563
+ var import_promises2, import_node_path2, CONFIG_FILENAME, SECRET_FILENAME, DEFAULT_ENVIRONMENT;
344
564
  var init_config = __esm({
345
565
  "src/config.ts"() {
346
566
  "use strict";
347
- import_promises = require("fs/promises");
348
- import_node_path = __toESM(require("path"), 1);
567
+ import_promises2 = require("fs/promises");
568
+ import_node_path2 = __toESM(require("path"), 1);
349
569
  init_types();
350
570
  init_client();
571
+ init_site_url();
572
+ init_site_name();
351
573
  CONFIG_FILENAME = ".patchstackrc.json";
352
574
  SECRET_FILENAME = ".patchstackrc.local.json";
353
575
  DEFAULT_ENVIRONMENT = "production";
@@ -358,7 +580,7 @@ var init_config = __esm({
358
580
  async function readDeclaredDependencyNames(cwd) {
359
581
  let raw;
360
582
  try {
361
- raw = await (0, import_promises2.readFile)(import_node_path2.default.join(cwd, "package.json"), "utf8");
583
+ raw = await (0, import_promises3.readFile)(import_node_path3.default.join(cwd, "package.json"), "utf8");
362
584
  } catch {
363
585
  return [];
364
586
  }
@@ -407,12 +629,12 @@ function disagreements(packages, other) {
407
629
  }
408
630
  return conflicts;
409
631
  }
410
- var import_promises2, import_node_path2;
632
+ var import_promises3, import_node_path3;
411
633
  var init_consistency = __esm({
412
634
  "src/parsers/consistency.ts"() {
413
635
  "use strict";
414
- import_promises2 = require("fs/promises");
415
- import_node_path2 = __toESM(require("path"), 1);
636
+ import_promises3 = require("fs/promises");
637
+ import_node_path3 = __toESM(require("path"), 1);
416
638
  }
417
639
  });
418
640
 
@@ -443,7 +665,7 @@ var init_report = __esm({
443
665
  async function parseBunLockfile(lockfilePath, report) {
444
666
  let raw;
445
667
  try {
446
- raw = await (0, import_promises3.readFile)(lockfilePath, "utf8");
668
+ raw = await (0, import_promises4.readFile)(lockfilePath, "utf8");
447
669
  } catch (cause) {
448
670
  throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
449
671
  }
@@ -557,11 +779,11 @@ function stripTrailingCommas(source) {
557
779
  }
558
780
  return out.join("");
559
781
  }
560
- var import_promises3, NON_REGISTRY_VERSION;
782
+ var import_promises4, NON_REGISTRY_VERSION;
561
783
  var init_bun = __esm({
562
784
  "src/parsers/bun.ts"() {
563
785
  "use strict";
564
- import_promises3 = require("fs/promises");
786
+ import_promises4 = require("fs/promises");
565
787
  init_types();
566
788
  init_report();
567
789
  NON_REGISTRY_VERSION = /^[a-z][a-z0-9+.-]*:|\//i;
@@ -572,7 +794,7 @@ var init_bun = __esm({
572
794
  async function parseNpmLockfile(lockfilePath) {
573
795
  let raw;
574
796
  try {
575
- raw = await (0, import_promises4.readFile)(lockfilePath, "utf8");
797
+ raw = await (0, import_promises5.readFile)(lockfilePath, "utf8");
576
798
  } catch (cause) {
577
799
  throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
578
800
  }
@@ -630,7 +852,7 @@ function extractFromV1(deps, acc = [], depth = 0) {
630
852
  return acc;
631
853
  }
632
854
  function extractNameFromPath(pkgPath) {
633
- const segments = pkgPath.split("node_modules" + import_node_path3.default.sep === pkgPath ? import_node_path3.default.sep : "/");
855
+ const segments = pkgPath.split("node_modules" + import_node_path4.default.sep === pkgPath ? import_node_path4.default.sep : "/");
634
856
  const parts = pkgPath.split("/");
635
857
  const nmIndex = parts.lastIndexOf("node_modules");
636
858
  if (nmIndex === -1 || nmIndex >= parts.length - 1) {
@@ -651,21 +873,21 @@ function isDirectV2(pkgPath) {
651
873
  const nmCount = parts.filter((p) => p === "node_modules").length;
652
874
  return nmCount === 1;
653
875
  }
654
- var import_promises4, import_node_path3;
876
+ var import_promises5, import_node_path4;
655
877
  var init_npm = __esm({
656
878
  "src/parsers/npm.ts"() {
657
879
  "use strict";
658
- import_promises4 = require("fs/promises");
659
- import_node_path3 = __toESM(require("path"), 1);
880
+ import_promises5 = require("fs/promises");
881
+ import_node_path4 = __toESM(require("path"), 1);
660
882
  init_types();
661
883
  }
662
884
  });
663
885
 
664
886
  // src/parsers/node_modules.ts
665
887
  async function walkNodeModules(cwd) {
666
- const root = import_node_path4.default.join(cwd, "node_modules");
888
+ const root = import_node_path5.default.join(cwd, "node_modules");
667
889
  try {
668
- const info = await (0, import_promises5.stat)(root);
890
+ const info = await (0, import_promises6.stat)(root);
669
891
  if (!info.isDirectory()) {
670
892
  throw new PatchstackError(
671
893
  `${root} exists but is not a directory.`,
@@ -684,7 +906,7 @@ async function walkNodeModules(cwd) {
684
906
  }
685
907
  const seen = /* @__PURE__ */ new Map();
686
908
  try {
687
- seen.set(await (0, import_promises5.realpath)(cwd), -1);
909
+ seen.set(await (0, import_promises6.realpath)(cwd), -1);
688
910
  } catch {
689
911
  }
690
912
  return dedupeByShallowest(await walk(root, 0, seen, cwd));
@@ -700,26 +922,26 @@ function dedupeByShallowest(found) {
700
922
  async function walk(dir, depth, seen, root) {
701
923
  let names;
702
924
  try {
703
- names = await (0, import_promises5.readdir)(dir);
925
+ names = await (0, import_promises6.readdir)(dir);
704
926
  } catch {
705
927
  return [];
706
928
  }
707
929
  const perEntry = await mapLimit(names, async (name) => {
708
930
  if (name.startsWith(".") && !PACKAGE_STORE_DIRS.has(name)) return [];
709
- const fullPath = import_node_path4.default.join(dir, name);
931
+ const fullPath = import_node_path5.default.join(dir, name);
710
932
  const resolved = await claim(fullPath, seen, depth);
711
933
  if (resolved === null) return [];
712
934
  if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen, root);
713
935
  if (name.startsWith("@")) {
714
936
  let subNames;
715
937
  try {
716
- subNames = await (0, import_promises5.readdir)(fullPath);
938
+ subNames = await (0, import_promises6.readdir)(fullPath);
717
939
  } catch {
718
940
  return [];
719
941
  }
720
942
  const scoped = await mapLimit(subNames, async (sub) => {
721
943
  if (sub.startsWith(".")) return [];
722
- const scopedDir = import_node_path4.default.join(fullPath, sub);
944
+ const scopedDir = import_node_path5.default.join(fullPath, sub);
723
945
  const scopedReal = await claim(scopedDir, seen, depth);
724
946
  if (scopedReal === null) return [];
725
947
  return packageAndNested(scopedDir, scopedReal, depth, seen, root);
@@ -746,13 +968,13 @@ async function mapLimit(items, fn) {
746
968
  await Promise.all(workers);
747
969
  return results;
748
970
  }
749
- function installLocation(root, pkgDir, impl = import_node_path4.default) {
971
+ function installLocation(root, pkgDir, impl = import_node_path5.default) {
750
972
  return impl.relative(root, pkgDir).split(impl.sep).join("/");
751
973
  }
752
974
  async function readPackage(pkgDir, depth, root) {
753
975
  let raw;
754
976
  try {
755
- raw = await (0, import_promises5.readFile)(import_node_path4.default.join(pkgDir, "package.json"), "utf8");
977
+ raw = await (0, import_promises6.readFile)(import_node_path5.default.join(pkgDir, "package.json"), "utf8");
756
978
  } catch {
757
979
  return null;
758
980
  }
@@ -771,7 +993,7 @@ async function readPackage(pkgDir, depth, root) {
771
993
  return { name: parsed.name, version: parsed.version, path: installLocation(root, pkgDir), direct: depth === 0 };
772
994
  }
773
995
  async function walkNested(pkgDir, depth, seen, root) {
774
- const nested = import_node_path4.default.join(pkgDir, "node_modules");
996
+ const nested = import_node_path5.default.join(pkgDir, "node_modules");
775
997
  if (await claim(nested, seen, depth + 1) === null) {
776
998
  return [];
777
999
  }
@@ -780,10 +1002,10 @@ async function walkNested(pkgDir, depth, seen, root) {
780
1002
  async function claim(dir, seen, depth) {
781
1003
  let resolved;
782
1004
  try {
783
- const info = await (0, import_promises5.lstat)(dir);
1005
+ const info = await (0, import_promises6.lstat)(dir);
784
1006
  if (!info.isDirectory() && !info.isSymbolicLink()) return null;
785
- resolved = await (0, import_promises5.realpath)(dir);
786
- const target = await (0, import_promises5.stat)(resolved);
1007
+ resolved = await (0, import_promises6.realpath)(dir);
1008
+ const target = await (0, import_promises6.stat)(resolved);
787
1009
  if (!target.isDirectory()) return null;
788
1010
  } catch {
789
1011
  return null;
@@ -793,12 +1015,12 @@ async function claim(dir, seen, depth) {
793
1015
  seen.set(resolved, depth);
794
1016
  return resolved;
795
1017
  }
796
- var import_promises5, import_node_path4, PACKAGE_STORE_DIRS, CONCURRENCY;
1018
+ var import_promises6, import_node_path5, PACKAGE_STORE_DIRS, CONCURRENCY;
797
1019
  var init_node_modules = __esm({
798
1020
  "src/parsers/node_modules.ts"() {
799
1021
  "use strict";
800
- import_promises5 = require("fs/promises");
801
- import_node_path4 = __toESM(require("path"), 1);
1022
+ import_promises6 = require("fs/promises");
1023
+ import_node_path5 = __toESM(require("path"), 1);
802
1024
  init_types();
803
1025
  PACKAGE_STORE_DIRS = /* @__PURE__ */ new Set([".bun", ".pnpm", ".store"]);
804
1026
  CONCURRENCY = 32;
@@ -809,7 +1031,7 @@ var init_node_modules = __esm({
809
1031
  async function parsePnpmLockfile(lockfilePath, report) {
810
1032
  let raw;
811
1033
  try {
812
- raw = await (0, import_promises6.readFile)(lockfilePath, "utf8");
1034
+ raw = await (0, import_promises7.readFile)(lockfilePath, "utf8");
813
1035
  } catch (cause) {
814
1036
  throw new PatchstackError(
815
1037
  `Could not read lockfile at ${lockfilePath}`,
@@ -1051,11 +1273,11 @@ function extractLeafName(trimmed) {
1051
1273
  }
1052
1274
  return name.length > 0 ? name : null;
1053
1275
  }
1054
- var import_promises6, DEP_SECTIONS;
1276
+ var import_promises7, DEP_SECTIONS;
1055
1277
  var init_pnpm = __esm({
1056
1278
  "src/parsers/pnpm.ts"() {
1057
1279
  "use strict";
1058
- import_promises6 = require("fs/promises");
1280
+ import_promises7 = require("fs/promises");
1059
1281
  init_types();
1060
1282
  init_report();
1061
1283
  DEP_SECTIONS = /* @__PURE__ */ new Set([
@@ -1070,7 +1292,7 @@ var init_pnpm = __esm({
1070
1292
  async function parseYarnLockfile(lockfilePath, report) {
1071
1293
  let raw;
1072
1294
  try {
1073
- raw = await (0, import_promises7.readFile)(lockfilePath, "utf8");
1295
+ raw = await (0, import_promises8.readFile)(lockfilePath, "utf8");
1074
1296
  } catch (cause) {
1075
1297
  throw new PatchstackError(
1076
1298
  `Could not read lockfile at ${lockfilePath}`,
@@ -1085,7 +1307,7 @@ async function parseYarnLockfile(lockfilePath, report) {
1085
1307
  "LOCKFILE_PARSE_ERROR"
1086
1308
  );
1087
1309
  }
1088
- const directNames = await readDirectDepNames(import_node_path5.default.dirname(lockfilePath));
1310
+ const directNames = await readDirectDepNames(import_node_path6.default.dirname(lockfilePath));
1089
1311
  const entries = [];
1090
1312
  const seen = /* @__PURE__ */ new Set();
1091
1313
  for (const block of blocks) {
@@ -1250,7 +1472,7 @@ async function readDirectDepNames(cwd) {
1250
1472
  const names = /* @__PURE__ */ new Set();
1251
1473
  let raw;
1252
1474
  try {
1253
- raw = await (0, import_promises7.readFile)(import_node_path5.default.join(cwd, "package.json"), "utf8");
1475
+ raw = await (0, import_promises8.readFile)(import_node_path6.default.join(cwd, "package.json"), "utf8");
1254
1476
  } catch {
1255
1477
  return names;
1256
1478
  }
@@ -1275,12 +1497,12 @@ async function readDirectDepNames(cwd) {
1275
1497
  }
1276
1498
  return names;
1277
1499
  }
1278
- var import_promises7, import_node_path5, NON_PACKAGE_KEYS, NON_REGISTRY_PROTOCOLS;
1500
+ var import_promises8, import_node_path6, NON_PACKAGE_KEYS, NON_REGISTRY_PROTOCOLS;
1279
1501
  var init_yarn = __esm({
1280
1502
  "src/parsers/yarn.ts"() {
1281
1503
  "use strict";
1282
- import_promises7 = require("fs/promises");
1283
- import_node_path5 = __toESM(require("path"), 1);
1504
+ import_promises8 = require("fs/promises");
1505
+ import_node_path6 = __toESM(require("path"), 1);
1284
1506
  init_types();
1285
1507
  init_report();
1286
1508
  NON_PACKAGE_KEYS = /* @__PURE__ */ new Set(["__metadata"]);
@@ -1292,7 +1514,7 @@ var init_yarn = __esm({
1292
1514
  async function detectLockfile(cwd) {
1293
1515
  const probed = await Promise.all(
1294
1516
  LOCKFILE_CANDIDATES.map(async (candidate) => {
1295
- const filePath = import_node_path6.default.join(cwd, candidate.filename);
1517
+ const filePath = import_node_path7.default.join(cwd, candidate.filename);
1296
1518
  return { ...candidate, filePath, present: await exists(filePath) };
1297
1519
  })
1298
1520
  );
@@ -1403,7 +1625,7 @@ function staleWarning(source, missing) {
1403
1625
  async function presentLockfiles(cwd) {
1404
1626
  const probed = await Promise.all(
1405
1627
  LOCKFILE_CANDIDATES.map(async (candidate) => {
1406
- const filePath = import_node_path6.default.join(cwd, candidate.filename);
1628
+ const filePath = import_node_path7.default.join(cwd, candidate.filename);
1407
1629
  return { ...candidate, filePath, present: await exists(filePath) };
1408
1630
  })
1409
1631
  );
@@ -1427,18 +1649,18 @@ async function runStrategy(detected, cwd, report) {
1427
1649
  }
1428
1650
  async function exists(filePath) {
1429
1651
  try {
1430
- await (0, import_promises8.access)(filePath);
1652
+ await (0, import_promises9.access)(filePath);
1431
1653
  return true;
1432
1654
  } catch {
1433
1655
  return false;
1434
1656
  }
1435
1657
  }
1436
- var import_promises8, import_node_path6, LOCKFILE_CANDIDATES;
1658
+ var import_promises9, import_node_path7, LOCKFILE_CANDIDATES;
1437
1659
  var init_parsers = __esm({
1438
1660
  "src/parsers/index.ts"() {
1439
1661
  "use strict";
1440
- import_promises8 = require("fs/promises");
1441
- import_node_path6 = __toESM(require("path"), 1);
1662
+ import_promises9 = require("fs/promises");
1663
+ import_node_path7 = __toESM(require("path"), 1);
1442
1664
  init_types();
1443
1665
  init_consistency();
1444
1666
  init_bun();
@@ -2879,12 +3101,12 @@ function isRegexForm(pattern) {
2879
3101
  const parts = REGEX_FORM.exec(pattern);
2880
3102
  return parts !== null && REGEX_SYNTAX.test(parts[1]);
2881
3103
  }
2882
- function pathMatches(pattern, path7) {
3104
+ function pathMatches(pattern, path8) {
2883
3105
  if (isRegexForm(pattern)) {
2884
3106
  const re = safeRegExp(pattern);
2885
- return re ? re.test(path7) : false;
3107
+ return re ? re.test(path8) : false;
2886
3108
  }
2887
- const target = foldPathForScope(path7);
3109
+ const target = foldPathForScope(path8);
2888
3110
  const folded = foldPathForScope(pattern);
2889
3111
  if (folded.includes("*")) {
2890
3112
  const trailing = folded.endsWith("/*");
@@ -4426,7 +4648,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
4426
4648
  for (const moduleName of ["node:http", "node:https"]) {
4427
4649
  try {
4428
4650
  const mod = await import(moduleName);
4429
- const restore = patchHttpModule(mod.default ?? mod, block, screen);
4651
+ const restore = patchHttpModule(mod.default ?? mod, block, screen, skip);
4430
4652
  if (restore) restores.push(restore);
4431
4653
  } catch {
4432
4654
  }
@@ -4440,7 +4662,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
4440
4662
  }
4441
4663
  };
4442
4664
  }
4443
- function patchHttpModule(http, block, screen) {
4665
+ function patchHttpModule(http, block, screen, skip) {
4444
4666
  if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
4445
4667
  const originalRequest = http.request;
4446
4668
  const originalGet = http.get;
@@ -4451,7 +4673,7 @@ function patchHttpModule(http, block, screen) {
4451
4673
  }
4452
4674
  if (target && screen && target.host && screen.isIP(target.host) === 0 && !screen.isExempt(target.host)) {
4453
4675
  try {
4454
- args = withScreeningLookup(args, target, block, screen.lookup);
4676
+ args = withScreeningLookup(args, target, block, screen.lookup, skip);
4455
4677
  } catch {
4456
4678
  }
4457
4679
  }
@@ -4483,8 +4705,8 @@ function extractHttpTarget(args) {
4483
4705
  const host = normalizeHost(first.hostname || first.host) || "localhost";
4484
4706
  const protocol = first.protocol || "http:";
4485
4707
  const port = first.port ? `:${first.port}` : "";
4486
- const path7 = first.path || "/";
4487
- return { url: `${protocol}//${host}${port}${path7}`, host, method: first.method || "GET" };
4708
+ const path8 = first.path || "/";
4709
+ return { url: `${protocol}//${host}${port}${path8}`, host, method: first.method || "GET" };
4488
4710
  }
4489
4711
  } catch {
4490
4712
  }
@@ -4504,7 +4726,9 @@ function screenResolved(addresses, target, block) {
4504
4726
  }
4505
4727
  return null;
4506
4728
  }
4507
- function withScreeningLookup(args, target, block, lookup) {
4729
+ function withScreeningLookup(args, target, block, lookup, skip) {
4730
+ const own = callerLookup(args);
4731
+ const resolve = typeof own === "function" ? own : lookup;
4508
4732
  const screeningLookup = (hostname, options, callback) => {
4509
4733
  let opts = options;
4510
4734
  let cb = callback;
@@ -4513,29 +4737,43 @@ function withScreeningLookup(args, target, block, lookup) {
4513
4737
  opts = {};
4514
4738
  }
4515
4739
  if (!opts || typeof opts !== "object") opts = {};
4740
+ const handOver = (list) => {
4741
+ if (opts.all) return cb(null, list);
4742
+ const first = list[0];
4743
+ if (!first) return cb(new Error(`Patchstack: could not resolve ${hostname}`));
4744
+ return cb(null, first.address, first.family);
4745
+ };
4746
+ let answered = false;
4516
4747
  try {
4517
- lookup(hostname, { ...opts, all: true }, (err, addresses) => {
4748
+ resolve(hostname, { ...opts, all: true }, (err, addresses) => {
4749
+ answered = true;
4518
4750
  if (err) return cb(err);
4519
4751
  const list = Array.isArray(addresses) ? addresses : [];
4520
- const blocked = screenResolved(list, target, block);
4752
+ let blocked;
4753
+ try {
4754
+ blocked = screenResolved(list, target, block);
4755
+ } catch {
4756
+ skip("screen-failed", { host: hostname });
4757
+ return handOver(list);
4758
+ }
4521
4759
  if (blocked) {
4522
4760
  return cb(new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host} resolved to ${blocked}`));
4523
4761
  }
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);
4762
+ return handOver(list);
4528
4763
  });
4529
- } catch {
4530
- try {
4531
- lookup(hostname, opts, cb);
4532
- } catch {
4533
- cb(new Error(`Patchstack: lookup failed for ${hostname}`));
4534
- }
4764
+ } catch (err) {
4765
+ if (answered) throw err;
4766
+ return cb(err);
4535
4767
  }
4536
4768
  };
4537
4769
  return injectLookupOption(args, screeningLookup);
4538
4770
  }
4771
+ function callerLookup(args) {
4772
+ for (const arg of args) {
4773
+ if (arg && typeof arg === "object" && !(arg instanceof URL) && typeof arg.lookup === "function") return arg.lookup;
4774
+ }
4775
+ return void 0;
4776
+ }
4539
4777
  function injectLookupOption(args, lookup) {
4540
4778
  const first = args[0];
4541
4779
  if (first && typeof first === "object" && !(first instanceof URL)) {
@@ -4833,12 +5071,12 @@ var fsMod;
4833
5071
  async function loadFs() {
4834
5072
  if (fsMod !== void 0) return fsMod;
4835
5073
  try {
4836
- const [fs, path7] = await Promise.all([import("fs"), import("path")]);
5074
+ const [fs, path8] = await Promise.all([import("fs"), import("path")]);
4837
5075
  fsMod = {
4838
5076
  readFileSync: fs.readFileSync,
4839
5077
  writeFileSync: fs.writeFileSync,
4840
5078
  mkdirSync: fs.mkdirSync,
4841
- join: path7.join
5079
+ join: path8.join
4842
5080
  };
4843
5081
  } catch {
4844
5082
  fsMod = null;
@@ -5132,13 +5370,13 @@ var MAX_CAPTURED_VALUE_CHARS = 512;
5132
5370
  var MAX_QUERY_KEYS = 10;
5133
5371
  var MAX_METHOD_CHARS = 16;
5134
5372
  var MAX_BODY_BYTES2 = 64 * 1024;
5135
- function queryKeysOf(path7) {
5136
- if (typeof path7 !== "string") return { keys: [], total: 0 };
5137
- const start = path7.indexOf("?");
5373
+ function queryKeysOf(path8) {
5374
+ if (typeof path8 !== "string") return { keys: [], total: 0 };
5375
+ const start = path8.indexOf("?");
5138
5376
  if (start === -1) return { keys: [], total: 0 };
5139
5377
  let names;
5140
5378
  try {
5141
- names = [...new URLSearchParams(path7.slice(start + 1)).keys()];
5379
+ names = [...new URLSearchParams(path8.slice(start + 1)).keys()];
5142
5380
  } catch {
5143
5381
  return { keys: [], total: 0 };
5144
5382
  }
@@ -5277,10 +5515,10 @@ function revisionOf(rule) {
5277
5515
  if (typeof revision === "number" && Number.isFinite(revision)) return String(revision);
5278
5516
  return null;
5279
5517
  }
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);
5518
+ function routeOf(path8) {
5519
+ if (typeof path8 !== "string" || path8 === "") return null;
5520
+ const cut = path8.search(/[?#]/);
5521
+ return cut === -1 ? path8 : path8.slice(0, cut);
5284
5522
  }
5285
5523
  function createDetectionReporter(opts) {
5286
5524
  const siteUuid = opts.siteUuid ?? process.env?.PATCHSTACK_SITE_UUID;
@@ -6116,8 +6354,17 @@ function createServerFnGuard({ protection }) {
6116
6354
  return { rule: body.rule, message: body.message || "Blocked by Patchstack" };
6117
6355
  };
6118
6356
  }
6357
+ function listOption(value, fallback, name, onError) {
6358
+ if (value === void 0) return fallback;
6359
+ if (Array.isArray(value)) return value;
6360
+ notify(onError, new Error(`patchstack: \`${name}\` has to be an array; the value configured is ignored`), "onError");
6361
+ return fallback;
6362
+ }
6119
6363
  async function createProtection(options = {}) {
6120
6364
  const onError = options.onError;
6365
+ const allowHosts = listOption(options.allowHosts, [], "allowHosts", onError);
6366
+ const configuredResponseRules = listOption(options.responseRules, DEFAULT_RESPONSE_RULES, "responseRules", onError);
6367
+ const configuredEgressRules = listOption(options.egressRules, DEFAULT_EGRESS_RULES, "egressRules", onError);
6121
6368
  const userOnDetect = options.onDetect ?? defaultOnDetect;
6122
6369
  const apiKey = await resolveApiKey(options);
6123
6370
  const firewallLog = apiKey && telemetryEnabled() && options.reportFirewallLog !== false ? createFirewallLogReporter({
@@ -6217,9 +6464,9 @@ async function createProtection(options = {}) {
6217
6464
  const applyBundle = (delivered) => {
6218
6465
  const incoming = delivered.firewall ?? [];
6219
6466
  requestRules = byPhase(incoming, "request");
6220
- responseRules = [...options.responseRules ?? DEFAULT_RESPONSE_RULES, ...byPhase(incoming, "response")];
6467
+ responseRules = [...configuredResponseRules, ...byPhase(incoming, "response")];
6221
6468
  screenCap = responseScreenCap(responseRules);
6222
- egressRules = [...options.egressRules ?? DEFAULT_EGRESS_RULES, ...byPhase(incoming, "egress")];
6469
+ egressRules = [...configuredEgressRules, ...byPhase(incoming, "egress")];
6223
6470
  engine = new RuleEngine({
6224
6471
  firewall: requestRules,
6225
6472
  whitelists: delivered.whitelists,
@@ -6688,7 +6935,7 @@ async function createProtection(options = {}) {
6688
6935
  return origEnd(cb);
6689
6936
  };
6690
6937
  };
6691
- const allow = new Set((options.allowHosts ?? []).map((h) => String(h).toLowerCase()));
6938
+ const allow = new Set(allowHosts.map((h) => String(h).toLowerCase()));
6692
6939
  const egressShouldBlock = (url, host, method) => {
6693
6940
  if (host && allow.has(host.toLowerCase())) return false;
6694
6941
  let egressPath = null;
@@ -6740,10 +6987,17 @@ async function createProtection(options = {}) {
6740
6987
  return { request: requestRules, response: responseRules, egress: egressRules };
6741
6988
  },
6742
6989
  /**
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.
6990
+ * Enforcement coverage: how often the guard failed open rather than inspecting, in the cases it can
6991
+ * observe itself — keyed `<phase>:<reason>` (e.g. `response:body-cap`, `request:body-cap`,
6992
+ * `response:live-stream`, `egress:resolver-failed` for a fetch call whose address could not be
6993
+ * resolved). "Always-on" is not "always inspected", so surface this (or pass `onSkip`) rather than
6994
+ * assuming coverage.
6995
+ *
6996
+ * Not a total, in two ways. An outbound call can miss the address check without being counted:
6997
+ * whether it resolved through the screening resolver depends on the agent and the socket pool,
6998
+ * which this guard cannot observe. And a resolver failure on the node path appears nowhere here
6999
+ * because it is not a bypass — that resolver is the connection's, so the call fails instead.
7000
+ * Zero skips means nothing the guard can see was bypassed, not that nothing was.
6747
7001
  */
6748
7002
  coverage() {
6749
7003
  return { skipped: { ...skipCounts } };
@@ -6876,7 +7130,7 @@ async function createProtection(options = {}) {
6876
7130
  // same skip accounting as the request/response phases.
6877
7131
  onSkip: ({ reason, detail }) => recordSkip("egress", reason, detail),
6878
7132
  dnsScreen: options.screenDns !== false,
6879
- allowHosts: options.allowHosts
7133
+ allowHosts
6880
7134
  });
6881
7135
  }
6882
7136
  const cwd = options.cwd ?? (typeof process !== "undefined" ? process.cwd() : void 0);
@@ -7378,20 +7632,20 @@ function requestMetaFromContext(reqCtx) {
7378
7632
  }
7379
7633
  function requestMeta(shaped, request) {
7380
7634
  const client = shaped?._clientIp ?? { ip: null, source: "unavailable" };
7381
- let path7 = null;
7635
+ let path8 = null;
7382
7636
  let method = null;
7383
7637
  let userAgent = null;
7384
7638
  if (request) {
7385
7639
  try {
7386
7640
  const u = new URL(request.url);
7387
- path7 = u.pathname + u.search;
7641
+ path8 = u.pathname + u.search;
7388
7642
  } catch {
7389
- path7 = typeof request.url === "string" ? request.url : null;
7643
+ path8 = typeof request.url === "string" ? request.url : null;
7390
7644
  }
7391
7645
  method = request.method ?? null;
7392
7646
  userAgent = request.headers?.get?.("user-agent") ?? null;
7393
7647
  }
7394
- return { method, path: path7, ip: client.ip, clientIpSource: client.source, userAgent, event: eventFor(request) };
7648
+ return { method, path: path8, ip: client.ip, clientIpSource: client.source, userAgent, event: eventFor(request) };
7395
7649
  }
7396
7650
  function nodeRequestMeta(req, client) {
7397
7651
  if (!req) return {};