@revenexx/integrations-node-sdk 1.14.0 → 1.15.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.
package/dist/fetch.d.ts CHANGED
@@ -65,6 +65,28 @@ export interface SafeFetchOptions extends RequestInit {
65
65
  /** Bind the request to the origin of the credential it carries. */
66
66
  credential?: SafeFetchCredential;
67
67
  }
68
+ /**
69
+ * Whether the request behind a failed {@link safeFetch} call may have reached
70
+ * the host — `false` only when the call is known to have put nothing on the wire
71
+ * on any hop of any attempt, and `true` for every other error, an unknown one
72
+ * included. A node that writes reads it to decide whether a create may be
73
+ * repeated: when this is `false` nothing was sent, so nothing can have been
74
+ * created.
75
+ *
76
+ * It reads `meta.sent === false`, which `safeFetch` puts on what it throws in
77
+ * that case: a refused address — before the lookup or at connect time — a lookup
78
+ * that ran out of time, a header or URL that cannot be sent, a credential bound
79
+ * to another origin, and a connection that was refused, could not be resolved or
80
+ * reached, or timed out while connecting. The last group arrives as the runtime's
81
+ * own `TypeError('fetch failed')`, unwrapped as before, with the marker added. A
82
+ * cancellation is the caller's own reason and is never marked.
83
+ */
84
+ export declare function mayHaveBeenSent(err: unknown): boolean;
85
+ /**
86
+ * Make a request through the SSRF guard, under a time budget and an optional
87
+ * retry budget. A failure that left nothing on the wire says so with
88
+ * `meta.sent = false` — read it through {@link mayHaveBeenSent}.
89
+ */
68
90
  export declare function safeFetch(url: string | URL, options?: SafeFetchOptions): Promise<Response>;
69
91
  /**
70
92
  * Read a response body into an ArrayBuffer while enforcing a hard byte cap.
package/dist/index.cjs CHANGED
@@ -83,6 +83,7 @@ __export(index_exports, {
83
83
  isOperator: () => isOperator,
84
84
  isVpnErrorCode: () => isVpnErrorCode,
85
85
  maxBytesConfigField: () => maxBytesConfigField,
86
+ mayHaveBeenSent: () => mayHaveBeenSent,
86
87
  nodeManifestProblems: () => nodeManifestProblems,
87
88
  normalizeCredentialType: () => normalizeCredentialType,
88
89
  normalizeLocalized: () => normalizeLocalized,
@@ -377,6 +378,9 @@ function assertBranchNamed(node, result, config) {
377
378
  }
378
379
  }
379
380
 
381
+ // src/fetch.ts
382
+ var import_undici2 = require("undici");
383
+
380
384
  // src/ssrf.ts
381
385
  var import_node_diagnostics_channel = require("diagnostics_channel");
382
386
  var import_node_dns = __toESM(require("dns"), 1);
@@ -868,7 +872,7 @@ function attemptFailure(err, ctxSignal, deadline) {
868
872
  return err;
869
873
  }
870
874
  function answerUnderDeadline(res, ctxSignal, deadline) {
871
- if (!res.body) return res;
875
+ if (!res.body) return rebuilt(res, null) ?? asRuntimeResponse(res);
872
876
  let reader;
873
877
  const body = new ReadableStream(
874
878
  {
@@ -894,13 +898,27 @@ function answerUnderDeadline(res, ctxSignal, deadline) {
894
898
  // a buffer filling while nobody reads.
895
899
  { highWaterMark: 0 }
896
900
  );
897
- let answer;
901
+ return rebuilt(res, body) ?? asRuntimeResponse(res);
902
+ }
903
+ function rebuilt(res, body) {
898
904
  try {
899
- answer = new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
905
+ return withUrl(new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers }), res.url);
900
906
  } catch {
901
- return res;
907
+ return void 0;
902
908
  }
903
- return withUrl(answer, res.url);
909
+ }
910
+ function asRuntimeResponse(res) {
911
+ return withUrl(withStatusLine(new Response(res.body, { headers: res.headers }), res), res.url);
912
+ }
913
+ function withStatusLine(answer, from) {
914
+ const clone = answer.clone.bind(answer);
915
+ Object.defineProperties(answer, {
916
+ status: { value: from.status },
917
+ statusText: { value: from.statusText },
918
+ ok: { value: from.ok },
919
+ clone: { value: () => withStatusLine(clone(), from), configurable: true }
920
+ });
921
+ return answer;
904
922
  }
905
923
  function withUrl(answer, url) {
906
924
  const clone = answer.clone.bind(answer);
@@ -908,12 +926,46 @@ function withUrl(answer, url) {
908
926
  Object.defineProperty(answer, "clone", { value: () => withUrl(clone(), url) });
909
927
  return answer;
910
928
  }
911
- async function timedFetch(url, fetchOptions, ctxSignal, deadline) {
929
+ var runtimeFetch = globalThis.fetch;
930
+ function transport() {
931
+ const current = globalThis.fetch;
932
+ return current === runtimeFetch ? import_undici2.fetch : current;
933
+ }
934
+ var NEVER_CONNECTED = /* @__PURE__ */ new Set(["ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"]);
935
+ var REFUSED_WHILE_CONNECTING = /* @__PURE__ */ new Set(["ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH"]);
936
+ function connectFailed(e) {
937
+ if (typeof e?.code !== "string") return false;
938
+ return NEVER_CONNECTED.has(e.code) || REFUSED_WHILE_CONNECTING.has(e.code) && e.syscall === "connect";
939
+ }
940
+ function neverConnected(err) {
941
+ const cause = err?.cause;
942
+ if (!cause) return false;
943
+ if (Array.isArray(cause.errors) && cause.errors.length > 0) return cause.errors.every(connectFailed);
944
+ return connectFailed(cause);
945
+ }
946
+ function markUnsent(err) {
947
+ if (typeof err !== "object" || err === null) return;
948
+ const meta = err.meta;
949
+ const sent = { ...typeof meta === "object" && meta !== null ? meta : {}, sent: false };
950
+ try {
951
+ Object.defineProperty(err, "meta", { value: sent, writable: true, configurable: true, enumerable: true });
952
+ } catch {
953
+ }
954
+ }
955
+ function mayHaveBeenSent(err) {
956
+ const meta = err?.meta;
957
+ return meta?.sent !== false;
958
+ }
959
+ async function timedFetch(url, fetchOptions, ctxSignal, deadline, wire) {
912
960
  try {
913
- return await fetch(url, { ...fetchOptions, signal: deadline.signal, dispatcher: guardedDispatcher() });
961
+ const send = transport();
962
+ const res = await send(url, { ...fetchOptions, signal: deadline.signal, dispatcher: guardedDispatcher() });
963
+ wire.maybeSent = true;
964
+ return res;
914
965
  } catch (err) {
915
966
  const blocked = connectionRefusal(err);
916
967
  if (blocked) throw blocked;
968
+ if (!neverConnected(err)) wire.maybeSent = true;
917
969
  throw attemptFailure(err, ctxSignal, deadline);
918
970
  }
919
971
  }
