@ai-sdk/provider-utils 4.0.40 → 4.0.42
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/CHANGELOG.md +12 -0
- package/dist/index.d.mts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +123 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +123 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/fetch-with-validated-redirects.ts +7 -0
- package/src/safe-node-fetch.ts +218 -0
- package/src/validate-download-url.ts +30 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @ai-sdk/provider-utils
|
|
2
2
|
|
|
3
|
+
## 4.0.42
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ee2bf30: fix(provider-utils): prevent Metro from parsing the Node 18 dynamic import fallback
|
|
8
|
+
|
|
9
|
+
## 4.0.41
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 9ecdefe: Prevent validated downloads on Node.js from reaching private or internal services through DNS aliases or DNS rebinding by validating and pinning every resolved address at connection time.
|
|
14
|
+
|
|
3
15
|
## 4.0.40
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -223,6 +223,11 @@ declare function readResponseWithSizeLimit({ response, url, maxBytes, }: {
|
|
|
223
223
|
* The returned response is the final (non-redirect) response. The caller is
|
|
224
224
|
* responsible for checking `response.ok` and reading the body.
|
|
225
225
|
*
|
|
226
|
+
* On Node.js, the default fetch resolves every hostname through a validating
|
|
227
|
+
* lookup hook and passes those exact addresses to the connector, preventing
|
|
228
|
+
* hostname-to-private-IP and DNS-rebinding bypasses. Other runtimes should
|
|
229
|
+
* constrain egress at the network layer when handling untrusted URLs.
|
|
230
|
+
*
|
|
226
231
|
* @throws DownloadError if a hop is unsafe, the redirect limit is exceeded, or
|
|
227
232
|
* a redirect cannot be validated on a non-browser runtime.
|
|
228
233
|
*/
|
|
@@ -1411,9 +1416,8 @@ declare function convertToBase64(value: string | Uint8Array): string;
|
|
|
1411
1416
|
* Validates that a URL is safe to download from, blocking private/internal addresses
|
|
1412
1417
|
* to prevent SSRF attacks.
|
|
1413
1418
|
*
|
|
1414
|
-
* Note: this performs string/literal-IP checks only.
|
|
1415
|
-
*
|
|
1416
|
-
* should additionally constrain egress at the network layer when handling untrusted URLs).
|
|
1419
|
+
* Note: this function performs string/literal-IP checks only. The Node.js
|
|
1420
|
+
* download fetch additionally validates and pins DNS results at connect time.
|
|
1417
1421
|
*
|
|
1418
1422
|
* @param url - The URL string to validate.
|
|
1419
1423
|
* @throws DownloadError if the URL is unsafe.
|
package/dist/index.d.ts
CHANGED
|
@@ -223,6 +223,11 @@ declare function readResponseWithSizeLimit({ response, url, maxBytes, }: {
|
|
|
223
223
|
* The returned response is the final (non-redirect) response. The caller is
|
|
224
224
|
* responsible for checking `response.ok` and reading the body.
|
|
225
225
|
*
|
|
226
|
+
* On Node.js, the default fetch resolves every hostname through a validating
|
|
227
|
+
* lookup hook and passes those exact addresses to the connector, preventing
|
|
228
|
+
* hostname-to-private-IP and DNS-rebinding bypasses. Other runtimes should
|
|
229
|
+
* constrain egress at the network layer when handling untrusted URLs.
|
|
230
|
+
*
|
|
226
231
|
* @throws DownloadError if a hop is unsafe, the redirect limit is exceeded, or
|
|
227
232
|
* a redirect cannot be validated on a non-browser runtime.
|
|
228
233
|
*/
|
|
@@ -1411,9 +1416,8 @@ declare function convertToBase64(value: string | Uint8Array): string;
|
|
|
1411
1416
|
* Validates that a URL is safe to download from, blocking private/internal addresses
|
|
1412
1417
|
* to prevent SSRF attacks.
|
|
1413
1418
|
*
|
|
1414
|
-
* Note: this performs string/literal-IP checks only.
|
|
1415
|
-
*
|
|
1416
|
-
* should additionally constrain egress at the network layer when handling untrusted URLs).
|
|
1419
|
+
* Note: this function performs string/literal-IP checks only. The Node.js
|
|
1420
|
+
* download fetch additionally validates and pins DNS results at connect time.
|
|
1417
1421
|
*
|
|
1418
1422
|
* @param url - The URL string to validate.
|
|
1419
1423
|
* @throws DownloadError if the URL is unsafe.
|
package/dist/index.js
CHANGED
|
@@ -402,6 +402,19 @@ function validateDownloadUrl(url) {
|
|
|
402
402
|
return;
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
|
+
function validateDownloadAddress({
|
|
406
|
+
address,
|
|
407
|
+
family,
|
|
408
|
+
hostname
|
|
409
|
+
}) {
|
|
410
|
+
const isUnsafe = family === 4 ? !isIPv4(address) || isPrivateIPv4(address) : family === 6 ? isPrivateIPv6(address) : true;
|
|
411
|
+
if (isUnsafe) {
|
|
412
|
+
throw new DownloadError({
|
|
413
|
+
url: hostname,
|
|
414
|
+
message: `Hostname ${hostname} resolved to disallowed IP address ${address}`
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
405
418
|
function isIPv4(hostname) {
|
|
406
419
|
const parts = hostname.split(".");
|
|
407
420
|
if (parts.length !== 4) return false;
|
|
@@ -488,6 +501,106 @@ function isPrivateIPv6(ip) {
|
|
|
488
501
|
return false;
|
|
489
502
|
}
|
|
490
503
|
|
|
504
|
+
// src/safe-node-fetch.ts
|
|
505
|
+
function createSafeLookup(lookup) {
|
|
506
|
+
return ((hostname, options, callback) => {
|
|
507
|
+
lookup(hostname, { ...options, all: true }, (error, addresses) => {
|
|
508
|
+
if (error) {
|
|
509
|
+
callback(error);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const [firstAddress] = addresses;
|
|
514
|
+
if (firstAddress == null) {
|
|
515
|
+
throw new Error(`Hostname ${hostname} did not resolve to an address`);
|
|
516
|
+
}
|
|
517
|
+
for (const { address, family } of addresses) {
|
|
518
|
+
validateDownloadAddress({ address, family, hostname });
|
|
519
|
+
}
|
|
520
|
+
if (options.all === true) {
|
|
521
|
+
callback(null, addresses);
|
|
522
|
+
} else {
|
|
523
|
+
callback(
|
|
524
|
+
null,
|
|
525
|
+
firstAddress.address,
|
|
526
|
+
firstAddress.family
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
} catch (error2) {
|
|
530
|
+
callback(
|
|
531
|
+
error2 instanceof Error ? error2 : new Error(String(error2))
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
var safeNodeFetchPromise;
|
|
538
|
+
var initialGlobalFetch = globalThis.fetch;
|
|
539
|
+
var initialGlobalFetchIsNodeDefault = isNodeDefaultFetch(initialGlobalFetch);
|
|
540
|
+
function isNodeRuntime() {
|
|
541
|
+
var _a2, _b2;
|
|
542
|
+
const runtimeProcess = globalThis.process;
|
|
543
|
+
return ((_a2 = runtimeProcess == null ? void 0 : runtimeProcess.release) == null ? void 0 : _a2.name) === "node" && ((_b2 = runtimeProcess.versions) == null ? void 0 : _b2.bun) == null;
|
|
544
|
+
}
|
|
545
|
+
async function getDefaultDownloadFetch() {
|
|
546
|
+
if (!isNodeRuntime() || !initialGlobalFetchIsNodeDefault || globalThis.fetch !== initialGlobalFetch) {
|
|
547
|
+
return globalThis.fetch;
|
|
548
|
+
}
|
|
549
|
+
return safeNodeFetchPromise != null ? safeNodeFetchPromise : safeNodeFetchPromise = createSafeNodeFetch();
|
|
550
|
+
}
|
|
551
|
+
function isNodeDefaultFetch(fetch) {
|
|
552
|
+
const source = Function.prototype.toString.call(fetch);
|
|
553
|
+
return source.includes("internal/deps/undici") || source.includes("lazy loading of undici");
|
|
554
|
+
}
|
|
555
|
+
async function createSafeNodeFetch() {
|
|
556
|
+
const [{ createRequire }, { lookup }] = await Promise.all([
|
|
557
|
+
loadNodeModule("node:module"),
|
|
558
|
+
loadNodeModule("node:dns")
|
|
559
|
+
]);
|
|
560
|
+
const { Agent, fetch } = createRequire(getCurrentModulePath())(
|
|
561
|
+
"undici"
|
|
562
|
+
);
|
|
563
|
+
const dispatcher = new Agent({
|
|
564
|
+
connect: {
|
|
565
|
+
lookup: createSafeLookup(lookup)
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
return ((input, init) => fetch(
|
|
569
|
+
input,
|
|
570
|
+
{
|
|
571
|
+
...init,
|
|
572
|
+
dispatcher
|
|
573
|
+
}
|
|
574
|
+
));
|
|
575
|
+
}
|
|
576
|
+
async function loadNodeModule(id) {
|
|
577
|
+
var _a2;
|
|
578
|
+
const processWithBuiltins = globalThis.process;
|
|
579
|
+
const builtinModule = (_a2 = processWithBuiltins == null ? void 0 : processWithBuiltins.getBuiltinModule) == null ? void 0 : _a2.call(processWithBuiltins, id);
|
|
580
|
+
return builtinModule == null ? await importNodeModule(id) : builtinModule;
|
|
581
|
+
}
|
|
582
|
+
var dynamicImport;
|
|
583
|
+
function importNodeModule(id) {
|
|
584
|
+
dynamicImport != null ? dynamicImport : dynamicImport = Function("specifier", "return import(specifier)");
|
|
585
|
+
return dynamicImport(id);
|
|
586
|
+
}
|
|
587
|
+
function getCurrentModulePath() {
|
|
588
|
+
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
589
|
+
try {
|
|
590
|
+
Error.prepareStackTrace = (_error, callSites) => callSites;
|
|
591
|
+
const error = new Error("Capture current module path");
|
|
592
|
+
Error.captureStackTrace(error, getCurrentModulePath);
|
|
593
|
+
const [caller] = error.stack;
|
|
594
|
+
const fileName = caller == null ? void 0 : caller.getFileName();
|
|
595
|
+
if (fileName == null) {
|
|
596
|
+
throw new Error("Unable to determine the current module path");
|
|
597
|
+
}
|
|
598
|
+
return fileName;
|
|
599
|
+
} finally {
|
|
600
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
491
604
|
// src/fetch-with-validated-redirects.ts
|
|
492
605
|
var MAX_DOWNLOAD_REDIRECTS = 10;
|
|
493
606
|
async function fetchWithValidatedRedirects({
|
|
@@ -503,6 +616,7 @@ async function fetchWithValidatedRedirects({
|
|
|
503
616
|
let currentUrl = url;
|
|
504
617
|
for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount++) {
|
|
505
618
|
validateDownloadUrl(currentUrl);
|
|
619
|
+
const fetch = await getDefaultDownloadFetch();
|
|
506
620
|
const response = await fetch(currentUrl, {
|
|
507
621
|
...baseInit,
|
|
508
622
|
redirect: "manual"
|
|
@@ -779,7 +893,7 @@ function withUserAgentSuffix(headers, ...userAgentSuffixParts) {
|
|
|
779
893
|
}
|
|
780
894
|
|
|
781
895
|
// src/version.ts
|
|
782
|
-
var VERSION = true ? "4.0.
|
|
896
|
+
var VERSION = true ? "4.0.42" : "0.0.0-test";
|
|
783
897
|
|
|
784
898
|
// src/get-from-api.ts
|
|
785
899
|
var getOriginalFetch = () => globalThis.fetch;
|
|
@@ -789,10 +903,10 @@ var getFromApi = async ({
|
|
|
789
903
|
successfulResponseHandler,
|
|
790
904
|
failedResponseHandler,
|
|
791
905
|
abortSignal,
|
|
792
|
-
fetch
|
|
906
|
+
fetch = getOriginalFetch()
|
|
793
907
|
}) => {
|
|
794
908
|
try {
|
|
795
|
-
const response = await
|
|
909
|
+
const response = await fetch(url, {
|
|
796
910
|
method: "GET",
|
|
797
911
|
headers: withUserAgentSuffix(
|
|
798
912
|
headers,
|
|
@@ -2544,7 +2658,7 @@ var postJsonToApi = async ({
|
|
|
2544
2658
|
failedResponseHandler,
|
|
2545
2659
|
successfulResponseHandler,
|
|
2546
2660
|
abortSignal,
|
|
2547
|
-
fetch
|
|
2661
|
+
fetch
|
|
2548
2662
|
}) => postToApi({
|
|
2549
2663
|
url,
|
|
2550
2664
|
headers: {
|
|
@@ -2558,7 +2672,7 @@ var postJsonToApi = async ({
|
|
|
2558
2672
|
failedResponseHandler,
|
|
2559
2673
|
successfulResponseHandler,
|
|
2560
2674
|
abortSignal,
|
|
2561
|
-
fetch
|
|
2675
|
+
fetch
|
|
2562
2676
|
});
|
|
2563
2677
|
var postFormDataToApi = async ({
|
|
2564
2678
|
url,
|
|
@@ -2567,7 +2681,7 @@ var postFormDataToApi = async ({
|
|
|
2567
2681
|
failedResponseHandler,
|
|
2568
2682
|
successfulResponseHandler,
|
|
2569
2683
|
abortSignal,
|
|
2570
|
-
fetch
|
|
2684
|
+
fetch
|
|
2571
2685
|
}) => postToApi({
|
|
2572
2686
|
url,
|
|
2573
2687
|
headers,
|
|
@@ -2578,7 +2692,7 @@ var postFormDataToApi = async ({
|
|
|
2578
2692
|
failedResponseHandler,
|
|
2579
2693
|
successfulResponseHandler,
|
|
2580
2694
|
abortSignal,
|
|
2581
|
-
fetch
|
|
2695
|
+
fetch
|
|
2582
2696
|
});
|
|
2583
2697
|
var postToApi = async ({
|
|
2584
2698
|
url,
|
|
@@ -2587,10 +2701,10 @@ var postToApi = async ({
|
|
|
2587
2701
|
successfulResponseHandler,
|
|
2588
2702
|
failedResponseHandler,
|
|
2589
2703
|
abortSignal,
|
|
2590
|
-
fetch
|
|
2704
|
+
fetch = getOriginalFetch2()
|
|
2591
2705
|
}) => {
|
|
2592
2706
|
try {
|
|
2593
|
-
const response = await
|
|
2707
|
+
const response = await fetch(url, {
|
|
2594
2708
|
method: "POST",
|
|
2595
2709
|
headers: withUserAgentSuffix(
|
|
2596
2710
|
headers,
|