@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
@@ -2913,7 +2913,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
2913
2913
  for (const moduleName of ["node:http", "node:https"]) {
2914
2914
  try {
2915
2915
  const mod = await import(moduleName);
2916
- const restore = patchHttpModule(mod.default ?? mod, block, screen);
2916
+ const restore = patchHttpModule(mod.default ?? mod, block, screen, skip);
2917
2917
  if (restore) restores.push(restore);
2918
2918
  } catch {
2919
2919
  }
@@ -2927,7 +2927,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
2927
2927
  }
2928
2928
  };
2929
2929
  }
2930
- function patchHttpModule(http, block, screen) {
2930
+ function patchHttpModule(http, block, screen, skip) {
2931
2931
  if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
2932
2932
  const originalRequest = http.request;
2933
2933
  const originalGet = http.get;
@@ -2938,7 +2938,7 @@ function patchHttpModule(http, block, screen) {
2938
2938
  }
2939
2939
  if (target && screen && target.host && screen.isIP(target.host) === 0 && !screen.isExempt(target.host)) {
2940
2940
  try {
2941
- args = withScreeningLookup(args, target, block, screen.lookup);
2941
+ args = withScreeningLookup(args, target, block, screen.lookup, skip);
2942
2942
  } catch {
2943
2943
  }
2944
2944
  }
@@ -2991,7 +2991,9 @@ function screenResolved(addresses, target, block) {
2991
2991
  }
2992
2992
  return null;
2993
2993
  }
2994
- function withScreeningLookup(args, target, block, lookup) {
2994
+ function withScreeningLookup(args, target, block, lookup, skip) {
2995
+ const own = callerLookup(args);
2996
+ const resolve = typeof own === "function" ? own : lookup;
2995
2997
  const screeningLookup = (hostname, options, callback) => {
2996
2998
  let opts = options;
2997
2999
  let cb = callback;
@@ -3000,29 +3002,43 @@ function withScreeningLookup(args, target, block, lookup) {
3000
3002
  opts = {};
3001
3003
  }
3002
3004
  if (!opts || typeof opts !== "object") opts = {};
3005
+ const handOver = (list) => {
3006
+ if (opts.all) return cb(null, list);
3007
+ const first = list[0];
3008
+ if (!first) return cb(new Error(`Patchstack: could not resolve ${hostname}`));
3009
+ return cb(null, first.address, first.family);
3010
+ };
3011
+ let answered = false;
3003
3012
  try {
3004
- lookup(hostname, { ...opts, all: true }, (err, addresses) => {
3013
+ resolve(hostname, { ...opts, all: true }, (err, addresses) => {
3014
+ answered = true;
3005
3015
  if (err) return cb(err);
3006
3016
  const list = Array.isArray(addresses) ? addresses : [];
3007
- const blocked = screenResolved(list, target, block);
3017
+ let blocked;
3018
+ try {
3019
+ blocked = screenResolved(list, target, block);
3020
+ } catch {
3021
+ skip("screen-failed", { host: hostname });
3022
+ return handOver(list);
3023
+ }
3008
3024
  if (blocked) {
3009
3025
  return cb(new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host} resolved to ${blocked}`));
3010
3026
  }
3011
- if (opts.all) return cb(null, list);
3012
- const first = list[0];
3013
- if (!first) return cb(new Error(`Patchstack: could not resolve ${hostname}`));
3014
- return cb(null, first.address, first.family);
3027
+ return handOver(list);
3015
3028
  });
3016
- } catch {
3017
- try {
3018
- lookup(hostname, opts, cb);
3019
- } catch {
3020
- cb(new Error(`Patchstack: lookup failed for ${hostname}`));
3021
- }
3029
+ } catch (err) {
3030
+ if (answered) throw err;
3031
+ return cb(err);
3022
3032
  }
3023
3033
  };
3024
3034
  return injectLookupOption(args, screeningLookup);
3025
3035
  }
3036
+ function callerLookup(args) {
3037
+ for (const arg of args) {
3038
+ if (arg && typeof arg === "object" && !(arg instanceof URL) && typeof arg.lookup === "function") return arg.lookup;
3039
+ }
3040
+ return void 0;
3041
+ }
3026
3042
  function injectLookupOption(args, lookup) {
3027
3043
  const first = args[0];
3028
3044
  if (first && typeof first === "object" && !(first instanceof URL)) {
@@ -4602,8 +4618,17 @@ function createServerFnGuard({ protection }) {
4602
4618
  return { rule: body.rule, message: body.message || "Blocked by Patchstack" };
4603
4619
  };
4604
4620
  }
4621
+ function listOption(value, fallback, name, onError) {
4622
+ if (value === void 0) return fallback;
4623
+ if (Array.isArray(value)) return value;
4624
+ notify(onError, new Error(`patchstack: \`${name}\` has to be an array; the value configured is ignored`), "onError");
4625
+ return fallback;
4626
+ }
4605
4627
  async function createProtection(options = {}) {
4606
4628
  const onError = options.onError;
4629
+ const allowHosts = listOption(options.allowHosts, [], "allowHosts", onError);
4630
+ const configuredResponseRules = listOption(options.responseRules, DEFAULT_RESPONSE_RULES, "responseRules", onError);
4631
+ const configuredEgressRules = listOption(options.egressRules, DEFAULT_EGRESS_RULES, "egressRules", onError);
4607
4632
  const userOnDetect = options.onDetect ?? defaultOnDetect;
4608
4633
  const apiKey = await resolveApiKey(options);
4609
4634
  const firewallLog = apiKey && telemetryEnabled() && options.reportFirewallLog !== false ? createFirewallLogReporter({
@@ -4703,9 +4728,9 @@ async function createProtection(options = {}) {
4703
4728
  const applyBundle = (delivered) => {
4704
4729
  const incoming = delivered.firewall ?? [];
4705
4730
  requestRules = byPhase(incoming, "request");
4706
- responseRules = [...options.responseRules ?? DEFAULT_RESPONSE_RULES, ...byPhase(incoming, "response")];
4731
+ responseRules = [...configuredResponseRules, ...byPhase(incoming, "response")];
4707
4732
  screenCap = responseScreenCap(responseRules);
4708
- egressRules = [...options.egressRules ?? DEFAULT_EGRESS_RULES, ...byPhase(incoming, "egress")];
4733
+ egressRules = [...configuredEgressRules, ...byPhase(incoming, "egress")];
4709
4734
  engine = new RuleEngine({
4710
4735
  firewall: requestRules,
4711
4736
  whitelists: delivered.whitelists,
@@ -5174,7 +5199,7 @@ async function createProtection(options = {}) {
5174
5199
  return origEnd(cb);
5175
5200
  };
5176
5201
  };
5177
- const allow = new Set((options.allowHosts ?? []).map((h) => String(h).toLowerCase()));
5202
+ const allow = new Set(allowHosts.map((h) => String(h).toLowerCase()));
5178
5203
  const egressShouldBlock = (url, host, method) => {
5179
5204
  if (host && allow.has(host.toLowerCase())) return false;
5180
5205
  let egressPath = null;
@@ -5226,10 +5251,17 @@ async function createProtection(options = {}) {
5226
5251
  return { request: requestRules, response: responseRules, egress: egressRules };
5227
5252
  },
5228
5253
  /**
5229
- * Enforcement coverage: how often the guard FAILED OPEN rather than inspecting, keyed
5230
- * `<phase>:<reason>` (e.g. `response:body-cap`, `request:body-cap`, `response:live-stream`,
5231
- * `egress:resolver-failed`). "Always-on" is not "always inspected" — surface this (or pass
5232
- * `onSkip`) so an unscreened path is visible and alertable rather than silent.
5254
+ * Enforcement coverage: how often the guard failed open rather than inspecting, in the cases it can
5255
+ * observe itself — keyed `<phase>:<reason>` (e.g. `response:body-cap`, `request:body-cap`,
5256
+ * `response:live-stream`, `egress:resolver-failed` for a fetch call whose address could not be
5257
+ * resolved). "Always-on" is not "always inspected", so surface this (or pass `onSkip`) rather than
5258
+ * assuming coverage.
5259
+ *
5260
+ * Not a total, in two ways. An outbound call can miss the address check without being counted:
5261
+ * whether it resolved through the screening resolver depends on the agent and the socket pool,
5262
+ * which this guard cannot observe. And a resolver failure on the node path appears nowhere here
5263
+ * because it is not a bypass — that resolver is the connection's, so the call fails instead.
5264
+ * Zero skips means nothing the guard can see was bypassed, not that nothing was.
5233
5265
  */
5234
5266
  coverage() {
5235
5267
  return { skipped: { ...skipCounts } };
@@ -5362,7 +5394,7 @@ async function createProtection(options = {}) {
5362
5394
  // same skip accounting as the request/response phases.
5363
5395
  onSkip: ({ reason, detail }) => recordSkip("egress", reason, detail),
5364
5396
  dnsScreen: options.screenDns !== false,
5365
- allowHosts: options.allowHosts
5397
+ allowHosts
5366
5398
  });
5367
5399
  }
5368
5400
  const cwd = options.cwd ?? (typeof process !== "undefined" ? process.cwd() : void 0);