@@ -944,10 +996,10 @@ function refuseUserinfo(url, status = 0) {
944
996
  );
945
997
  }
946
998
  }
947
- async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding) {
999
+ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, wire, binding) {
948
1000
  const deadline = attemptDeadline(ctxSignal, effectiveMs);
949
1001
  try {
950
- const res = await followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, binding);
1002
+ const res = await followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, wire, binding);
951
1003
  deadline.clear();
952
1004
  return answerUnderDeadline(res, ctxSignal, deadline);
953
1005
  } catch (err) {
@@ -955,7 +1007,7 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding)
955
1007
  throw err;
956
1008
  }
957
1009
  }
958
- async function followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, binding) {
1010
+ async function followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, wire, binding) {
959
1011
  let currentUrl = url;
960
1012
  let currentInit = { ...fetchOptions, redirect: "manual" };
961
1013
  await guardUrl(currentUrl, ctxSignal, deadline, effectiveMs);
@@ -965,7 +1017,7 @@ async function followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, b
965
1017
  );
966
1018
  let res;
967
1019
  try {
968
- res = await timedFetch(currentUrl, currentInit, ctxSignal, deadline);
1020
+ res = await timedFetch(currentUrl, currentInit, ctxSignal, deadline, wire);
969
1021
  } finally {
970
1022
  release();
971
1023
  }
@@ -1020,6 +1072,15 @@ async function followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, b
1020
1072
  }
1021
1073
  }
1022
1074
  async function safeFetch(url, options = {}) {
1075
+ const wire = { maybeSent: false };
1076
+ try {
1077
+ return await sendWithin(url, options, wire);
1078
+ } catch (err) {
1079
+ if (!wire.maybeSent && err !== options.signal?.reason) markUnsent(err);
1080
+ throw err;
1081
+ }
1082
+ }
1083
+ async function sendWithin(url, options, wire) {
1023
1084
  const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, credential, ...fetchOptions } = options;
