@ai-sdk/provider-utils 3.0.30 → 3.0.32

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @ai-sdk/provider-utils
2
2
 
3
+ ## 3.0.32
4
+
5
+ ### Patch Changes
6
+
7
+ - 0e51b7b: Preserve streamed download size-limit errors when response cancellation fails.
8
+
9
+ ## 3.0.31
10
+
11
+ ### Patch Changes
12
+
13
+ - 7a6bdbc: 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
  ## 3.0.30
4
16
 
5
17
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -92,6 +92,11 @@ declare class DownloadError extends AISDKError {
92
92
  * The returned response is the final (non-redirect) response. The caller is
93
93
  * responsible for checking `response.ok` and reading the body.
94
94
  *
95
+ * On Node.js, the default fetch resolves every hostname through a validating
96
+ * lookup hook and passes those exact addresses to the connector, preventing
97
+ * hostname-to-private-IP and DNS-rebinding bypasses. Other runtimes should
98
+ * constrain egress at the network layer when handling untrusted URLs.
99
+ *
95
100
  * @throws DownloadError if a hop is unsafe, the redirect limit is exceeded, or
96
101
  * a redirect cannot be validated on a non-browser runtime.
97
102
  */
@@ -945,9 +950,8 @@ declare function convertToBase64(value: string | Uint8Array): string;
945
950
  * Validates that a URL is safe to download from, blocking private/internal addresses
946
951
  * to prevent SSRF attacks.
947
952
  *
948
- * Note: this performs string/literal-IP checks only. It does not resolve DNS, so a
949
- * hostname that resolves to a private address is not blocked here (see callers, which
950
- * should additionally constrain egress at the network layer when handling untrusted URLs).
953
+ * Note: this function performs string/literal-IP checks only. The Node.js
954
+ * download fetch additionally validates and pins DNS results at connect time.
951
955
  *
952
956
  * @param url - The URL string to validate.
953
957
  * @throws DownloadError if the URL is unsafe.
package/dist/index.d.ts CHANGED
@@ -92,6 +92,11 @@ declare class DownloadError extends AISDKError {
92
92
  * The returned response is the final (non-redirect) response. The caller is
93
93
  * responsible for checking `response.ok` and reading the body.
94
94
  *
95
+ * On Node.js, the default fetch resolves every hostname through a validating
96
+ * lookup hook and passes those exact addresses to the connector, preventing
97
+ * hostname-to-private-IP and DNS-rebinding bypasses. Other runtimes should
98
+ * constrain egress at the network layer when handling untrusted URLs.
99
+ *
95
100
  * @throws DownloadError if a hop is unsafe, the redirect limit is exceeded, or
96
101
  * a redirect cannot be validated on a non-browser runtime.
97
102
  */
@@ -945,9 +950,8 @@ declare function convertToBase64(value: string | Uint8Array): string;
945
950
  * Validates that a URL is safe to download from, blocking private/internal addresses
946
951
  * to prevent SSRF attacks.
947
952
  *
948
- * Note: this performs string/literal-IP checks only. It does not resolve DNS, so a
949
- * hostname that resolves to a private address is not blocked here (see callers, which
950
- * should additionally constrain egress at the network layer when handling untrusted URLs).
953
+ * Note: this function performs string/literal-IP checks only. The Node.js
954
+ * download fetch additionally validates and pins DNS results at connect time.
951
955
  *
952
956
  * @param url - The URL string to validate.
953
957
  * @throws DownloadError if the URL is unsafe.
package/dist/index.js CHANGED
@@ -322,6 +322,19 @@ function validateDownloadUrl(url) {
322
322
  return;
323
323
  }
324
324
  }
325
+ function validateDownloadAddress({
326
+ address,
327
+ family,
328
+ hostname
329
+ }) {
330
+ const isUnsafe = family === 4 ? !isIPv4(address) || isPrivateIPv4(address) : family === 6 ? isPrivateIPv6(address) : true;
331
+ if (isUnsafe) {
332
+ throw new DownloadError({
333
+ url: hostname,
334
+ message: `Hostname ${hostname} resolved to disallowed IP address ${address}`
335
+ });
336
+ }
337
+ }
325
338
  function isIPv4(hostname) {
326
339
  const parts = hostname.split(".");
327
340
  if (parts.length !== 4) return false;
@@ -408,6 +421,104 @@ function isPrivateIPv6(ip) {
408
421
  return false;
409
422
  }
410
423
 
424
+ // src/safe-node-fetch.ts
425
+ function createSafeLookup(lookup) {
426
+ return ((hostname, options, callback) => {
427
+ lookup(hostname, { ...options, all: true }, (error, addresses) => {
428
+ if (error) {
429
+ callback(error);
430
+ return;
431
+ }
432
+ try {
433
+ const [firstAddress] = addresses;
434
+ if (firstAddress == null) {
435
+ throw new Error(`Hostname ${hostname} did not resolve to an address`);
436
+ }
437
+ for (const { address, family } of addresses) {
438
+ validateDownloadAddress({ address, family, hostname });
439
+ }
440
+ if (options.all === true) {
441
+ callback(null, addresses);
442
+ } else {
443
+ callback(
444
+ null,
445
+ firstAddress.address,
446
+ firstAddress.family
447
+ );
448
+ }
449
+ } catch (error2) {
450
+ callback(
451
+ error2 instanceof Error ? error2 : new Error(String(error2))
452
+ );
453
+ }
454
+ });
455
+ });
456
+ }
457
+ var safeNodeFetchPromise;
458
+ var initialGlobalFetch = globalThis.fetch;
459
+ var initialGlobalFetchIsNodeDefault = isNodeDefaultFetch(initialGlobalFetch);
460
+ function isNodeRuntime() {
461
+ var _a2, _b2;
462
+ const runtimeProcess = globalThis.process;
463
+ return ((_a2 = runtimeProcess == null ? void 0 : runtimeProcess.release) == null ? void 0 : _a2.name) === "node" && ((_b2 = runtimeProcess.versions) == null ? void 0 : _b2.bun) == null;
464
+ }
465
+ async function getDefaultDownloadFetch() {
466
+ if (!isNodeRuntime() || !initialGlobalFetchIsNodeDefault || globalThis.fetch !== initialGlobalFetch) {
467
+ return globalThis.fetch;
468
+ }
469
+ return safeNodeFetchPromise != null ? safeNodeFetchPromise : safeNodeFetchPromise = createSafeNodeFetch();
470
+ }
471
+ function isNodeDefaultFetch(fetch) {
472
+ const source = Function.prototype.toString.call(fetch);
473
+ return source.includes("internal/deps/undici") || source.includes("lazy loading of undici");
474
+ }
475
+ async function createSafeNodeFetch() {
476
+ const [{ createRequire }, { lookup }] = await Promise.all([
477
+ loadNodeModule("node:module"),
478
+ loadNodeModule("node:dns")
479
+ ]);
480
+ const { Agent, fetch } = createRequire(getCurrentModulePath())(
481
+ "undici"
482
+ );
483
+ const dispatcher = new Agent({
484
+ connect: {
485
+ lookup: createSafeLookup(lookup)
486
+ }
487
+ });
488
+ return ((input, init) => fetch(
489
+ input,
490
+ {
491
+ ...init,
492
+ dispatcher
493
+ }
494
+ ));
495
+ }
496
+ async function loadNodeModule(id) {
497
+ var _a2;
498
+ const processWithBuiltins = globalThis.process;
499
+ const builtinModule = (_a2 = processWithBuiltins == null ? void 0 : processWithBuiltins.getBuiltinModule) == null ? void 0 : _a2.call(processWithBuiltins, id);
500
+ return builtinModule == null ? await importNodeModule(id) : builtinModule;
501
+ }
502
+ function importNodeModule(id) {
503
+ return import(id);
504
+ }
505
+ function getCurrentModulePath() {
506
+ const originalPrepareStackTrace = Error.prepareStackTrace;
507
+ try {
508
+ Error.prepareStackTrace = (_error, callSites) => callSites;
509
+ const error = new Error("Capture current module path");
510
+ Error.captureStackTrace(error, getCurrentModulePath);
511
+ const [caller] = error.stack;
512
+ const fileName = caller == null ? void 0 : caller.getFileName();
513
+ if (fileName == null) {
514
+ throw new Error("Unable to determine the current module path");
515
+ }
516
+ return fileName;
517
+ } finally {
518
+ Error.prepareStackTrace = originalPrepareStackTrace;
519
+ }
520
+ }
521
+
411
522
  // src/fetch-with-validated-redirects.ts
412
523
  var MAX_DOWNLOAD_REDIRECTS = 10;
413
524
  async function fetchWithValidatedRedirects({
@@ -423,6 +534,7 @@ async function fetchWithValidatedRedirects({
423
534
  let currentUrl = url;
424
535
  for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount++) {
425
536
  validateDownloadUrl(currentUrl);
537
+ const fetch = await getDefaultDownloadFetch();
426
538
  const response = await fetch(currentUrl, {
427
539
  ...baseInit,
428
540
  redirect: "manual"
@@ -493,6 +605,7 @@ async function readResponseWithSizeLimit({
493
605
  } finally {
494
606
  try {
495
607
  await reader.cancel();
608
+ } catch (e) {
496
609
  } finally {
497
610
  reader.releaseLock();
498
611
  }
@@ -640,7 +753,7 @@ function withUserAgentSuffix(headers, ...userAgentSuffixParts) {
640
753
  }
641
754
 
642
755
  // src/version.ts
643
- var VERSION = true ? "3.0.30" : "0.0.0-test";
756
+ var VERSION = true ? "3.0.32" : "0.0.0-test";
644
757
 
645
758
  // src/get-from-api.ts
646
759
  var getOriginalFetch = () => globalThis.fetch;
@@ -650,10 +763,10 @@ var getFromApi = async ({
650
763
  successfulResponseHandler,
651
764
  failedResponseHandler,
652
765
  abortSignal,
653
- fetch: fetch2 = getOriginalFetch()
766
+ fetch = getOriginalFetch()
654
767
  }) => {
655
768
  try {
656
- const response = await fetch2(url, {
769
+ const response = await fetch(url, {
657
770
  method: "GET",
658
771
  headers: withUserAgentSuffix(
659
772
  headers,
@@ -1099,7 +1212,7 @@ var postJsonToApi = async ({
1099
1212
  failedResponseHandler,
1100
1213
  successfulResponseHandler,
1101
1214
  abortSignal,
1102
- fetch: fetch2
1215
+ fetch
1103
1216
  }) => postToApi({
1104
1217
  url,
1105
1218
  headers: {
@@ -1113,7 +1226,7 @@ var postJsonToApi = async ({
1113
1226
  failedResponseHandler,
1114
1227
  successfulResponseHandler,
1115
1228
  abortSignal,
1116
- fetch: fetch2
1229
+ fetch
1117
1230
  });
1118
1231
  var postFormDataToApi = async ({
1119
1232
  url,
@@ -1122,7 +1235,7 @@ var postFormDataToApi = async ({
1122
1235
  failedResponseHandler,
1123
1236
  successfulResponseHandler,
1124
1237
  abortSignal,
1125
- fetch: fetch2
1238
+ fetch
1126
1239
  }) => postToApi({
1127
1240
  url,
1128
1241
  headers,
@@ -1133,7 +1246,7 @@ var postFormDataToApi = async ({
1133
1246
  failedResponseHandler,
1134
1247
  successfulResponseHandler,
1135
1248
  abortSignal,
1136
- fetch: fetch2
1249
+ fetch
1137
1250
  });
1138
1251
  var postToApi = async ({
1139
1252
  url,
@@ -1142,10 +1255,10 @@ var postToApi = async ({
1142
1255
  successfulResponseHandler,
1143
1256
  failedResponseHandler,
1144
1257
  abortSignal,
1145
- fetch: fetch2 = getOriginalFetch2()
1258
+ fetch = getOriginalFetch2()
1146
1259
  }) => {
1147
1260
  try {
1148
- const response = await fetch2(url, {
1261
+ const response = await fetch(url, {
1149
1262
  method: "POST",
1150
1263
  headers: withUserAgentSuffix(
1151
1264
  headers,