@ai-sdk/provider-utils 4.0.28 → 4.0.30

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,34 @@
1
1
  # @ai-sdk/provider-utils
2
2
 
3
+ ## 4.0.30
4
+
5
+ ### Patch Changes
6
+
7
+ - 779f5cd: fix(provider-utils): cancel response body on download rejection to prevent socket leak
8
+
9
+ 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.
10
+
11
+ ## 4.0.29
12
+
13
+ ### Patch Changes
14
+
15
+ - bfa5864: fix: only send provider credentials to same-origin response-supplied URLs
16
+
17
+ 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.
18
+
19
+ 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.
20
+
21
+ - f42aa79: fix: harden download URL SSRF guard against hostname and redirect bypasses
22
+
23
+ `validateDownloadUrl` and the file download helpers (`downloadBlob`, `download`) could be bypassed in several ways when handling untrusted URLs:
24
+
25
+ - A fully-qualified hostname with a trailing dot (e.g. `localhost.`, `myhost.local.`) skipped the localhost/`.local` blocklist.
26
+ - 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.
27
+ - 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.
28
+ - 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`).
29
+
30
+ 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.
31
+
3
32
  ## 4.0.28
4
33
 
5
34
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -203,6 +203,36 @@ declare function readResponseWithSizeLimit({ response, url, maxBytes, }: {
203
203
  maxBytes?: number;
204
204
  }): Promise<Uint8Array>;
205
205
 
206
+ /**
207
+ * Fetches a URL while enforcing the SSRF download guard on every hop.
208
+ *
209
+ * Redirects are followed manually (`redirect: 'manual'`) so each hop is
210
+ * validated with {@link validateDownloadUrl} *before* it is requested. Relying
211
+ * on the default `redirect: 'follow'` would issue the request to a redirect
212
+ * target (e.g. an internal address) before we ever see its URL, defeating the
213
+ * SSRF guard.
214
+ *
215
+ * A `redirect: 'manual'` request yields an unreadable opaque response in the
216
+ * browser (and in other spec-compliant fetch implementations), so the redirect
217
+ * target cannot be validated here. In a real browser this is safe to follow
218
+ * natively because SSRF is not reachable (fetch is constrained by CORS and
219
+ * cannot reach a server's internal network or cloud-metadata). On any other
220
+ * runtime we cannot validate the hop, so we fail closed rather than follow it
221
+ * blindly and bypass the SSRF guard.
222
+ *
223
+ * The returned response is the final (non-redirect) response. The caller is
224
+ * responsible for checking `response.ok` and reading the body.
225
+ *
226
+ * @throws DownloadError if a hop is unsafe, the redirect limit is exceeded, or
227
+ * a redirect cannot be validated on a non-browser runtime.
228
+ */
229
+ declare function fetchWithValidatedRedirects({ url, headers, abortSignal, maxRedirects, }: {
230
+ url: string;
231
+ headers?: HeadersInit;
232
+ abortSignal?: AbortSignal;
233
+ maxRedirects?: number;
234
+ }): Promise<Response>;
235
+
206
236
  /**
207
237
  * Fetch function type (standardizes the version of fetch used).
208
238
  */
@@ -398,6 +428,16 @@ declare function injectJsonInstructionIntoMessages({ messages, schema, schemaPre
398
428
 
399
429
  declare function isAbortError(error: unknown): error is Error;
400
430
 
431
+ /**
432
+ * Returns `true` when running in a browser.
433
+ *
434
+ * Detection keys on the presence of a global `window`, matching the browser
435
+ * check used elsewhere in this package (see `getRuntimeEnvironmentUserAgent`)
436
+ * so the SDK has a single, consistent definition of "browser". Server runtimes
437
+ * (Node.js, Deno, Bun, edge/workers) do not define `window`.
438
+ */
439
+ declare function isBrowserRuntime(globalThisAny?: any): boolean;
440
+
401
441
  /**
402
442
  * Type guard that checks whether a value is not `null` or `undefined`.
403
443
  *
@@ -407,6 +447,20 @@ declare function isAbortError(error: unknown): error is Error;
407
447
  */
408
448
  declare function isNonNullable<T>(value: T | undefined | null): value is NonNullable<T>;
409
449
 
450
+ /**
451
+ * Returns true when `url` has the same origin (scheme + host + port) as
452
+ * `baseUrl`.
453
+ *
454
+ * Used to decide whether provider credentials may be attached to a request to a
455
+ * URL taken from a provider response (e.g. a polling or media-download URL).
456
+ * Credentials must only be sent to the provider's own origin; a response that
457
+ * names a foreign host (a CDN, or an attacker-controlled host if the response
458
+ * is tampered with) must not receive the API key.
459
+ *
460
+ * Returns false if either value is not a valid absolute URL (fail-closed).
461
+ */
462
+ declare function isSameOrigin(url: string, baseUrl: string): boolean;
463
+
410
464
  /**
411
465
  * Checks if the given URL is supported natively by the model.
412
466
  *
@@ -1278,6 +1332,20 @@ declare function createProviderToolFactoryWithOutputSchema<INPUT, OUTPUT, ARGS e
1278
1332
  supportsDeferredResults?: boolean;
1279
1333
  }): ProviderToolFactoryWithOutputSchema<INPUT, OUTPUT, ARGS>;
1280
1334
 
1335
+ /**
1336
+ * Cancels a response body to release the underlying connection.
1337
+ *
1338
+ * When a fetch Response is rejected without consuming its body (e.g. a failed
1339
+ * status code, an open-redirect rejection, or a Content-Length that exceeds the
1340
+ * size limit), the underlying TCP socket is not returned to the connection pool
1341
+ * and may stay open until the process runs out of file descriptors. Cancelling
1342
+ * the body avoids this leak.
1343
+ *
1344
+ * Errors thrown while cancelling are ignored: the body may already be locked,
1345
+ * disturbed, or absent, none of which should mask the original rejection.
1346
+ */
1347
+ declare function cancelResponseBody(response: Response): Promise<void>;
1348
+
1281
1349
  /**
1282
1350
  * Removes entries from a record where the value is null or undefined.
1283
1351
  * @param record - The input object whose entries may be null or undefined.
@@ -1310,6 +1378,10 @@ declare function convertToBase64(value: string | Uint8Array): string;
1310
1378
  * Validates that a URL is safe to download from, blocking private/internal addresses
1311
1379
  * to prevent SSRF attacks.
1312
1380
  *
1381
+ * Note: this performs string/literal-IP checks only. It does not resolve DNS, so a
1382
+ * hostname that resolves to a private address is not blocked here (see callers, which
1383
+ * should additionally constrain egress at the network layer when handling untrusted URLs).
1384
+ *
1313
1385
  * @param url - The URL string to validate.
1314
1386
  * @throws DownloadError if the URL is unsafe.
1315
1387
  */
@@ -1446,4 +1518,4 @@ interface ToolResult<NAME extends string, INPUT, OUTPUT> {
1446
1518
  */
1447
1519
  type ToolCallOptions = ToolExecutionOptions;
1448
1520
 
1449
- export { type AssistantContent, type AssistantModelMessage, DEFAULT_MAX_DOWNLOAD_SIZE, type DataContent, DelayedPromise, DownloadError, type FetchFunction, type FilePart, type FlexibleSchema, type IdGenerator, type ImagePart, type InferSchema, type InferToolInput, type InferToolOutput, type LazySchema, type MaybePromiseLike, type ModelMessage, type ParseResult, type ProviderOptions, type ProviderToolFactory, type ProviderToolFactoryWithOutputSchema, type ReasoningPart, type Resolvable, type ResponseHandler, type Schema, type SystemModelMessage, type TextPart, type Tool, type ToolApprovalRequest, type ToolApprovalResponse, type ToolCall, type ToolCallOptions, type ToolCallPart, type ToolContent, type ToolExecuteFunction, type ToolExecutionOptions, type ToolModelMessage, type ToolNameMapping, type ToolNeedsApprovalFunction, type ToolResult, type ToolResultOutput, type ToolResultPart, type UserContent, type UserModelMessage, VERSION, type ValidationResult, asSchema, combineHeaders, convertAsyncIteratorToReadableStream, convertBase64ToUint8Array, convertImageModelFileToDataUri, convertToBase64, convertToFormData, convertUint8ArrayToBase64, createBinaryResponseHandler, createEventSourceResponseHandler, createIdGenerator, createJsonErrorResponseHandler, createJsonResponseHandler, createProviderToolFactory, createProviderToolFactoryWithOutputSchema, createStatusCodeErrorResponseHandler, createToolNameMapping, delay, downloadBlob, dynamicTool, executeTool, extractResponseHeaders, generateId, getErrorMessage, getFromApi, getRuntimeEnvironmentUserAgent, injectJsonInstructionIntoMessages, isAbortError, isNonNullable, isParsableJson, isUrlSupported, jsonSchema, lazySchema, loadApiKey, loadOptionalSetting, loadSetting, mediaTypeToExtension, normalizeHeaders, parseJSON, parseJsonEventStream, parseProviderOptions, postFormDataToApi, postJsonToApi, postToApi, readResponseWithSizeLimit, removeUndefinedEntries, resolve, safeParseJSON, safeValidateTypes, stripFileExtension, tool, validateDownloadUrl, validateTypes, withUserAgentSuffix, withoutTrailingSlash, zodSchema };
1521
+ export { type AssistantContent, type AssistantModelMessage, DEFAULT_MAX_DOWNLOAD_SIZE, type DataContent, DelayedPromise, DownloadError, type FetchFunction, type FilePart, type FlexibleSchema, type IdGenerator, type ImagePart, type InferSchema, type InferToolInput, type InferToolOutput, type LazySchema, type MaybePromiseLike, type ModelMessage, type ParseResult, type ProviderOptions, type ProviderToolFactory, type ProviderToolFactoryWithOutputSchema, type ReasoningPart, type Resolvable, type ResponseHandler, type Schema, type SystemModelMessage, type TextPart, type Tool, type ToolApprovalRequest, type ToolApprovalResponse, type ToolCall, type ToolCallOptions, type ToolCallPart, type ToolContent, type ToolExecuteFunction, type ToolExecutionOptions, type ToolModelMessage, type ToolNameMapping, type ToolNeedsApprovalFunction, type ToolResult, type ToolResultOutput, type ToolResultPart, type UserContent, type UserModelMessage, VERSION, type ValidationResult, asSchema, cancelResponseBody, combineHeaders, convertAsyncIteratorToReadableStream, convertBase64ToUint8Array, convertImageModelFileToDataUri, convertToBase64, convertToFormData, convertUint8ArrayToBase64, createBinaryResponseHandler, createEventSourceResponseHandler, createIdGenerator, createJsonErrorResponseHandler, createJsonResponseHandler, createProviderToolFactory, createProviderToolFactoryWithOutputSchema, createStatusCodeErrorResponseHandler, createToolNameMapping, delay, downloadBlob, dynamicTool, executeTool, extractResponseHeaders, fetchWithValidatedRedirects, generateId, getErrorMessage, getFromApi, getRuntimeEnvironmentUserAgent, injectJsonInstructionIntoMessages, isAbortError, isBrowserRuntime, isNonNullable, isParsableJson, isSameOrigin, isUrlSupported, jsonSchema, lazySchema, loadApiKey, loadOptionalSetting, loadSetting, mediaTypeToExtension, normalizeHeaders, parseJSON, parseJsonEventStream, parseProviderOptions, postFormDataToApi, postJsonToApi, postToApi, readResponseWithSizeLimit, removeUndefinedEntries, resolve, safeParseJSON, safeValidateTypes, stripFileExtension, tool, validateDownloadUrl, validateTypes, withUserAgentSuffix, withoutTrailingSlash, zodSchema };
package/dist/index.d.ts CHANGED
@@ -203,6 +203,36 @@ declare function readResponseWithSizeLimit({ response, url, maxBytes, }: {
203
203
  maxBytes?: number;
204
204
  }): Promise<Uint8Array>;
205
205
 
206
+ /**
207
+ * Fetches a URL while enforcing the SSRF download guard on every hop.
208
+ *
209
+ * Redirects are followed manually (`redirect: 'manual'`) so each hop is
210
+ * validated with {@link validateDownloadUrl} *before* it is requested. Relying
211
+ * on the default `redirect: 'follow'` would issue the request to a redirect
212
+ * target (e.g. an internal address) before we ever see its URL, defeating the
213
+ * SSRF guard.
214
+ *
215
+ * A `redirect: 'manual'` request yields an unreadable opaque response in the
216
+ * browser (and in other spec-compliant fetch implementations), so the redirect
217
+ * target cannot be validated here. In a real browser this is safe to follow
218
+ * natively because SSRF is not reachable (fetch is constrained by CORS and
219
+ * cannot reach a server's internal network or cloud-metadata). On any other
220
+ * runtime we cannot validate the hop, so we fail closed rather than follow it
221
+ * blindly and bypass the SSRF guard.
222
+ *
223
+ * The returned response is the final (non-redirect) response. The caller is
224
+ * responsible for checking `response.ok` and reading the body.
225
+ *
226
+ * @throws DownloadError if a hop is unsafe, the redirect limit is exceeded, or
227
+ * a redirect cannot be validated on a non-browser runtime.
228
+ */
229
+ declare function fetchWithValidatedRedirects({ url, headers, abortSignal, maxRedirects, }: {
230
+ url: string;
231
+ headers?: HeadersInit;
232
+ abortSignal?: AbortSignal;
233
+ maxRedirects?: number;
234
+ }): Promise<Response>;
235
+
206
236
  /**
207
237
  * Fetch function type (standardizes the version of fetch used).
208
238
  */
@@ -398,6 +428,16 @@ declare function injectJsonInstructionIntoMessages({ messages, schema, schemaPre
398
428
 
399
429
  declare function isAbortError(error: unknown): error is Error;
400
430
 
431
+ /**
432
+ * Returns `true` when running in a browser.
433
+ *
434
+ * Detection keys on the presence of a global `window`, matching the browser
435
+ * check used elsewhere in this package (see `getRuntimeEnvironmentUserAgent`)
436
+ * so the SDK has a single, consistent definition of "browser". Server runtimes
437
+ * (Node.js, Deno, Bun, edge/workers) do not define `window`.
438
+ */
439
+ declare function isBrowserRuntime(globalThisAny?: any): boolean;
440
+
401
441
  /**
402
442
  * Type guard that checks whether a value is not `null` or `undefined`.
403
443
  *
@@ -407,6 +447,20 @@ declare function isAbortError(error: unknown): error is Error;
407
447
  */
408
448
  declare function isNonNullable<T>(value: T | undefined | null): value is NonNullable<T>;
409
449
 
450
+ /**
451
+ * Returns true when `url` has the same origin (scheme + host + port) as
452
+ * `baseUrl`.
453
+ *
454
+ * Used to decide whether provider credentials may be attached to a request to a
455
+ * URL taken from a provider response (e.g. a polling or media-download URL).
456
+ * Credentials must only be sent to the provider's own origin; a response that
457
+ * names a foreign host (a CDN, or an attacker-controlled host if the response
458
+ * is tampered with) must not receive the API key.
459
+ *
460
+ * Returns false if either value is not a valid absolute URL (fail-closed).
461
+ */
462
+ declare function isSameOrigin(url: string, baseUrl: string): boolean;
463
+
410
464
  /**
411
465
  * Checks if the given URL is supported natively by the model.
412
466
  *
@@ -1278,6 +1332,20 @@ declare function createProviderToolFactoryWithOutputSchema<INPUT, OUTPUT, ARGS e
1278
1332
  supportsDeferredResults?: boolean;
1279
1333
  }): ProviderToolFactoryWithOutputSchema<INPUT, OUTPUT, ARGS>;
1280
1334
 
1335
+ /**
1336
+ * Cancels a response body to release the underlying connection.
1337
+ *
1338
+ * When a fetch Response is rejected without consuming its body (e.g. a failed
1339
+ * status code, an open-redirect rejection, or a Content-Length that exceeds the
1340
+ * size limit), the underlying TCP socket is not returned to the connection pool
1341
+ * and may stay open until the process runs out of file descriptors. Cancelling
1342
+ * the body avoids this leak.
1343
+ *
1344
+ * Errors thrown while cancelling are ignored: the body may already be locked,
1345
+ * disturbed, or absent, none of which should mask the original rejection.
1346
+ */
1347
+ declare function cancelResponseBody(response: Response): Promise<void>;
1348
+
1281
1349
  /**
1282
1350
  * Removes entries from a record where the value is null or undefined.
1283
1351
  * @param record - The input object whose entries may be null or undefined.
@@ -1310,6 +1378,10 @@ declare function convertToBase64(value: string | Uint8Array): string;
1310
1378
  * Validates that a URL is safe to download from, blocking private/internal addresses
1311
1379
  * to prevent SSRF attacks.
1312
1380
  *
1381
+ * Note: this performs string/literal-IP checks only. It does not resolve DNS, so a
1382
+ * hostname that resolves to a private address is not blocked here (see callers, which
1383
+ * should additionally constrain egress at the network layer when handling untrusted URLs).
1384
+ *
1313
1385
  * @param url - The URL string to validate.
1314
1386
  * @throws DownloadError if the URL is unsafe.
1315
1387
  */
@@ -1446,4 +1518,4 @@ interface ToolResult<NAME extends string, INPUT, OUTPUT> {
1446
1518
  */
1447
1519
  type ToolCallOptions = ToolExecutionOptions;
1448
1520
 
1449
- export { type AssistantContent, type AssistantModelMessage, DEFAULT_MAX_DOWNLOAD_SIZE, type DataContent, DelayedPromise, DownloadError, type FetchFunction, type FilePart, type FlexibleSchema, type IdGenerator, type ImagePart, type InferSchema, type InferToolInput, type InferToolOutput, type LazySchema, type MaybePromiseLike, type ModelMessage, type ParseResult, type ProviderOptions, type ProviderToolFactory, type ProviderToolFactoryWithOutputSchema, type ReasoningPart, type Resolvable, type ResponseHandler, type Schema, type SystemModelMessage, type TextPart, type Tool, type ToolApprovalRequest, type ToolApprovalResponse, type ToolCall, type ToolCallOptions, type ToolCallPart, type ToolContent, type ToolExecuteFunction, type ToolExecutionOptions, type ToolModelMessage, type ToolNameMapping, type ToolNeedsApprovalFunction, type ToolResult, type ToolResultOutput, type ToolResultPart, type UserContent, type UserModelMessage, VERSION, type ValidationResult, asSchema, combineHeaders, convertAsyncIteratorToReadableStream, convertBase64ToUint8Array, convertImageModelFileToDataUri, convertToBase64, convertToFormData, convertUint8ArrayToBase64, createBinaryResponseHandler, createEventSourceResponseHandler, createIdGenerator, createJsonErrorResponseHandler, createJsonResponseHandler, createProviderToolFactory, createProviderToolFactoryWithOutputSchema, createStatusCodeErrorResponseHandler, createToolNameMapping, delay, downloadBlob, dynamicTool, executeTool, extractResponseHeaders, generateId, getErrorMessage, getFromApi, getRuntimeEnvironmentUserAgent, injectJsonInstructionIntoMessages, isAbortError, isNonNullable, isParsableJson, isUrlSupported, jsonSchema, lazySchema, loadApiKey, loadOptionalSetting, loadSetting, mediaTypeToExtension, normalizeHeaders, parseJSON, parseJsonEventStream, parseProviderOptions, postFormDataToApi, postJsonToApi, postToApi, readResponseWithSizeLimit, removeUndefinedEntries, resolve, safeParseJSON, safeValidateTypes, stripFileExtension, tool, validateDownloadUrl, validateTypes, withUserAgentSuffix, withoutTrailingSlash, zodSchema };
1521
+ export { type AssistantContent, type AssistantModelMessage, DEFAULT_MAX_DOWNLOAD_SIZE, type DataContent, DelayedPromise, DownloadError, type FetchFunction, type FilePart, type FlexibleSchema, type IdGenerator, type ImagePart, type InferSchema, type InferToolInput, type InferToolOutput, type LazySchema, type MaybePromiseLike, type ModelMessage, type ParseResult, type ProviderOptions, type ProviderToolFactory, type ProviderToolFactoryWithOutputSchema, type ReasoningPart, type Resolvable, type ResponseHandler, type Schema, type SystemModelMessage, type TextPart, type Tool, type ToolApprovalRequest, type ToolApprovalResponse, type ToolCall, type ToolCallOptions, type ToolCallPart, type ToolContent, type ToolExecuteFunction, type ToolExecutionOptions, type ToolModelMessage, type ToolNameMapping, type ToolNeedsApprovalFunction, type ToolResult, type ToolResultOutput, type ToolResultPart, type UserContent, type UserModelMessage, VERSION, type ValidationResult, asSchema, cancelResponseBody, combineHeaders, convertAsyncIteratorToReadableStream, convertBase64ToUint8Array, convertImageModelFileToDataUri, convertToBase64, convertToFormData, convertUint8ArrayToBase64, createBinaryResponseHandler, createEventSourceResponseHandler, createIdGenerator, createJsonErrorResponseHandler, createJsonResponseHandler, createProviderToolFactory, createProviderToolFactoryWithOutputSchema, createStatusCodeErrorResponseHandler, createToolNameMapping, delay, downloadBlob, dynamicTool, executeTool, extractResponseHeaders, fetchWithValidatedRedirects, generateId, getErrorMessage, getFromApi, getRuntimeEnvironmentUserAgent, injectJsonInstructionIntoMessages, isAbortError, isBrowserRuntime, isNonNullable, isParsableJson, isSameOrigin, isUrlSupported, jsonSchema, lazySchema, loadApiKey, loadOptionalSetting, loadSetting, mediaTypeToExtension, normalizeHeaders, parseJSON, parseJsonEventStream, parseProviderOptions, postFormDataToApi, postJsonToApi, postToApi, readResponseWithSizeLimit, removeUndefinedEntries, resolve, safeParseJSON, safeValidateTypes, stripFileExtension, tool, validateDownloadUrl, validateTypes, withUserAgentSuffix, withoutTrailingSlash, zodSchema };
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  EventSourceParserStream: () => import_stream2.EventSourceParserStream,
37
37
  VERSION: () => VERSION,
38
38
  asSchema: () => asSchema,
39
+ cancelResponseBody: () => cancelResponseBody,
39
40
  combineHeaders: () => combineHeaders,
40
41
  convertAsyncIteratorToReadableStream: () => convertAsyncIteratorToReadableStream,
41
42
  convertBase64ToUint8Array: () => convertBase64ToUint8Array,
@@ -57,14 +58,17 @@ __export(index_exports, {
57
58
  dynamicTool: () => dynamicTool,
58
59
  executeTool: () => executeTool,
59
60
  extractResponseHeaders: () => extractResponseHeaders,
61
+ fetchWithValidatedRedirects: () => fetchWithValidatedRedirects,
60
62
  generateId: () => generateId,
61
63
  getErrorMessage: () => getErrorMessage,
62
64
  getFromApi: () => getFromApi,
63
65
  getRuntimeEnvironmentUserAgent: () => getRuntimeEnvironmentUserAgent,
64
66
  injectJsonInstructionIntoMessages: () => injectJsonInstructionIntoMessages,
65
67
  isAbortError: () => isAbortError,
68
+ isBrowserRuntime: () => isBrowserRuntime,
66
69
  isNonNullable: () => isNonNullable,
67
70
  isParsableJson: () => isParsableJson,
71
+ isSameOrigin: () => isSameOrigin,
68
72
  isUrlSupported: () => isUrlSupported,
69
73
  jsonSchema: () => jsonSchema,
70
74
  lazySchema: () => lazySchema,
@@ -304,6 +308,15 @@ function convertToFormData(input, options = {}) {
304
308
  return formData;
305
309
  }
306
310
 
311
+ // src/cancel-response-body.ts
312
+ async function cancelResponseBody(response) {
313
+ var _a2;
314
+ try {
315
+ await ((_a2 = response.body) == null ? void 0 : _a2.cancel());
316
+ } catch (e) {
317
+ }
318
+ }
319
+
307
320
  // src/download-error.ts
308
321
  var import_provider = require("@ai-sdk/provider");
309
322
  var name = "AI_DownloadError";
@@ -329,59 +342,9 @@ var DownloadError = class extends (_b = import_provider.AISDKError, _a = symbol,
329
342
  }
330
343
  };
331
344
 
332
- // src/read-response-with-size-limit.ts
333
- var DEFAULT_MAX_DOWNLOAD_SIZE = 2 * 1024 * 1024 * 1024;
334
- async function readResponseWithSizeLimit({
335
- response,
336
- url,
337
- maxBytes = DEFAULT_MAX_DOWNLOAD_SIZE
338
- }) {
339
- const contentLength = response.headers.get("content-length");
340
- if (contentLength != null) {
341
- const length = parseInt(contentLength, 10);
342
- if (!isNaN(length) && length > maxBytes) {
343
- throw new DownloadError({
344
- url,
345
- message: `Download of ${url} exceeded maximum size of ${maxBytes} bytes (Content-Length: ${length}).`
346
- });
347
- }
348
- }
349
- const body = response.body;
350
- if (body == null) {
351
- return new Uint8Array(0);
352
- }
353
- const reader = body.getReader();
354
- const chunks = [];
355
- let totalBytes = 0;
356
- try {
357
- while (true) {
358
- const { done, value } = await reader.read();
359
- if (done) {
360
- break;
361
- }
362
- totalBytes += value.length;
363
- if (totalBytes > maxBytes) {
364
- throw new DownloadError({
365
- url,
366
- message: `Download of ${url} exceeded maximum size of ${maxBytes} bytes.`
367
- });
368
- }
369
- chunks.push(value);
370
- }
371
- } finally {
372
- try {
373
- await reader.cancel();
374
- } finally {
375
- reader.releaseLock();
376
- }
377
- }
378
- const result = new Uint8Array(totalBytes);
379
- let offset = 0;
380
- for (const chunk of chunks) {
381
- result.set(chunk, offset);
382
- offset += chunk.length;
383
- }
384
- return result;
345
+ // src/is-browser-runtime.ts
346
+ function isBrowserRuntime(globalThisAny = globalThis) {
347
+ return globalThisAny.window != null;
385
348
  }
386
349
 
387
350
  // src/validate-download-url.ts
@@ -404,7 +367,7 @@ function validateDownloadUrl(url) {
404
367
  message: `URL scheme must be http, https, or data, got ${parsed.protocol}`
405
368
  });
406
369
  }
407
- const hostname = parsed.hostname;
370
+ const hostname = parsed.hostname.toLowerCase().replace(/\.+$/, "");
408
371
  if (!hostname) {
409
372
  throw new DownloadError({
410
373
  url,
@@ -447,54 +410,190 @@ function isIPv4(hostname) {
447
410
  }
448
411
  function isPrivateIPv4(ip) {
449
412
  const parts = ip.split(".").map(Number);
450
- const [a, b] = parts;
413
+ const [a, b, c] = parts;
451
414
  if (a === 0) return true;
452
415
  if (a === 10) return true;
416
+ if (a === 100 && b >= 64 && b <= 127) return true;
453
417
  if (a === 127) return true;
454
418
  if (a === 169 && b === 254) return true;
455
419
  if (a === 172 && b >= 16 && b <= 31) return true;
420
+ if (a === 192 && b === 0 && c === 0) return true;
456
421
  if (a === 192 && b === 168) return true;
422
+ if (a === 198 && (b === 18 || b === 19)) return true;
423
+ if (a >= 240) return true;
457
424
  return false;
458
425
  }
459
- function isPrivateIPv6(ip) {
460
- const normalized = ip.toLowerCase();
461
- if (normalized === "::1") return true;
462
- if (normalized === "::") return true;
463
- if (normalized.startsWith("::ffff:")) {
464
- const mappedPart = normalized.slice(7);
465
- if (isIPv4(mappedPart)) {
466
- return isPrivateIPv4(mappedPart);
467
- }
468
- const hexParts = mappedPart.split(":");
469
- if (hexParts.length === 2) {
470
- const high = parseInt(hexParts[0], 16);
471
- const low = parseInt(hexParts[1], 16);
472
- if (!isNaN(high) && !isNaN(low)) {
473
- const a = high >> 8 & 255;
474
- const b = high & 255;
475
- const c = low >> 8 & 255;
476
- const d = low & 255;
477
- return isPrivateIPv4(`${a}.${b}.${c}.${d}`);
426
+ function parseIPv6(ip) {
427
+ let address = ip.toLowerCase();
428
+ const zoneIndex = address.indexOf("%");
429
+ if (zoneIndex !== -1) {
430
+ address = address.slice(0, zoneIndex);
431
+ }
432
+ const halves = address.split("::");
433
+ if (halves.length > 2) return null;
434
+ const toGroups = (segment) => {
435
+ if (segment === "") return [];
436
+ const groups = [];
437
+ const parts = segment.split(":");
438
+ for (let i = 0; i < parts.length; i++) {
439
+ const part = parts[i];
440
+ if (part.includes(".")) {
441
+ if (i !== parts.length - 1 || !isIPv4(part)) return null;
442
+ const [a, b, c, d] = part.split(".").map(Number);
443
+ groups.push(a << 8 | b, c << 8 | d);
444
+ continue;
478
445
  }
446
+ if (!/^[0-9a-f]{1,4}$/.test(part)) return null;
447
+ groups.push(parseInt(part, 16));
479
448
  }
449
+ return groups;
450
+ };
451
+ const head = toGroups(halves[0]);
452
+ if (head === null) return null;
453
+ if (halves.length === 2) {
454
+ const tail = toGroups(halves[1]);
455
+ if (tail === null) return null;
456
+ const fill = 8 - head.length - tail.length;
457
+ if (fill < 0) return null;
458
+ return [...head, ...new Array(fill).fill(0), ...tail];
459
+ }
460
+ return head.length === 8 ? head : null;
461
+ }
462
+ function isPrivateIPv6(ip) {
463
+ const groups = parseIPv6(ip);
464
+ if (groups === null) return true;
465
+ const topZero = (count) => groups.slice(0, count).every((group) => group === 0);
466
+ if (topZero(7) && (groups[7] === 0 || groups[7] === 1)) return true;
467
+ if ((groups[0] & 65024) === 64512) return true;
468
+ if ((groups[0] & 65472) === 65152) return true;
469
+ if ((groups[0] & 65472) === 65216) return true;
470
+ if ((groups[0] & 65280) === 65280) return true;
471
+ const embedsIPv4 = (
472
+ // ::/96 — IPv4-compatible (deprecated)
473
+ topZero(6) || // ::ffff:0:0/96 — IPv4-mapped (ffff in group 5)
474
+ topZero(5) && groups[5] === 65535 || // ::ffff:0:0/96 — IPv4-translated form (ffff in group 4, group 5 zero)
475
+ topZero(4) && groups[4] === 65535 && groups[5] === 0 || // 64:ff9b::/96 — NAT64 well-known prefix
476
+ 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
477
+ groups[0] === 100 && groups[1] === 65435 && groups[2] === 1
478
+ );
479
+ if (embedsIPv4) {
480
+ const a = groups[6] >> 8 & 255;
481
+ const b = groups[6] & 255;
482
+ const c = groups[7] >> 8 & 255;
483
+ const d = groups[7] & 255;
484
+ return isPrivateIPv4(`${a}.${b}.${c}.${d}`);
480
485
  }
481
- if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
482
- if (normalized.startsWith("fe80")) return true;
483
486
  return false;
484
487
  }
485
488
 
489
+ // src/fetch-with-validated-redirects.ts
490
+ var MAX_DOWNLOAD_REDIRECTS = 10;
491
+ async function fetchWithValidatedRedirects({
492
+ url,
493
+ headers,
494
+ abortSignal,
495
+ maxRedirects = MAX_DOWNLOAD_REDIRECTS
496
+ }) {
497
+ const baseInit = { signal: abortSignal };
498
+ if (headers !== void 0) {
499
+ baseInit.headers = headers;
500
+ }
501
+ let currentUrl = url;
502
+ for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount++) {
503
+ validateDownloadUrl(currentUrl);
504
+ const response = await fetch(currentUrl, {
505
+ ...baseInit,
506
+ redirect: "manual"
507
+ });
508
+ if (response.type === "opaqueredirect") {
509
+ if (!isBrowserRuntime()) {
510
+ throw new DownloadError({
511
+ url,
512
+ message: `Redirect from ${currentUrl} could not be validated and was blocked`
513
+ });
514
+ }
515
+ return await fetch(currentUrl, { ...baseInit, redirect: "follow" });
516
+ }
517
+ const location = response.headers.get("location");
518
+ if (response.status >= 300 && response.status < 400 && location) {
519
+ await cancelResponseBody(response);
520
+ currentUrl = new URL(location, currentUrl).toString();
521
+ continue;
522
+ }
523
+ return response;
524
+ }
525
+ throw new DownloadError({
526
+ url,
527
+ message: `Too many redirects (max ${maxRedirects})`
528
+ });
529
+ }
530
+
531
+ // src/read-response-with-size-limit.ts
532
+ var DEFAULT_MAX_DOWNLOAD_SIZE = 2 * 1024 * 1024 * 1024;
533
+ async function readResponseWithSizeLimit({
534
+ response,
535
+ url,
536
+ maxBytes = DEFAULT_MAX_DOWNLOAD_SIZE
537
+ }) {
538
+ const contentLength = response.headers.get("content-length");
539
+ if (contentLength != null) {
540
+ const length = parseInt(contentLength, 10);
541
+ if (!isNaN(length) && length > maxBytes) {
542
+ await cancelResponseBody(response);
543
+ throw new DownloadError({
544
+ url,
545
+ message: `Download of ${url} exceeded maximum size of ${maxBytes} bytes (Content-Length: ${length}).`
546
+ });
547
+ }
548
+ }
549
+ const body = response.body;
550
+ if (body == null) {
551
+ return new Uint8Array(0);
552
+ }
553
+ const reader = body.getReader();
554
+ const chunks = [];
555
+ let totalBytes = 0;
556
+ try {
557
+ while (true) {
558
+ const { done, value } = await reader.read();
559
+ if (done) {
560
+ break;
561
+ }
562
+ totalBytes += value.length;
563
+ if (totalBytes > maxBytes) {
564
+ throw new DownloadError({
565
+ url,
566
+ message: `Download of ${url} exceeded maximum size of ${maxBytes} bytes.`
567
+ });
568
+ }
569
+ chunks.push(value);
570
+ }
571
+ } finally {
572
+ try {
573
+ await reader.cancel();
574
+ } finally {
575
+ reader.releaseLock();
576
+ }
577
+ }
578
+ const result = new Uint8Array(totalBytes);
579
+ let offset = 0;
580
+ for (const chunk of chunks) {
581
+ result.set(chunk, offset);
582
+ offset += chunk.length;
583
+ }
584
+ return result;
585
+ }
586
+
486
587
  // src/download-blob.ts
487
588
  async function downloadBlob(url, options) {
488
589
  var _a2, _b2;
489
- validateDownloadUrl(url);
490
590
  try {
491
- const response = await fetch(url, {
492
- signal: options == null ? void 0 : options.abortSignal
591
+ const response = await fetchWithValidatedRedirects({
592
+ url,
593
+ abortSignal: options == null ? void 0 : options.abortSignal
493
594
  });
494
- if (response.redirected) {
495
- validateDownloadUrl(response.url);
496
- }
497
595
  if (!response.ok) {
596
+ await cancelResponseBody(response);
498
597
  throw new DownloadError({
499
598
  url,
500
599
  statusCode: response.status,
@@ -678,7 +777,7 @@ function withUserAgentSuffix(headers, ...userAgentSuffixParts) {
678
777
  }
679
778
 
680
779
  // src/version.ts
681
- var VERSION = true ? "4.0.28" : "0.0.0-test";
780
+ var VERSION = true ? "4.0.30" : "0.0.0-test";
682
781
 
683
782
  // src/get-from-api.ts
684
783
  var getOriginalFetch = () => globalThis.fetch;
@@ -794,6 +893,15 @@ function isNonNullable(value) {
794
893
  return value != null;
795
894
  }
796
895
 
896
+ // src/is-same-origin.ts
897
+ function isSameOrigin(url, baseUrl) {
898
+ try {
899
+ return new URL(url).origin === new URL(baseUrl).origin;
900
+ } catch (e) {
901
+ return false;
902
+ }
903
+ }
904
+
797
905
  // src/is-url-supported.ts
798
906
  function isUrlSupported({
799
907
  mediaType,
@@ -2791,6 +2899,7 @@ var import_stream2 = require("eventsource-parser/stream");
2791
2899
  EventSourceParserStream,
2792
2900
  VERSION,
2793
2901
  asSchema,
2902
+ cancelResponseBody,
2794
2903
  combineHeaders,
2795
2904
  convertAsyncIteratorToReadableStream,
2796
2905
  convertBase64ToUint8Array,
@@ -2812,14 +2921,17 @@ var import_stream2 = require("eventsource-parser/stream");
2812
2921
  dynamicTool,
2813
2922
  executeTool,
2814
2923
  extractResponseHeaders,
2924
+ fetchWithValidatedRedirects,
2815
2925
  generateId,
2816
2926
  getErrorMessage,
2817
2927
  getFromApi,
2818
2928
  getRuntimeEnvironmentUserAgent,
2819
2929
  injectJsonInstructionIntoMessages,
2820
2930
  isAbortError,
2931
+ isBrowserRuntime,
2821
2932
  isNonNullable,
2822
2933
  isParsableJson,
2934
+ isSameOrigin,
2823
2935
  isUrlSupported,
2824
2936
  jsonSchema,
2825
2937
  lazySchema,