@patchstack/connect 0.4.2 → 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-JNCTIAM5.js → refresh-manifest-LDS35UKV.js} +234 -33
  32. package/dist/refresh-manifest-LDS35UKV.js.map +1 -0
  33. package/package.json +3 -3
  34. package/dist/refresh-manifest-JNCTIAM5.js.map +0 -1
package/dist/protect.js CHANGED
@@ -2799,7 +2799,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
2799
2799
  for (const moduleName of ["node:http", "node:https"]) {
2800
2800
  try {
2801
2801
  const mod = await import(moduleName);
2802
- const restore = patchHttpModule(mod.default ?? mod, block, screen);
2802
+ const restore = patchHttpModule(mod.default ?? mod, block, screen, skip);
2803
2803
  if (restore) restores.push(restore);
2804
2804
  } catch {
2805
2805
  }
@@ -2813,7 +2813,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
2813
2813
  }
2814
2814
  };
2815
2815
  }
2816
- function patchHttpModule(http, block, screen) {
2816
+ function patchHttpModule(http, block, screen, skip) {
2817
2817
  if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
2818
2818
  const originalRequest = http.request;
2819
2819
  const originalGet = http.get;
@@ -2824,7 +2824,7 @@ function patchHttpModule(http, block, screen) {
2824
2824
  }
2825
2825
  if (target && screen && target.host && screen.isIP(target.host) === 0 && !screen.isExempt(target.host)) {
2826
2826
  try {
2827
- args = withScreeningLookup(args, target, block, screen.lookup);
2827
+ args = withScreeningLookup(args, target, block, screen.lookup, skip);
2828
2828
  } catch {
2829
2829
  }
2830
2830
  }
@@ -2877,7 +2877,9 @@ function screenResolved(addresses, target, block) {
2877
2877
  }
2878
2878
  return null;
2879
2879
  }
2880
- function withScreeningLookup(args, target, block, lookup) {
2880
+ function withScreeningLookup(args, target, block, lookup, skip) {
2881
+ const own = callerLookup(args);
2882
+ const resolve = typeof own === "function" ? own : lookup;
2881
2883
  const screeningLookup = (hostname, options, callback) => {
2882
2884
  let opts = options;
2883
2885
  let cb = callback;
@@ -2886,29 +2888,43 @@ function withScreeningLookup(args, target, block, lookup) {
2886
2888
  opts = {};
2887
2889
  }
2888
2890
  if (!opts || typeof opts !== "object") opts = {};
2891
+ const handOver = (list) => {
2892
+ if (opts.all) return cb(null, list);
2893
+ const first = list[0];
2894
+ if (!first) return cb(new Error(`Patchstack: could not resolve ${hostname}`));
2895
+ return cb(null, first.address, first.family);
2896
+ };
2897
+ let answered = false;
2889
2898
  try {
2890
- lookup(hostname, { ...opts, all: true }, (err, addresses) => {
2899
+ resolve(hostname, { ...opts, all: true }, (err, addresses) => {
2900
+ answered = true;
2891
2901
  if (err) return cb(err);
2892
2902
  const list = Array.isArray(addresses) ? addresses : [];
2893
- const blocked = screenResolved(list, target, block);
2903
+ let blocked;
2904
+ try {
2905
+ blocked = screenResolved(list, target, block);
2906
+ } catch {
2907
+ skip("screen-failed", { host: hostname });
2908
+ return handOver(list);
2909
+ }
2894
2910
  if (blocked) {
2895
2911
  return cb(new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host} resolved to ${blocked}`));
2896
2912
  }
2897
- if (opts.all) return cb(null, list);
2898
- const first = list[0];
2899
- if (!first) return cb(new Error(`Patchstack: could not resolve ${hostname}`));
2900
- return cb(null, first.address, first.family);
2913
+ return handOver(list);
2901
2914
  });
2902
- } catch {
2903
- try {
2904
- lookup(hostname, opts, cb);
2905
- } catch {
2906
- cb(new Error(`Patchstack: lookup failed for ${hostname}`));
2907
- }
2915
+ } catch (err) {
2916
+ if (answered) throw err;
2917
+ return cb(err);
2908
2918
  }
2909
2919
  };
2910
2920
  return injectLookupOption(args, screeningLookup);
2911
2921
  }
2922
+ function callerLookup(args) {
2923
+ for (const arg of args) {
2924
+ if (arg && typeof arg === "object" && !(arg instanceof URL) && typeof arg.lookup === "function") return arg.lookup;
2925
+ }
2926
+ return void 0;
2927
+ }
2912
2928
  function injectLookupOption(args, lookup) {
2913
2929
  const first = args[0];
2914
2930
  if (first && typeof first === "object" && !(first instanceof URL)) {
@@ -4488,8 +4504,17 @@ function createServerFnGuard({ protection }) {
4488
4504
  return { rule: body.rule, message: body.message || "Blocked by Patchstack" };
4489
4505
  };
4490
4506
  }
4507
+ function listOption(value, fallback, name, onError) {
4508
+ if (value === void 0) return fallback;
4509
+ if (Array.isArray(value)) return value;
4510
+ notify(onError, new Error(`patchstack: \`${name}\` has to be an array; the value configured is ignored`), "onError");
4511
+ return fallback;
4512
+ }
4491
4513
  async function createProtection(options = {}) {
4492
4514
  const onError = options.onError;
4515
+ const allowHosts = listOption(options.allowHosts, [], "allowHosts", onError);
4516
+ const configuredResponseRules = listOption(options.responseRules, DEFAULT_RESPONSE_RULES, "responseRules", onError);
4517
+ const configuredEgressRules = listOption(options.egressRules, DEFAULT_EGRESS_RULES, "egressRules", onError);
4493
4518
  const userOnDetect = options.onDetect ?? defaultOnDetect;
4494
4519
  const apiKey = await resolveApiKey(options);
4495
4520
  const firewallLog = apiKey && telemetryEnabled() && options.reportFirewallLog !== false ? createFirewallLogReporter({
@@ -4589,9 +4614,9 @@ async function createProtection(options = {}) {
4589
4614
  const applyBundle = (delivered) => {
4590
4615
  const incoming = delivered.firewall ?? [];
4591
4616
  requestRules = byPhase(incoming, "request");
4592
- responseRules = [...options.responseRules ?? DEFAULT_RESPONSE_RULES, ...byPhase(incoming, "response")];
4617
+ responseRules = [...configuredResponseRules, ...byPhase(incoming, "response")];
4593
4618
  screenCap = responseScreenCap(responseRules);
4594
- egressRules = [...options.egressRules ?? DEFAULT_EGRESS_RULES, ...byPhase(incoming, "egress")];
4619
+ egressRules = [...configuredEgressRules, ...byPhase(incoming, "egress")];
4595
4620
  engine = new RuleEngine({
4596
4621
  firewall: requestRules,
4597
4622
  whitelists: delivered.whitelists,
@@ -5060,7 +5085,7 @@ async function createProtection(options = {}) {
5060
5085
  return origEnd(cb);
5061
5086
  };
5062
5087
  };
5063
- const allow = new Set((options.allowHosts ?? []).map((h) => String(h).toLowerCase()));
5088
+ const allow = new Set(allowHosts.map((h) => String(h).toLowerCase()));
5064
5089
  const egressShouldBlock = (url, host, method) => {
5065
5090
  if (host && allow.has(host.toLowerCase())) return false;
5066
5091
  let egressPath = null;
@@ -5112,10 +5137,17 @@ async function createProtection(options = {}) {
5112
5137
  return { request: requestRules, response: responseRules, egress: egressRules };
5113
5138
  },
5114
5139
  /**
5115
- * Enforcement coverage: how often the guard FAILED OPEN rather than inspecting, keyed
5116
- * `<phase>:<reason>` (e.g. `response:body-cap`, `request:body-cap`, `response:live-stream`,
5117
- * `egress:resolver-failed`). "Always-on" is not "always inspected" — surface this (or pass
5118
- * `onSkip`) so an unscreened path is visible and alertable rather than silent.
5140
+ * Enforcement coverage: how often the guard failed open rather than inspecting, in the cases it can
5141
+ * observe itself — keyed `<phase>:<reason>` (e.g. `response:body-cap`, `request:body-cap`,
5142
+ * `response:live-stream`, `egress:resolver-failed` for a fetch call whose address could not be
5143
+ * resolved). "Always-on" is not "always inspected", so surface this (or pass `onSkip`) rather than
5144
+ * assuming coverage.
5145
+ *
5146
+ * Not a total, in two ways. An outbound call can miss the address check without being counted:
5147
+ * whether it resolved through the screening resolver depends on the agent and the socket pool,
5148
+ * which this guard cannot observe. And a resolver failure on the node path appears nowhere here
5149
+ * because it is not a bypass — that resolver is the connection's, so the call fails instead.
5150
+ * Zero skips means nothing the guard can see was bypassed, not that nothing was.
5119
5151
  */
5120
5152
  coverage() {
5121
5153
  return { skipped: { ...skipCounts } };
@@ -5248,7 +5280,7 @@ async function createProtection(options = {}) {
5248
5280
  // same skip accounting as the request/response phases.
5249
5281
  onSkip: ({ reason, detail }) => recordSkip("egress", reason, detail),
5250
5282
  dnsScreen: options.screenDns !== false,
5251
- allowHosts: options.allowHosts
5283
+ allowHosts
5252
5284
  });
5253
5285
  }
5254
5286
  const cwd = options.cwd ?? (typeof process !== "undefined" ? process.cwd() : void 0);
@@ -5258,7 +5290,7 @@ async function createProtection(options = {}) {
5258
5290
  let reporter = null;
5259
5291
  if (refreshable && options.siteUuid && options.reportManifest !== false && cwd) {
5260
5292
  try {
5261
- ({ reportManifest: reporter } = await import("./refresh-manifest-JNCTIAM5.js"));
5293
+ ({ reportManifest: reporter } = await import("./refresh-manifest-LDS35UKV.js"));
5262
5294
  } catch (err) {
5263
5295
  notify(onError, err, "onError");
5264
5296
  }