@ai-sdk/provider-utils 5.0.0-canary.47 → 5.0.0-canary.48

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,30 @@
1
1
  # @ai-sdk/provider-utils
2
2
 
3
+ ## 5.0.0-canary.48
4
+
5
+ ### Patch Changes
6
+
7
+ - aeda373: fix: only send provider credentials to same-origin response-supplied URLs
8
+
9
+ Several provider clients followed a URL taken from the provider's API response (a polling/status URL or a final media URL such as `polling_url`, `urls.get`, `result_url`, `result.sample`, or `video.uri`) and reused the authenticated headers — or appended `?key=<API_KEY>` — on that request. Because the host of the response-supplied URL was never validated, the long-lived API key was sent to whatever host the response named (a CDN in the benign case, or an attacker-chosen host if the provider response was tampered with), allowing credential exfiltration.
10
+
11
+ A new `isSameOrigin` helper is added to `@ai-sdk/provider-utils`, and the affected fetches in `@ai-sdk/black-forest-labs`, `@ai-sdk/fireworks`, `@ai-sdk/replicate`, `@ai-sdk/gladia`, `@ai-sdk/fal`, and `@ai-sdk/google` now attach credentials only when the followed URL is same-origin with the provider's configured API origin. Requests to a foreign origin are made without the credential.
12
+
13
+ - 375fdd7: fix: harden download URL SSRF guard against hostname and redirect bypasses
14
+
15
+ `validateDownloadUrl` and the file download helpers (`downloadBlob`, `download`) could be bypassed in several ways when handling untrusted URLs:
16
+
17
+ - A fully-qualified hostname with a trailing dot (e.g. `localhost.`, `myhost.local.`) skipped the localhost/`.local` blocklist.
18
+ - IPv6 addresses that embed an IPv4 address in their last 32 bits — IPv4-compatible (`::127.0.0.1`), IPv4-translated (`::ffff:0:127.0.0.1`), and NAT64 (`64:ff9b::127.0.0.1`, including the `64:ff9b:1::/48` local-use prefix) — were not decoded and checked against the private IPv4 ranges.
19
+ - Redirects were validated only _after_ `fetch` had already followed them, so the request to a redirect target (e.g. an internal/metadata address) had already been issued before the check ran.
20
+ - Several reserved/internal address ranges were not blocked: CGNAT (`100.64.0.0/10`, used by some cloud providers for internal traffic), benchmarking (`198.18.0.0/15`), IETF protocol assignments (`192.0.0.0/24`), the reserved `240.0.0.0/4` block (including the `255.255.255.255` broadcast address), and IPv6 site-local (`fec0::/10`) and multicast (`ff00::/8`).
21
+
22
+ The validator now strips trailing dots before the hostname checks and fully expands IPv6 addresses to detect embedded private IPv4 targets. The download helpers now follow redirects manually (`redirect: 'manual'`), re-validating each hop before requesting it, so an unsafe redirect target is never fetched. When a redirect cannot be inspected because the runtime returns an opaque response, the helpers fail closed (reject the redirect) on the server; only in a real browser — where SSRF is not reachable (fetch is constrained by CORS and cannot reach a server's internal network or cloud-metadata endpoints) — is the redirect followed natively so legitimate redirected downloads keep working.
23
+
24
+ - b4507d5: fix(provider-utils): cancel response body on download rejection to prevent socket leak
25
+
26
+ When a download was rejected early — because the `Content-Length` header exceeded the size limit, the response status was not ok, or a redirect resolved to a blocked URL — the fetch response body was left unconsumed and uncancelled. With WHATWG Fetch/undici this leaves the underlying TCP socket open instead of returning it to the connection pool, allowing an attacker-controlled origin to exhaust file descriptors and cause a denial of service. The body is now cancelled on all early-rejection paths in `readResponseWithSizeLimit`, `download`, and `downloadBlob`, and `fetchWithValidatedRedirects` cancels each redirect hop's body before following or rejecting the next hop.
27
+
3
28
  ## 5.0.0-canary.47
4
29
 
5
30
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -738,6 +738,36 @@ declare class DownloadError extends AISDKError {
738
738
  static isInstance(error: unknown): error is DownloadError;
739
739
  }
740
740
 
741
+ /**
742
+ * Fetches a URL while enforcing the SSRF download guard on every hop.
743
+ *
744
+ * Redirects are followed manually (`redirect: 'manual'`) so each hop is
745
+ * validated with {@link validateDownloadUrl} *before* it is requested. Relying
746
+ * on the default `redirect: 'follow'` would issue the request to a redirect
747
+ * target (e.g. an internal address) before we ever see its URL, defeating the
748
+ * SSRF guard.
749
+ *
750
+ * A `redirect: 'manual'` request yields an unreadable opaque response in the
751
+ * browser (and in other spec-compliant fetch implementations), so the redirect
752
+ * target cannot be validated here. In a real browser this is safe to follow
753
+ * natively because SSRF is not reachable (fetch is constrained by CORS and
754
+ * cannot reach a server's internal network or cloud-metadata). On any other
755
+ * runtime we cannot validate the hop, so we fail closed rather than follow it
756
+ * blindly and bypass the SSRF guard.
757
+ *
758
+ * The returned response is the final (non-redirect) response. The caller is
759
+ * responsible for checking `response.ok` and reading the body.
760
+ *
761
+ * @throws DownloadError if a hop is unsafe, the redirect limit is exceeded, or
762
+ * a redirect cannot be validated on a non-browser runtime.
763
+ */
764
+ declare function fetchWithValidatedRedirects({ url, headers, abortSignal, maxRedirects, }: {
765
+ url: string;
766
+ headers?: HeadersInit;
767
+ abortSignal?: AbortSignal;
768
+ maxRedirects?: number;
769
+ }): Promise<Response>;
770
+
741
771
  /**
742
772
  * Extracts a 1-based inclusive line range from `text`, auto-detecting the
743
773
  * file's line ending (`\r\n`, `\n`, or `\r`, in that priority).
@@ -971,6 +1001,16 @@ declare function injectJsonInstructionIntoMessages({ messages, schema, schemaPre
971
1001
 
972
1002
  declare function isAbortError(error: unknown): error is Error;
973
1003
 
1004
+ /**
1005
+ * Returns `true` when running in a browser.
1006
+ *
1007
+ * Detection keys on the presence of a global `window`, matching the browser
1008
+ * check used elsewhere in this package (see `getRuntimeEnvironmentUserAgent`)
1009
+ * so the SDK has a single, consistent definition of "browser". Server runtimes
1010
+ * (Node.js, Deno, Bun, edge/workers) do not define `window`.
1011
+ */
1012
+ declare function isBrowserRuntime(globalThisAny?: any): boolean;
1013
+
974
1014
  /**
975
1015
  * Type-guard for Node.js `Buffer` instances.
976
1016
  *
@@ -979,6 +1019,20 @@ declare function isAbortError(error: unknown): error is Error;
979
1019
  */
980
1020
  declare function isBuffer(value: unknown): value is Buffer;
981
1021
 
1022
+ /**
1023
+ * Returns true when `url` has the same origin (scheme + host + port) as
1024
+ * `baseUrl`.
1025
+ *
1026
+ * Used to decide whether provider credentials may be attached to a request to a
1027
+ * URL taken from a provider response (e.g. a polling or media-download URL).
1028
+ * Credentials must only be sent to the provider's own origin; a response that
1029
+ * names a foreign host (a CDN, or an attacker-controlled host if the response
1030
+ * is tampered with) must not receive the API key.
1031
+ *
1032
+ * Returns false if either value is not a valid absolute URL (fail-closed).
1033
+ */
1034
+ declare function isSameOrigin(url: string, baseUrl: string): boolean;
1035
+
982
1036
  /**
983
1037
  * Type guard that checks whether a value is not `null` or `undefined`.
984
1038
  *
@@ -1870,6 +1924,20 @@ declare function createProviderExecutedToolFactory<INPUT, OUTPUT, ARGS extends o
1870
1924
  supportsDeferredResults?: boolean;
1871
1925
  }): ProviderExecutedToolFactory<INPUT, OUTPUT, ARGS, CONTEXT>;
1872
1926
 
1927
+ /**
1928
+ * Cancels a response body to release the underlying connection.
1929
+ *
1930
+ * When a fetch Response is rejected without consuming its body (e.g. a failed
1931
+ * status code, an open-redirect rejection, or a Content-Length that exceeds the
1932
+ * size limit), the underlying TCP socket is not returned to the connection pool
1933
+ * and may stay open until the process runs out of file descriptors. Cancelling
1934
+ * the body avoids this leak.
1935
+ *
1936
+ * Errors thrown while cancelling are ignored: the body may already be locked,
1937
+ * disturbed, or absent, none of which should mask the original rejection.
1938
+ */
1939
+ declare function cancelResponseBody(response: Response): Promise<void>;
1940
+
1873
1941
  /**
1874
1942
  * Default maximum download size: 2 GiB.
1875
1943
  *
@@ -2076,6 +2144,10 @@ declare function convertToBase64(value: string | Uint8Array): string;
2076
2144
  * Validates that a URL is safe to download from, blocking private/internal addresses
2077
2145
  * to prevent SSRF attacks.
2078
2146
  *
2147
+ * Note: this performs string/literal-IP checks only. It does not resolve DNS, so a
2148
+ * hostname that resolves to a private address is not blocked here (see callers, which
2149
+ * should additionally constrain egress at the network layer when handling untrusted URLs).
2150
+ *
2079
2151
  * @param url - The URL string to validate.
2080
2152
  * @throws DownloadError if the URL is unsafe.
2081
2153
  */
@@ -2287,4 +2359,4 @@ interface ToolResult<NAME extends string, INPUT, OUTPUT> {
2287
2359
  dynamic?: boolean;
2288
2360
  }
2289
2361
 
2290
- export { type Arrayable, type AssistantContent, type AssistantModelMessage, type Context, type CustomPart, DEFAULT_MAX_DOWNLOAD_SIZE, type DataContent, DelayedPromise, DownloadError, type DynamicTool, type ExecutableTool, type SandboxProcess as Experimental_SandboxProcess, type SandboxSession as Experimental_SandboxSession, type FetchFunction, type FileData, type FileDataData, type FileDataReference, type FileDataText, type FileDataUrl, type FilePart, type FlexibleSchema, type FunctionTool, type HasRequiredKey, type IdGenerator, type ImagePart, type InferSchema, type InferToolContext, type InferToolInput, type InferToolOutput, type InferToolSetContext, type LazySchema, type MaybePromiseLike, type ModelMessage, type ParseResult, type ProviderDefinedTool, type ProviderDefinedToolFactory, type ProviderDefinedToolFactoryWithOutputSchema, type ProviderExecutedTool, type ProviderExecutedToolFactory, type ProviderOptions, type ProviderReference, type ReasoningFilePart, type ReasoningPart, type Resolvable, type ResponseHandler, type Schema, type StreamingToolCallDelta, StreamingToolCallTracker, type StreamingToolCallTrackerOptions, type SystemModelMessage, type TextPart, type Tool, type ToolApprovalRequest, type ToolApprovalResponse, type ToolCall, type ToolCallPart, type ToolContent, type ToolExecuteFunction, type ToolExecutionOptions, type ToolModelMessage, type ToolNameMapping, type ToolNeedsApprovalFunction, type ToolResult, type ToolResultOutput, type ToolResultPart, type ToolSet, type UserContent, type UserModelMessage, VERSION, type ValidationResult, asArray, asSchema, combineHeaders, convertAsyncIteratorToReadableStream, convertBase64ToUint8Array, convertImageModelFileToDataUri, convertInlineFileDataToUint8Array, convertToBase64, convertToFormData, convertUint8ArrayToBase64, createBinaryResponseHandler, createEventSourceResponseHandler, createIdGenerator, createJsonErrorResponseHandler, createJsonResponseHandler, createProviderDefinedToolFactory, createProviderDefinedToolFactoryWithOutputSchema, createProviderExecutedToolFactory, createStatusCodeErrorResponseHandler, createToolNameMapping, delay, detectMediaType, downloadBlob, dynamicTool, executeTool, extractLines, extractResponseHeaders, filterNullable, generateId, getFromApi, getRuntimeEnvironmentUserAgent, getTopLevelMediaType, injectJsonInstructionIntoMessages, isAbortError, isBuffer, isCustomReasoning, isExecutableTool, isFullMediaType, isNonNullable, isParsableJson, isProviderReference, isUrlSupported, jsonSchema, lazySchema, loadApiKey, loadOptionalSetting, loadSetting, mapReasoningToProviderBudget, mapReasoningToProviderEffort, mediaTypeToExtension, normalizeHeaders, parseJSON, parseJsonEventStream, parseProviderOptions, postFormDataToApi, postJsonToApi, postToApi, readResponseWithSizeLimit, removeUndefinedEntries, resolve, resolveFullMediaType, resolveProviderReference, safeParseJSON, safeValidateTypes, serializeModelOptions, stripFileExtension, tool, validateDownloadUrl, validateTypes, withUserAgentSuffix, withoutTrailingSlash, zodSchema };
2362
+ export { type Arrayable, type AssistantContent, type AssistantModelMessage, type Context, type CustomPart, DEFAULT_MAX_DOWNLOAD_SIZE, type DataContent, DelayedPromise, DownloadError, type DynamicTool, type ExecutableTool, type SandboxProcess as Experimental_SandboxProcess, type SandboxSession as Experimental_SandboxSession, type FetchFunction, type FileData, type FileDataData, type FileDataReference, type FileDataText, type FileDataUrl, type FilePart, type FlexibleSchema, type FunctionTool, type HasRequiredKey, type IdGenerator, type ImagePart, type InferSchema, type InferToolContext, type InferToolInput, type InferToolOutput, type InferToolSetContext, type LazySchema, type MaybePromiseLike, type ModelMessage, type ParseResult, type ProviderDefinedTool, type ProviderDefinedToolFactory, type ProviderDefinedToolFactoryWithOutputSchema, type ProviderExecutedTool, type ProviderExecutedToolFactory, type ProviderOptions, type ProviderReference, type ReasoningFilePart, type ReasoningPart, type Resolvable, type ResponseHandler, type Schema, type StreamingToolCallDelta, StreamingToolCallTracker, type StreamingToolCallTrackerOptions, type SystemModelMessage, type TextPart, type Tool, type ToolApprovalRequest, type ToolApprovalResponse, type ToolCall, type ToolCallPart, type ToolContent, type ToolExecuteFunction, type ToolExecutionOptions, type ToolModelMessage, type ToolNameMapping, type ToolNeedsApprovalFunction, type ToolResult, type ToolResultOutput, type ToolResultPart, type ToolSet, type UserContent, type UserModelMessage, VERSION, type ValidationResult, asArray, asSchema, cancelResponseBody, combineHeaders, convertAsyncIteratorToReadableStream, convertBase64ToUint8Array, convertImageModelFileToDataUri, convertInlineFileDataToUint8Array, convertToBase64, convertToFormData, convertUint8ArrayToBase64, createBinaryResponseHandler, createEventSourceResponseHandler, createIdGenerator, createJsonErrorResponseHandler, createJsonResponseHandler, createProviderDefinedToolFactory, createProviderDefinedToolFactoryWithOutputSchema, createProviderExecutedToolFactory, createStatusCodeErrorResponseHandler, createToolNameMapping, delay, detectMediaType, downloadBlob, dynamicTool, executeTool, extractLines, extractResponseHeaders, fetchWithValidatedRedirects, filterNullable, generateId, getFromApi, getRuntimeEnvironmentUserAgent, getTopLevelMediaType, injectJsonInstructionIntoMessages, isAbortError, isBrowserRuntime, isBuffer, isCustomReasoning, isExecutableTool, isFullMediaType, isNonNullable, isParsableJson, isProviderReference, isSameOrigin, isUrlSupported, jsonSchema, lazySchema, loadApiKey, loadOptionalSetting, loadSetting, mapReasoningToProviderBudget, mapReasoningToProviderEffort, mediaTypeToExtension, normalizeHeaders, parseJSON, parseJsonEventStream, parseProviderOptions, postFormDataToApi, postJsonToApi, postToApi, readResponseWithSizeLimit, removeUndefinedEntries, resolve, resolveFullMediaType, resolveProviderReference, safeParseJSON, safeValidateTypes, serializeModelOptions, stripFileExtension, tool, validateDownloadUrl, validateTypes, withUserAgentSuffix, withoutTrailingSlash, zodSchema };
package/dist/index.js CHANGED
@@ -489,6 +489,15 @@ function isFullMediaType(mediaType) {
489
489
  return subtype.length > 0 && subtype !== "*";
490
490
  }
491
491
 
492
+ // src/cancel-response-body.ts
493
+ async function cancelResponseBody(response) {
494
+ var _a2;
495
+ try {
496
+ await ((_a2 = response.body) == null ? void 0 : _a2.cancel());
497
+ } catch (e) {
498
+ }
499
+ }
500
+
492
501
  // src/download-error.ts
493
502
  import { AISDKError } from "@ai-sdk/provider";
494
503
  var name = "AI_DownloadError";
@@ -514,59 +523,9 @@ var DownloadError = class extends (_b = AISDKError, _a = symbol, _b) {
514
523
  }
515
524
  };