1024
1085
  const effectiveMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
1025
1086
  const rawAttempts = retry?.attempts ?? DEFAULT_RETRY_ATTEMPTS;
@@ -1044,7 +1105,7 @@ async function safeFetch(url, options = {}) {
1044
1105
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1045
1106
  if (ctxSignal?.aborted) throw ctxSignal.reason;
1046
1107
  try {
1047
- return await guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding);
1108
+ return await guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, wire, binding);
1048
1109
  } catch (err) {
1049
1110
  if (ctxSignal?.aborted) throw ctxSignal.reason;
1050
1111
  if (err instanceof NodeError && (err.code === "BLOCKED_ADDRESS" || err.code === "TOO_MANY_REDIRECTS" || err.code === "CREDENTIAL_ORIGIN" || err.code === "INVALID_URL")) {
@@ -1660,6 +1721,7 @@ function base64Url(buf) {
1660
1721
  isOperator,
1661
1722
  isVpnErrorCode,
1662
1723
  maxBytesConfigField,
1724
+ mayHaveBeenSent,
1663
1725
  nodeManifestProblems,
1664
1726
  normalizeCredentialType,
1665
1727
  normalizeLocalized,
package/dist/index.js CHANGED
@@ -217,6 +217,9 @@ function errorPort(opts = {}) {
217
217
  };
218
218
  }
219
219
 
220
+ // src/fetch.ts
221
+ import { fetch as undiciFetch } from "undici";
222
+
220
223
  // src/ssrf.ts
221
224
  import { subscribe } from "diagnostics_channel";
222
225
  import dns from "dns";
@@ -708,7 +711,7 @@ function attemptFailure(err, ctxSignal, deadline) {
708
711
  return err;
709
712
  }
710
713
  function answerUnderDeadline(res, ctxSignal, deadline) {
711
- if (!res.body) return res;
714
+ if (!res.body) return rebuilt(res, null) ?? asRuntimeResponse(res);
712
715
  let reader;
713
716
  const body = new ReadableStream(
714
717
  {
@@ -734,13 +737,27 @@ function answerUnderDeadline(res, ctxSignal, deadline) {
734
737
  // a buffer filling while nobody reads.
735
738
  { highWaterMark: 0 }
736
739
  );
737
- let answer;
740
+ return rebuilt(res, body) ?? asRuntimeResponse(res);
741
+ }
742
+ function rebuilt(res, body) {
738
743
  try {
739
- answer = new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
744
+ return withUrl(new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers }), res.url);
740
745
  } catch {
741
- return res;
746
+ return void 0;
742
747
  }
743
- return withUrl(answer, res.url);
748
+ }
749
+ function asRuntimeResponse(res) {
750
+ return withUrl(withStatusLine(new Response(res.body, { headers: res.headers }), res), res.url);
751
+ }
752
+ function withStatusLine(answer, from) {
753
+ const clone = answer.clone.bind(answer);
754
+ Object.defineProperties(answer, {
755
+ status: { value: from.status },
756
+ statusText: { value: from.statusText },
757
+ ok: { value: from.ok },
758
+ clone: { value: () => withStatusLine(clone(), from), configurable: true }
759
+ });
760
+ return answer;
744
761
  }
745
762
  function withUrl(answer, url) {
746
763
  const clone = answer.clone.bind(answer);
@@ -748,12 +765,46 @@ function withUrl(answer, url) {
748
765
  Object.defineProperty(answer, "clone", { value: () => withUrl(clone(), url) });
749
766
  return answer;
750
767
  }
751
- async function timedFetch(url, fetchOptions, ctxSignal, deadline) {
768
+ var runtimeFetch = globalThis.fetch;
769
+ function transport() {
770
+ const current = globalThis.fetch;
771
+ return current === runtimeFetch ? undiciFetch : current;
772
+ }
773
+ var NEVER_CONNECTED = /* @__PURE__ */ new Set(["ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"]);
774
+ var REFUSED_WHILE_CONNECTING = /* @__PURE__ */ new Set(["ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH"]);
775
+ function connectFailed(e) {
776
+ if (typeof e?.code !== "string") return false;
777
+ return NEVER_CONNECTED.has(e.code) || REFUSED_WHILE_CONNECTING.has(e.code) && e.syscall === "connect";
778
+ }
779
+ function neverConnected(err) {
780
+ const cause = err?.cause;
781
+ if (!cause) return false;
782
+ if (Array.isArray(cause.errors) && cause.errors.length > 0) return cause.errors.every(connectFailed);
783
+ return connectFailed(cause);
784
+ }
785
+ function markUnsent(err) {
786
+ if (typeof err !== "object" || err === null) return;
787
+ const meta = err.meta;
788
+ const sent = { ...typeof meta === "object" && meta !== null ? meta : {}, sent: false };
789
+ try {
790
+ Object.defineProperty(err, "meta", { value: sent, writable: true, configurable: true, enumerable: true });
791
+ } catch {
792
+ }
793
+ }
794
+ function mayHaveBeenSent(err) {
795
+ const meta = err?.meta;
796
+ return meta?.sent !== false;
797
+ }
798
+ async function timedFetch(url, fetchOptions, ctxSignal, deadline, wire) {
752
799
  try {
753
- return await fetch(url, { ...fetchOptions, signal: deadline.signal, dispatcher: guardedDispatcher() });
800
+ const send = transport();
801
+ const res = await send(url, { ...fetchOptions, signal: deadline.signal, dispatcher: guardedDispatcher() });
802
+ wire.maybeSent = true;
803
+ return res;
754
804
  } catch (err) {
755
805
  const blocked = connectionRefusal(err);
756
806
  if (blocked) throw blocked;
807
+ if (!neverConnected(err)) wire.maybeSent = true;
757
808
  throw attemptFailure(err, ctxSignal, deadline);
758
809
  }
759
810
  }
@@ -784,10 +835,10 @@ function refuseUserinfo(url, status = 0) {
784
835
  );
785
836
  }
786
837
  }
787
- async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding) {
838
+ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, wire, binding) {
788
839
  const deadline = attemptDeadline(ctxSignal, effectiveMs);
789
840
  try {
790
- const res = await followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, binding);
841
+ const res = await followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, wire, binding);
791
842
  deadline.clear();
792
843
  return answerUnderDeadline(res, ctxSignal, deadline);
793
844
  } catch (err) {
@@ -795,7 +846,7 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding)
795
846
  throw err;
