@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 +22 -0
- package/dist/index.cjs +74 -12
- package/dist/index.js +73 -12
- package/package.json +1 -1
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
|
-
|
|
901
|
+
return rebuilt(res, body) ?? asRuntimeResponse(res);
|
|
902
|
+
}
|
|
903
|
+
function rebuilt(res, body) {
|
|
898
904
|
try {
|
|
899
|
-
|
|
905
|
+
return withUrl(new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers }), res.url);
|
|
900
906
|
} catch {
|
|
901
|
-
return
|
|
907
|
+
return void 0;
|
|
902
908
|
}
|
|
903
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
740
|
+
return rebuilt(res, body) ?? asRuntimeResponse(res);
|
|
741
|
+
}
|
|
742
|
+
function rebuilt(res, body) {
|
|
738
743
|
try {
|
|
739
|
-
|
|
744
|
+
return withUrl(new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers }), res.url);
|
|
740
745
|
} catch {
|
|
741
|
-
return
|
|
746
|
+
return void 0;
|
|
742
747
|
}
|
|
743
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|