516
525
 
517
- // src/read-response-with-size-limit.ts
518
- var DEFAULT_MAX_DOWNLOAD_SIZE = 2 * 1024 * 1024 * 1024;
519
- async function readResponseWithSizeLimit({
520
- response,
521
- url,
522
- maxBytes = DEFAULT_MAX_DOWNLOAD_SIZE
523
- }) {
524
- const contentLength = response.headers.get("content-length");
525
- if (contentLength != null) {
526
- const length = parseInt(contentLength, 10);
527
- if (!isNaN(length) && length > maxBytes) {
528
- throw new DownloadError({
529
- url,
530
- message: `Download of ${url} exceeded maximum size of ${maxBytes} bytes (Content-Length: ${length}).`
531
- });
532
- }
533
- }
534
- const body = response.body;
535
- if (body == null) {
536
- return new Uint8Array(0);
537
- }
538
- const reader = body.getReader();
539
- const chunks = [];
540
- let totalBytes = 0;
541
- try {
542
- while (true) {
543
- const { done, value } = await reader.read();
544
- if (done) {
545
- break;
546
- }
547
- totalBytes += value.length;
548
- if (totalBytes > maxBytes) {
549
- throw new DownloadError({
550
- url,
551
- message: `Download of ${url} exceeded maximum size of ${maxBytes} bytes.`
552
- });
553
- }
554
- chunks.push(value);
555
- }
556
- } finally {
557
- try {
558
- await reader.cancel();
559
- } finally {
560
- reader.releaseLock();
561
- }
562
- }
563
- const result = new Uint8Array(totalBytes);
564
- let offset = 0;
565
- for (const chunk of chunks) {
566
- result.set(chunk, offset);
567
- offset += chunk.length;
568
- }
569
- return result;
526
+ // src/is-browser-runtime.ts
527
+ function isBrowserRuntime(globalThisAny = globalThis) {
528
+ return globalThisAny.window != null;
570
529
  }
571
530
 
572
531
  // src/validate-download-url.ts
@@ -589,7 +548,7 @@ function validateDownloadUrl(url) {
589
548
  message: `URL scheme must be http, https, or data, got ${parsed.protocol}`
590
549
  });