796
847
  }
797
848
  }
798
- async function followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, binding) {
849
+ async function followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, wire, binding) {
799
850
  let currentUrl = url;
800
851
  let currentInit = { ...fetchOptions, redirect: "manual" };
801
852
  await guardUrl(currentUrl, ctxSignal, deadline, effectiveMs);
@@ -805,7 +856,7 @@ async function followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, b
805
856
  );
806
857
  let res;
807
858
  try {
808
- res = await timedFetch(currentUrl, currentInit, ctxSignal, deadline);
859
+ res = await timedFetch(currentUrl, currentInit, ctxSignal, deadline, wire);
809
860
  } finally {
810
861
  release();
811
862
  }
@@ -860,6 +911,15 @@ async function followHops(url, fetchOptions, ctxSignal, deadline, effectiveMs, b
860
911
  }
861
912
  }
862
913
  async function safeFetch(url, options = {}) {
914
+ const wire = { maybeSent: false };
915
+ try {
916
+ return await sendWithin(url, options, wire);
917
+ } catch (err) {
918
+ if (!wire.maybeSent && err !== options.signal?.reason) markUnsent(err);
919
+ throw err;
920
+ }
921
+ }
922
+ async function sendWithin(url, options, wire) {
863
923
  const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, credential, ...fetchOptions } = options;
864
924
  const effectiveMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
865
925
  const rawAttempts = retry?.attempts ?? DEFAULT_RETRY_ATTEMPTS;
@@ -884,7 +944,7 @@ async function safeFetch(url, options = {}) {
884
944
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
885
945
  if (ctxSignal?.aborted) throw ctxSignal.reason;
886
946
  try {
887
- return await guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding);
947
+ return await guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, wire, binding);
888
948
  } catch (err) {
889
949
  if (ctxSignal?.aborted) throw ctxSignal.reason;
890
950
  if (err instanceof NodeError && (err.code === "BLOCKED_ADDRESS" || err.code === "TOO_MANY_REDIRECTS" || err.code === "CREDENTIAL_ORIGIN" || err.code === "INVALID_URL")) {
@@ -1417,6 +1477,7 @@ export {
1417
1477
  isOperator,
1418
1478
  isVpnErrorCode,
1419
1479
  maxBytesConfigField,
1480
+ mayHaveBeenSent,
1420
1481
  nodeManifestProblems,
1421
1482
  normalizeCredentialType,
1422
1483
  normalizeLocalized,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/integrations-node-sdk",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "TypeScript interfaces and utilities for Revenexx integration nodes",
5
5
  "license": "MIT",
6
6
  "author": "revenexx GmbH",