591
550
  }
592
- const hostname = parsed.hostname;
551
+ const hostname = parsed.hostname.toLowerCase().replace(/\.+$/, "");
593
552
  if (!hostname) {
594
553
  throw new DownloadError({
595
554
  url,
@@ -632,54 +591,190 @@ function isIPv4(hostname) {
632
591
  }
633
592
  function isPrivateIPv4(ip) {
634
593
  const parts = ip.split(".").map(Number);
635
- const [a, b] = parts;
594
+ const [a, b, c] = parts;
636
595
  if (a === 0) return true;
637
596
  if (a === 10) return true;
597
+ if (a === 100 && b >= 64 && b <= 127) return true;
638
598
  if (a === 127) return true;
639
599
  if (a === 169 && b === 254) return true;
640
600
  if (a === 172 && b >= 16 && b <= 31) return true;
601
+ if (a === 192 && b === 0 && c === 0) return true;
641
602
  if (a === 192 && b === 168) return true;
603
+ if (a === 198 && (b === 18 || b === 19)) return true;
604
+ if (a >= 240) return true;
642
605
  return false;
643
606
  }
644
- function isPrivateIPv6(ip) {
645
- const normalized = ip.toLowerCase();
646
- if (normalized === "::1") return true;
647
- if (normalized === "::") return true;
648
- if (normalized.startsWith("::ffff:")) {
649
- const mappedPart = normalized.slice(7);
650
- if (isIPv4(mappedPart)) {
651
- return isPrivateIPv4(mappedPart);
652
- }
653
- const hexParts = mappedPart.split(":");
654
- if (hexParts.length === 2) {
655
- const high = parseInt(hexParts[0], 16);
656
- const low = parseInt(hexParts[1], 16);
657
- if (!isNaN(high) && !isNaN(low)) {
658
- const a = high >> 8 & 255;
659
- const b = high & 255;
660
- const c = low >> 8 & 255;
661
- const d = low & 255;
662
- return isPrivateIPv4(`${a}.${b}.${c}.${d}`);
607
+ function parseIPv6(ip) {
608
+ let address = ip.toLowerCase();
609
+ const zoneIndex = address.indexOf("%");
610
+ if (zoneIndex !== -1) {
611
+ address = address.slice(0, zoneIndex);
612
+ }
613
+ const halves = address.split("::");
614
+ if (halves.length > 2) return null;
615
+ const toGroups = (segment) => {
616
+ if (segment === "") return [];
617
+ const groups = [];
618
+ const parts = segment.split(":");
619
+ for (let i = 0; i < parts.length; i++) {
620
+ const part = parts[i];
621
+ if (part.includes(".")) {
622
+ if (i !== parts.length - 1 || !isIPv4(part)) return null;
623
+ const [a, b, c, d] = part.split(".").map(Number);
624
+ groups.push(a << 8 | b, c << 8 | d);
625
+ continue;
663
626
  }
627
+ if (!/^[0-9a-f]{1,4}$/.test(part)) return null;
628
+ groups.push(parseInt(part, 16));
664
629
  }
630
+ return groups;
631
+ };
632
+ const head = toGroups(halves[0]);
633
+ if (head === null) return null;
634
+ if (halves.length === 2) {
635
+ const tail = toGroups(halves[1]);
636
+ if (tail === null) return null;
637
+ const fill = 8 - head.length - tail.length;
638
+ if (fill < 0) return null;
639
+ return [...head, ...new Array(fill).fill(0), ...tail];
640
+ }
641
+ return head.length === 8 ? head : null;
642
+ }
643
+ function isPrivateIPv6(ip) {
644
+ const groups = parseIPv6(ip);
645
+ if (groups === null) return true;
646
+ const topZero = (count) => groups.slice(0, count).every((group) => group === 0);
647
+ if (topZero(7) && (groups[7] === 0 || groups[7] === 1)) return true;
648
+ if ((groups[0] & 65024) === 64512) return true;
649
+ if ((groups[0] & 65472) === 65152) return true;
650
+ if ((groups[0] & 65472) === 65216) return true;
651
+ if ((groups[0] & 65280) === 65280) return true;
652
+ const embedsIPv4 = (
653
+ // ::/96 — IPv4-compatible (deprecated)
654
+ topZero(6) || // ::ffff:0:0/96 — IPv4-mapped (ffff in group 5)
655
+ topZero(5) && groups[5] === 65535 || // ::ffff:0:0/96 — IPv4-translated form (ffff in group 4, group 5 zero)
656
+ topZero(4) && groups[4] === 65535 && groups[5] === 0 || // 64:ff9b::/96 — NAT64 well-known prefix
657
+ groups[0] === 100 && groups[1] === 65435 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && groups[5] === 0 || // 64:ff9b:1::/48 — NAT64 local-use prefix
658
+ groups[0] === 100 && groups[1] === 65435 && groups[2] === 1
659
+ );
660
+ if (embedsIPv4) {
661
+ const a = groups[6] >> 8 & 255;
662
+ const b = groups[6] & 255;
663
+ const c = groups[7] >> 8 & 255;
664
+ const d = groups[7] & 255;
665
+ return isPrivateIPv4(`${a}.${b}.${c}.${d}`);
665
666
  }
666
- if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
667
- if (normalized.startsWith("fe80")) return true;
668
667
  return false;
669
668
  }
670
669
 
670
+ // src/fetch-with-validated-redirects.ts
671
+ var MAX_DOWNLOAD_REDIRECTS = 10;
672
+ async function fetchWithValidatedRedirects({
673
+ url,
674
+ headers,
675
+ abortSignal,
676
+ maxRedirects = MAX_DOWNLOAD_REDIRECTS
677
+ }) {
678
+ const baseInit = { signal: abortSignal };
679
+ if (headers !== void 0) {
680
+ baseInit.headers = headers;
681
+ }
682
+ let currentUrl = url;
683
+ for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount++) {
684
+ validateDownloadUrl(currentUrl);
685
+ const response = await fetch(currentUrl, {
686
+ ...baseInit,
687
+ redirect: "manual"
688
+ });
689
+ if (response.type === "opaqueredirect") {
690
+ if (!isBrowserRuntime()) {
691
+ throw new DownloadError({
692
+ url,
693
+ message: `Redirect from ${currentUrl} could not be validated and was blocked`
694
+ });
695
+ }
696
+ return await fetch(currentUrl, { ...baseInit, redirect: "follow" });
697
+ }
698
+ const location = response.headers.get("location");
699
+ if (response.status >= 300 && response.status < 400 && location) {
700
+ await cancelResponseBody(response);
701
+ currentUrl = new URL(location, currentUrl).toString();
702
+ continue;
703
+ }
704
+ return response;
705
+ }
706
+ throw new DownloadError({
707
+ url,
708
+ message: `Too many redirects (max ${maxRedirects})`
709
+ });
710
+ }
711
+
712
+ // src/read-response-with-size-limit.ts
713
+ var DEFAULT_MAX_DOWNLOAD_SIZE = 2 * 1024 * 1024 * 1024;
714
+ async function readResponseWithSizeLimit({
715
+ response,
716
+ url,
717
+ maxBytes = DEFAULT_MAX_DOWNLOAD_SIZE
718
+ }) {
719
+ const contentLength = response.headers.get("content-length");
720
+ if (contentLength != null) {
721
+ const length = parseInt(contentLength, 10);
722
+ if (!isNaN(length) && length > maxBytes) {
723
+ await cancelResponseBody(response);
724
+ throw new DownloadError({
725
+ url,
726
+ message: `Download of ${url} exceeded maximum size of ${maxBytes} bytes (Content-Length: ${length}).`
727
+ });
728
+ }
729
+ }
730
+ const body = response.body;
731
+ if (body == null) {
732
+ return new Uint8Array(0);
733
+ }
734
+ const reader = body.getReader();
735
+ const chunks = [];
736
+ let totalBytes = 0;
737
+ try {
738
+ while (true) {
739
+ const { done, value } = await reader.read();
740
+ if (done) {
741
+ break;
742
+ }
743
+ totalBytes += value.length;
744
+ if (totalBytes > maxBytes) {
745
+ throw new DownloadError({
746
+ url,
747
+ message: `Download of ${url} exceeded maximum size of ${maxBytes} bytes.`
748
+ });
749
+ }
750
+ chunks.push(value);
751
+ }
752
+ } finally {
753
+ try {
754
+ await reader.cancel();
755
+ } finally {
756
+ reader.releaseLock();
757
+ }
758
+ }
759
+ const result = new Uint8Array(totalBytes);
760
+ let offset = 0;
761
+ for (const chunk of chunks) {
762
+ result.set(chunk, offset);
763
+ offset += chunk.length;
764
+ }
765
+ return result;
766
+ }
767
+
671
768
  // src/download-blob.ts
672
769
  async function downloadBlob(url, options) {
673
770
  var _a2, _b2;
674
- validateDownloadUrl(url);
675
771
  try {
676
- const response = await fetch(url, {
677
- signal: options == null ? void 0 : options.abortSignal
772
+ const response = await fetchWithValidatedRedirects({
773
+ url,
774
+ abortSignal: options == null ? void 0 : options.abortSignal
678
775
  });
679
- if (response.redirected) {
680
- validateDownloadUrl(response.url);
681
- }
682
776
  if (!response.ok) {
777
+ await cancelResponseBody(response);
683
778
  throw new DownloadError({
684
779
  url,
685
780
  statusCode: response.status,
@@ -876,7 +971,7 @@ function withUserAgentSuffix(headers, ...userAgentSuffixParts) {
876
971
  }
877
972
 
878
973
  // src/version.ts
879
- var VERSION = true ? "5.0.0-canary.47" : "0.0.0-test";
974
+ var VERSION = true ? "5.0.0-canary.48" : "0.0.0-test";
880
975
 
881
976
  // src/get-from-api.ts
882
977
  var getOriginalFetch = () => globalThis.fetch;
@@ -993,6 +1088,15 @@ function isBuffer(value) {
993
1088
  return (_b2 = (_a2 = globalThis.Buffer) == null ? void 0 : _a2.isBuffer(value)) != null ? _b2 : false;
994
1089
  }
995
1090
 
1091
+ // src/is-same-origin.ts
1092
+ function isSameOrigin(url, baseUrl) {
1093
+ try {
1094
+ return new URL(url).origin === new URL(baseUrl).origin;
1095
+ } catch (e) {
1096
+ return false;
1097
+ }
1098
+ }
1099
+
996
1100
  // src/is-non-nullable.ts
997
1101
  function isNonNullable(value) {
998
1102
  return value != null;
@@ -3342,6 +3446,7 @@ export {
3342
3446
  WORKFLOW_SERIALIZE,
3343
3447
  asArray,
3344
3448
  asSchema,
3449
+ cancelResponseBody,
3345
3450
  combineHeaders,
3346
3451
  convertAsyncIteratorToReadableStream,
3347
3452
  convertBase64ToUint8Array,
@@ -3367,6 +3472,7 @@ export {
3367
3472
  executeTool,
3368
3473
  extractLines,
3369
3474
  extractResponseHeaders,
3475
+ fetchWithValidatedRedirects,
3370
3476
  filterNullable,
3371
3477
  generateId,
3372
3478
  getErrorMessage,
@@ -3375,6 +3481,7 @@ export {
3375
3481
  getTopLevelMediaType,
3376
3482
  injectJsonInstructionIntoMessages,
3377
3483
  isAbortError,
3484
+ isBrowserRuntime,
3378
3485
  isBuffer,
3379
3486
  isCustomReasoning,
3380
3487
  isExecutableTool,
@@ -3382,6 +3489,7 @@ export {
3382
3489
  isNonNullable,
3383
3490
  isParsableJson,
3384
3491
  isProviderReference,
3492
+ isSameOrigin,
3385
3493
  isUrlSupported,
3386
3494
  jsonSchema,
3387
3495
  lazySchema,