@ai-sdk/provider-utils 5.0.0-beta.4 → 5.0.0-beta.49
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 +373 -11
- package/dist/index.d.ts +1460 -553
- package/dist/index.js +1044 -361
- package/dist/index.js.map +1 -1
- package/dist/test/index.d.ts +2 -1
- package/dist/test/index.js +18 -37
- package/dist/test/index.js.map +1 -1
- package/package.json +16 -16
- package/src/add-additional-properties-to-json-schema.ts +1 -1
- package/src/as-array.ts +12 -0
- package/src/cancel-response-body.ts +19 -0
- package/src/convert-image-model-file-to-data-uri.ts +1 -1
- package/src/convert-inline-file-data-to-uint8-array.ts +30 -0
- package/src/create-tool-name-mapping.ts +1 -1
- package/src/detect-media-type.ts +312 -0
- package/src/download-blob.ts +8 -9
- package/src/extract-lines.ts +31 -0
- package/src/fetch-with-validated-redirects.ts +87 -0
- package/src/filter-nullable.ts +11 -0
- package/src/get-error-message.ts +1 -15
- package/src/get-from-api.ts +2 -2
- package/src/has-required-key.ts +6 -0
- package/src/index.ts +47 -12
- package/src/inject-json-instruction.ts +1 -1
- package/src/is-browser-runtime.ts +13 -0
- package/src/is-buffer.ts +9 -0
- package/src/is-json-serializable.ts +29 -0
- package/src/is-provider-reference.ts +21 -0
- package/src/is-same-origin.ts +19 -0
- package/src/is-url-supported.ts +17 -2
- package/src/load-api-key.ts +1 -1
- package/src/load-setting.ts +1 -1
- package/src/map-reasoning-to-provider.ts +108 -0
- package/src/maybe-promise-like.ts +3 -0
- package/src/parse-json-event-stream.ts +3 -3
- package/src/parse-json.ts +3 -3
- package/src/parse-provider-options.ts +1 -1
- package/src/post-to-api.ts +4 -4
- package/src/provider-defined-tool-factory.ts +129 -0
- package/src/provider-executed-tool-factory.ts +69 -0
- package/src/read-response-with-size-limit.ts +4 -0
- package/src/resolve-full-media-type.ts +49 -0
- package/src/resolve-provider-reference.ts +26 -0
- package/src/resolve.ts +16 -1
- package/src/response-handler.ts +3 -3
- package/src/schema.ts +11 -4
- package/src/secure-json-parse.ts +1 -1
- package/src/serialize-model-options.ts +63 -0
- package/src/streaming-tool-call-tracker.ts +241 -0
- package/src/test/convert-response-stream-to-array.ts +1 -1
- package/src/test/is-node-version.ts +22 -1
- package/src/to-json-schema/zod3-to-json-schema/options.ts +3 -3
- package/src/to-json-schema/zod3-to-json-schema/parse-def.ts +3 -3
- package/src/to-json-schema/zod3-to-json-schema/parse-types.ts +22 -22
- package/src/to-json-schema/zod3-to-json-schema/parsers/array.ts +3 -3
- package/src/to-json-schema/zod3-to-json-schema/parsers/bigint.ts +1 -1
- package/src/to-json-schema/zod3-to-json-schema/parsers/branded.ts +2 -2
- package/src/to-json-schema/zod3-to-json-schema/parsers/catch.ts +2 -2
- package/src/to-json-schema/zod3-to-json-schema/parsers/date.ts +4 -4
- package/src/to-json-schema/zod3-to-json-schema/parsers/default.ts +3 -3
- package/src/to-json-schema/zod3-to-json-schema/parsers/effects.ts +3 -3
- package/src/to-json-schema/zod3-to-json-schema/parsers/enum.ts +1 -1
- package/src/to-json-schema/zod3-to-json-schema/parsers/intersection.ts +5 -5
- package/src/to-json-schema/zod3-to-json-schema/parsers/literal.ts +1 -1
- package/src/to-json-schema/zod3-to-json-schema/parsers/map.ts +4 -5
- package/src/to-json-schema/zod3-to-json-schema/parsers/native-enum.ts +1 -1
- package/src/to-json-schema/zod3-to-json-schema/parsers/never.ts +1 -2
- package/src/to-json-schema/zod3-to-json-schema/parsers/nullable.ts +4 -4
- package/src/to-json-schema/zod3-to-json-schema/parsers/number.ts +1 -1
- package/src/to-json-schema/zod3-to-json-schema/parsers/object.ts +3 -3
- package/src/to-json-schema/zod3-to-json-schema/parsers/optional.ts +3 -3
- package/src/to-json-schema/zod3-to-json-schema/parsers/pipeline.ts +10 -8
- package/src/to-json-schema/zod3-to-json-schema/parsers/promise.ts +3 -3
- package/src/to-json-schema/zod3-to-json-schema/parsers/readonly.ts +2 -2
- package/src/to-json-schema/zod3-to-json-schema/parsers/record.ts +9 -10
- package/src/to-json-schema/zod3-to-json-schema/parsers/set.ts +3 -3
- package/src/to-json-schema/zod3-to-json-schema/parsers/string.ts +2 -2
- package/src/to-json-schema/zod3-to-json-schema/parsers/tuple.ts +3 -3
- package/src/to-json-schema/zod3-to-json-schema/parsers/undefined.ts +1 -2
- package/src/to-json-schema/zod3-to-json-schema/parsers/union.ts +3 -3
- package/src/to-json-schema/zod3-to-json-schema/parsers/unknown.ts +1 -2
- package/src/to-json-schema/zod3-to-json-schema/refs.ts +3 -3
- package/src/to-json-schema/zod3-to-json-schema/select-parser.ts +2 -2
- package/src/to-json-schema/zod3-to-json-schema/zod3-to-json-schema.ts +3 -3
- package/src/types/assistant-model-message.ts +5 -3
- package/src/types/content-part.ts +158 -19
- package/src/types/context.ts +4 -0
- package/src/types/executable-tool.ts +17 -0
- package/src/types/execute-tool.ts +29 -9
- package/src/types/file-data.ts +48 -0
- package/src/types/index.ts +29 -11
- package/src/types/infer-tool-context.ts +41 -0
- package/src/types/infer-tool-input.ts +7 -0
- package/src/types/infer-tool-output.ts +7 -0
- package/src/types/infer-tool-set-context.ts +44 -0
- package/src/types/model-message.ts +4 -4
- package/src/types/never-optional.ts +7 -0
- package/src/types/provider-options.ts +1 -1
- package/src/types/provider-reference.ts +10 -0
- package/src/types/sandbox.ts +217 -0
- package/src/types/system-model-message.ts +1 -1
- package/src/types/tool-approval-request.ts +13 -0
- package/src/types/tool-execute-function.ts +56 -0
- package/src/types/tool-model-message.ts +3 -3
- package/src/types/tool-needs-approval-function.ts +39 -0
- package/src/types/tool-set.ts +22 -0
- package/src/types/tool.ts +278 -225
- package/src/types/user-model-message.ts +2 -2
- package/src/validate-download-url.ts +120 -33
- package/src/validate-types.ts +5 -3
- package/dist/index.d.mts +0 -1455
- package/dist/index.mjs +0 -2754
- package/dist/index.mjs.map +0 -1
- package/dist/test/index.d.mts +0 -17
- package/dist/test/index.mjs +0 -77
- package/dist/test/index.mjs.map +0 -1
- package/src/provider-tool-factory.ts +0 -125
package/dist/test/index.d.ts
CHANGED
|
@@ -9,9 +9,10 @@ declare function convertReadableStreamToArray<T>(stream: ReadableStream<T>): Pro
|
|
|
9
9
|
declare function convertResponseStreamToArray(response: Response): Promise<string[]>;
|
|
10
10
|
|
|
11
11
|
declare function isNodeVersion(version: number): boolean;
|
|
12
|
+
declare function isNodeVersionAtLeast(major: number, minor?: number, patch?: number): boolean;
|
|
12
13
|
|
|
13
14
|
declare function mockId({ prefix, }?: {
|
|
14
15
|
prefix?: string;
|
|
15
16
|
}): () => string;
|
|
16
17
|
|
|
17
|
-
export { convertArrayToAsyncIterable, convertArrayToReadableStream, convertAsyncIterableToArray, convertReadableStreamToArray, convertResponseStreamToArray, isNodeVersion, mockId };
|
|
18
|
+
export { convertArrayToAsyncIterable, convertArrayToReadableStream, convertAsyncIterableToArray, convertReadableStreamToArray, convertResponseStreamToArray, isNodeVersion, isNodeVersionAtLeast, mockId };
|
package/dist/test/index.js
CHANGED
|
@@ -1,35 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/test/index.ts
|
|
21
|
-
var index_exports = {};
|
|
22
|
-
__export(index_exports, {
|
|
23
|
-
convertArrayToAsyncIterable: () => convertArrayToAsyncIterable,
|
|
24
|
-
convertArrayToReadableStream: () => convertArrayToReadableStream,
|
|
25
|
-
convertAsyncIterableToArray: () => convertAsyncIterableToArray,
|
|
26
|
-
convertReadableStreamToArray: () => convertReadableStreamToArray,
|
|
27
|
-
convertResponseStreamToArray: () => convertResponseStreamToArray,
|
|
28
|
-
isNodeVersion: () => isNodeVersion,
|
|
29
|
-
mockId: () => mockId
|
|
30
|
-
});
|
|
31
|
-
module.exports = __toCommonJS(index_exports);
|
|
32
|
-
|
|
33
1
|
// src/test/convert-array-to-async-iterable.ts
|
|
34
2
|
function convertArrayToAsyncIterable(values) {
|
|
35
3
|
return {
|
|
@@ -79,16 +47,29 @@ async function convertReadableStreamToArray(stream) {
|
|
|
79
47
|
|
|
80
48
|
// src/test/convert-response-stream-to-array.ts
|
|
81
49
|
async function convertResponseStreamToArray(response) {
|
|
82
|
-
return convertReadableStreamToArray(
|
|
50
|
+
return await convertReadableStreamToArray(
|
|
83
51
|
response.body.pipeThrough(new TextDecoderStream())
|
|
84
52
|
);
|
|
85
53
|
}
|
|
86
54
|
|
|
87
55
|
// src/test/is-node-version.ts
|
|
56
|
+
function getNodeVersionParts() {
|
|
57
|
+
return process.versions.node.split(".").map((version) => Number.parseInt(version, 10));
|
|
58
|
+
}
|
|
88
59
|
function isNodeVersion(version) {
|
|
89
|
-
const nodeMajorVersion =
|
|
60
|
+
const [nodeMajorVersion] = getNodeVersionParts();
|
|
90
61
|
return nodeMajorVersion === version;
|
|
91
62
|
}
|
|
63
|
+
function isNodeVersionAtLeast(major, minor = 0, patch = 0) {
|
|
64
|
+
const [nodeMajorVersion, nodeMinorVersion, nodePatchVersion] = getNodeVersionParts();
|
|
65
|
+
if (nodeMajorVersion !== major) {
|
|
66
|
+
return nodeMajorVersion > major;
|
|
67
|
+
}
|
|
68
|
+
if (nodeMinorVersion !== minor) {
|
|
69
|
+
return nodeMinorVersion > minor;
|
|
70
|
+
}
|
|
71
|
+
return nodePatchVersion >= patch;
|
|
72
|
+
}
|
|
92
73
|
|
|
93
74
|
// src/test/mock-id.ts
|
|
94
75
|
function mockId({
|
|
@@ -97,14 +78,14 @@ function mockId({
|
|
|
97
78
|
let counter = 0;
|
|
98
79
|
return () => `${prefix}-${counter++}`;
|
|
99
80
|
}
|
|
100
|
-
|
|
101
|
-
0 && (module.exports = {
|
|
81
|
+
export {
|
|
102
82
|
convertArrayToAsyncIterable,
|
|
103
83
|
convertArrayToReadableStream,
|
|
104
84
|
convertAsyncIterableToArray,
|
|
105
85
|
convertReadableStreamToArray,
|
|
106
86
|
convertResponseStreamToArray,
|
|
107
87
|
isNodeVersion,
|
|
88
|
+
isNodeVersionAtLeast,
|
|
108
89
|
mockId
|
|
109
|
-
}
|
|
90
|
+
};
|
|
110
91
|
//# sourceMappingURL=index.js.map
|
package/dist/test/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/test/
|
|
1
|
+
{"version":3,"sources":["../../src/test/convert-array-to-async-iterable.ts","../../src/test/convert-array-to-readable-stream.ts","../../src/test/convert-async-iterable-to-array.ts","../../src/test/convert-readable-stream-to-array.ts","../../src/test/convert-response-stream-to-array.ts","../../src/test/is-node-version.ts","../../src/test/mock-id.ts"],"sourcesContent":["export function convertArrayToAsyncIterable<T>(values: T[]): AsyncIterable<T> {\n return {\n async *[Symbol.asyncIterator]() {\n for (const value of values) {\n yield value;\n }\n },\n };\n}\n","export function convertArrayToReadableStream<T>(\n values: T[],\n): ReadableStream<T> {\n return new ReadableStream({\n start(controller) {\n try {\n for (const value of values) {\n controller.enqueue(value);\n }\n } finally {\n controller.close();\n }\n },\n });\n}\n","export async function convertAsyncIterableToArray<T>(\n iterable: AsyncIterable<T>,\n): Promise<T[]> {\n const result: T[] = [];\n for await (const item of iterable) {\n result.push(item);\n }\n return result;\n}\n","export async function convertReadableStreamToArray<T>(\n stream: ReadableStream<T>,\n): Promise<T[]> {\n const reader = stream.getReader();\n const result: T[] = [];\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n result.push(value);\n }\n\n return result;\n}\n","import { convertReadableStreamToArray } from './convert-readable-stream-to-array';\n\nexport async function convertResponseStreamToArray(\n response: Response,\n): Promise<string[]> {\n return await convertReadableStreamToArray(\n response.body!.pipeThrough(new TextDecoderStream()),\n );\n}\n","function getNodeVersionParts() {\n return process.versions.node\n .split('.')\n .map(version => Number.parseInt(version, 10));\n}\n\nexport function isNodeVersion(version: number) {\n const [nodeMajorVersion] = getNodeVersionParts();\n return nodeMajorVersion === version;\n}\n\nexport function isNodeVersionAtLeast(major: number, minor = 0, patch = 0) {\n const [nodeMajorVersion, nodeMinorVersion, nodePatchVersion] =\n getNodeVersionParts();\n\n if (nodeMajorVersion !== major) {\n return nodeMajorVersion > major;\n }\n\n if (nodeMinorVersion !== minor) {\n return nodeMinorVersion > minor;\n }\n\n return nodePatchVersion >= patch;\n}\n","export function mockId({\n prefix = 'id',\n}: {\n prefix?: string;\n} = {}): () => string {\n let counter = 0;\n return () => `${prefix}-${counter++}`;\n}\n"],"mappings":";AAAO,SAAS,4BAA+B,QAA+B;AAC5E,SAAO;AAAA,IACL,QAAQ,OAAO,aAAa,IAAI;AAC9B,iBAAW,SAAS,QAAQ;AAC1B,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;;;ACRO,SAAS,6BACd,QACmB;AACnB,SAAO,IAAI,eAAe;AAAA,IACxB,MAAM,YAAY;AAChB,UAAI;AACF,mBAAW,SAAS,QAAQ;AAC1B,qBAAW,QAAQ,KAAK;AAAA,QAC1B;AAAA,MACF,UAAE;AACA,mBAAW,MAAM;AAAA,MACnB;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;ACdA,eAAsB,4BACpB,UACc;AACd,QAAM,SAAc,CAAC;AACrB,mBAAiB,QAAQ,UAAU;AACjC,WAAO,KAAK,IAAI;AAAA,EAClB;AACA,SAAO;AACT;;;ACRA,eAAsB,6BACpB,QACc;AACd,QAAM,SAAS,OAAO,UAAU;AAChC,QAAM,SAAc,CAAC;AAErB,SAAO,MAAM;AACX,UAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,QAAI,KAAM;AACV,WAAO,KAAK,KAAK;AAAA,EACnB;AAEA,SAAO;AACT;;;ACXA,eAAsB,6BACpB,UACmB;AACnB,SAAO,MAAM;AAAA,IACX,SAAS,KAAM,YAAY,IAAI,kBAAkB,CAAC;AAAA,EACpD;AACF;;;ACRA,SAAS,sBAAsB;AAC7B,SAAO,QAAQ,SAAS,KACrB,MAAM,GAAG,EACT,IAAI,aAAW,OAAO,SAAS,SAAS,EAAE,CAAC;AAChD;AAEO,SAAS,cAAc,SAAiB;AAC7C,QAAM,CAAC,gBAAgB,IAAI,oBAAoB;AAC/C,SAAO,qBAAqB;AAC9B;AAEO,SAAS,qBAAqB,OAAe,QAAQ,GAAG,QAAQ,GAAG;AACxE,QAAM,CAAC,kBAAkB,kBAAkB,gBAAgB,IACzD,oBAAoB;AAEtB,MAAI,qBAAqB,OAAO;AAC9B,WAAO,mBAAmB;AAAA,EAC5B;AAEA,MAAI,qBAAqB,OAAO;AAC9B,WAAO,mBAAmB;AAAA,EAC5B;AAEA,SAAO,oBAAoB;AAC7B;;;ACxBO,SAAS,OAAO;AAAA,EACrB,SAAS;AACX,IAEI,CAAC,GAAiB;AACpB,MAAI,UAAU;AACd,SAAO,MAAM,GAAG,MAAM,IAAI,SAAS;AACrC;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/provider-utils",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.49",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"license": "Apache-2.0",
|
|
5
6
|
"sideEffects": false,
|
|
6
7
|
"main": "./dist/index.js",
|
|
7
|
-
"module": "./dist/index.mjs",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"source": "./src/index.ts",
|
|
10
10
|
"files": [
|
|
@@ -22,25 +22,25 @@
|
|
|
22
22
|
"./package.json": "./package.json",
|
|
23
23
|
".": {
|
|
24
24
|
"types": "./dist/index.d.ts",
|
|
25
|
-
"import": "./dist/index.
|
|
26
|
-
"
|
|
25
|
+
"import": "./dist/index.js",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
27
|
},
|
|
28
28
|
"./test": {
|
|
29
29
|
"types": "./dist/test/index.d.ts",
|
|
30
|
-
"import": "./dist/test/index.
|
|
31
|
-
"
|
|
32
|
-
"require": "./dist/test/index.js"
|
|
30
|
+
"import": "./dist/test/index.js",
|
|
31
|
+
"default": "./dist/test/index.js"
|
|
33
32
|
}
|
|
34
33
|
},
|
|
35
34
|
"dependencies": {
|
|
36
35
|
"@standard-schema/spec": "^1.1.0",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
36
|
+
"@workflow/serde": "4.1.0",
|
|
37
|
+
"eventsource-parser": "^3.0.8",
|
|
38
|
+
"@ai-sdk/provider": "4.0.0-beta.19"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@types/node": "
|
|
41
|
+
"@types/node": "22.19.19",
|
|
42
42
|
"msw": "2.7.0",
|
|
43
|
-
"tsup": "^8",
|
|
43
|
+
"tsup": "^8.5.1",
|
|
44
44
|
"typescript": "5.8.3",
|
|
45
45
|
"zod": "3.25.76",
|
|
46
46
|
"@vercel/ai-tsconfig": "0.0.0"
|
|
@@ -49,15 +49,17 @@
|
|
|
49
49
|
"zod": "^3.25.76 || ^4.1.8"
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|
|
52
|
-
"node": ">=
|
|
52
|
+
"node": ">=22"
|
|
53
53
|
},
|
|
54
54
|
"publishConfig": {
|
|
55
|
-
"access": "public"
|
|
55
|
+
"access": "public",
|
|
56
|
+
"provenance": true
|
|
56
57
|
},
|
|
57
58
|
"homepage": "https://ai-sdk.dev/docs",
|
|
58
59
|
"repository": {
|
|
59
60
|
"type": "git",
|
|
60
|
-
"url": "
|
|
61
|
+
"url": "https://github.com/vercel/ai",
|
|
62
|
+
"directory": "packages/provider-utils"
|
|
61
63
|
},
|
|
62
64
|
"bugs": {
|
|
63
65
|
"url": "https://github.com/vercel/ai/issues"
|
|
@@ -69,9 +71,7 @@
|
|
|
69
71
|
"build": "pnpm clean && tsup --tsconfig tsconfig.build.json",
|
|
70
72
|
"build:watch": "pnpm clean && tsup --watch",
|
|
71
73
|
"clean": "del-cli dist *.tsbuildinfo",
|
|
72
|
-
"lint": "eslint \"./**/*.ts*\"",
|
|
73
74
|
"type-check": "tsc --build",
|
|
74
|
-
"prettier-check": "prettier --check \"./**/*.ts*\"",
|
|
75
75
|
"test": "pnpm test:node && pnpm test:edge",
|
|
76
76
|
"test:update": "pnpm test:node -u",
|
|
77
77
|
"test:watch": "vitest --config vitest.node.config.js",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JSONSchema7, JSONSchema7Definition } from '@ai-sdk/provider';
|
|
1
|
+
import type { JSONSchema7, JSONSchema7Definition } from '@ai-sdk/provider';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Recursively adds additionalProperties: false to the JSON schema. This is necessary because some providers (e.g. OpenAI) do not support additionalProperties: true.
|
package/src/as-array.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A value that can be provided either as a single item, an array of items,
|
|
3
|
+
* or be left undefined.
|
|
4
|
+
*/
|
|
5
|
+
export type Arrayable<T> = T | T[] | undefined;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Normalizes a possibly undefined or non-array value into an array.
|
|
9
|
+
*/
|
|
10
|
+
export function asArray<T>(value: Arrayable<T>): T[] {
|
|
11
|
+
return value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cancels a response body to release the underlying connection.
|
|
3
|
+
*
|
|
4
|
+
* When a fetch Response is rejected without consuming its body (e.g. a failed
|
|
5
|
+
* status code, an open-redirect rejection, or a Content-Length that exceeds the
|
|
6
|
+
* size limit), the underlying TCP socket is not returned to the connection pool
|
|
7
|
+
* and may stay open until the process runs out of file descriptors. Cancelling
|
|
8
|
+
* the body avoids this leak.
|
|
9
|
+
*
|
|
10
|
+
* Errors thrown while cancelling are ignored: the body may already be locked,
|
|
11
|
+
* disturbed, or absent, none of which should mask the original rejection.
|
|
12
|
+
*/
|
|
13
|
+
export async function cancelResponseBody(response: Response): Promise<void> {
|
|
14
|
+
try {
|
|
15
|
+
await response.body?.cancel();
|
|
16
|
+
} catch {
|
|
17
|
+
// Ignore cancel errors so the original rejection is preserved.
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FilePart } from './types/content-part';
|
|
2
|
+
import { convertBase64ToUint8Array } from './uint8-utils';
|
|
3
|
+
|
|
4
|
+
type InlineFileData = Extract<
|
|
5
|
+
FilePart['data'],
|
|
6
|
+
{ type: 'data' } | { type: 'text' }
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts inline file data (a tagged `data` or `text` shape) into raw bytes.
|
|
11
|
+
*
|
|
12
|
+
* - `{ type: 'text', text }` → UTF-8 encoded bytes
|
|
13
|
+
* - `{ type: 'data', data: Uint8Array | Buffer }` → returned as-is
|
|
14
|
+
* - `{ type: 'data', data: ArrayBuffer }` → wrapped in a `Uint8Array`
|
|
15
|
+
* - `{ type: 'data', data: string }` → decoded as base64
|
|
16
|
+
*/
|
|
17
|
+
export function convertInlineFileDataToUint8Array(
|
|
18
|
+
data: InlineFileData,
|
|
19
|
+
): Uint8Array {
|
|
20
|
+
if (data.type === 'text') {
|
|
21
|
+
return new TextEncoder().encode(data.text);
|
|
22
|
+
}
|
|
23
|
+
if (data.data instanceof Uint8Array) {
|
|
24
|
+
return data.data;
|
|
25
|
+
}
|
|
26
|
+
if (data.data instanceof ArrayBuffer) {
|
|
27
|
+
return new Uint8Array(data.data);
|
|
28
|
+
}
|
|
29
|
+
return convertBase64ToUint8Array(data.data);
|
|
30
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { convertBase64ToUint8Array } from './uint8-utils';
|
|
2
|
+
|
|
3
|
+
const imageMediaTypeSignatures = [
|
|
4
|
+
{
|
|
5
|
+
mediaType: 'image/gif' as const,
|
|
6
|
+
bytesPrefix: [0x47, 0x49, 0x46], // GIF
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
mediaType: 'image/png' as const,
|
|
10
|
+
bytesPrefix: [0x89, 0x50, 0x4e, 0x47], // PNG
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
mediaType: 'image/jpeg' as const,
|
|
14
|
+
bytesPrefix: [0xff, 0xd8], // JPEG
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
mediaType: 'image/webp' as const,
|
|
18
|
+
bytesPrefix: [
|
|
19
|
+
0x52,
|
|
20
|
+
0x49,
|
|
21
|
+
0x46,
|
|
22
|
+
0x46, // "RIFF"
|
|
23
|
+
null,
|
|
24
|
+
null,
|
|
25
|
+
null,
|
|
26
|
+
null, // file size (variable)
|
|
27
|
+
0x57,
|
|
28
|
+
0x45,
|
|
29
|
+
0x42,
|
|
30
|
+
0x50, // "WEBP"
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
mediaType: 'image/bmp' as const,
|
|
35
|
+
bytesPrefix: [0x42, 0x4d],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
mediaType: 'image/tiff' as const,
|
|
39
|
+
bytesPrefix: [0x49, 0x49, 0x2a, 0x00],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
mediaType: 'image/tiff' as const,
|
|
43
|
+
bytesPrefix: [0x4d, 0x4d, 0x00, 0x2a],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
mediaType: 'image/avif' as const,
|
|
47
|
+
bytesPrefix: [
|
|
48
|
+
0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66,
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
mediaType: 'image/heic' as const,
|
|
53
|
+
bytesPrefix: [
|
|
54
|
+
0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63,
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
const documentMediaTypeSignatures = [
|
|
60
|
+
{
|
|
61
|
+
mediaType: 'application/pdf' as const,
|
|
62
|
+
bytesPrefix: [0x25, 0x50, 0x44, 0x46], // %PDF
|
|
63
|
+
},
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
const audioMediaTypeSignatures = [
|
|
67
|
+
{
|
|
68
|
+
mediaType: 'audio/mpeg' as const,
|
|
69
|
+
bytesPrefix: [0xff, 0xfb],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
mediaType: 'audio/mpeg' as const,
|
|
73
|
+
bytesPrefix: [0xff, 0xfa],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
mediaType: 'audio/mpeg' as const,
|
|
77
|
+
bytesPrefix: [0xff, 0xf3],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
mediaType: 'audio/mpeg' as const,
|
|
81
|
+
bytesPrefix: [0xff, 0xf2],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
mediaType: 'audio/mpeg' as const,
|
|
85
|
+
bytesPrefix: [0xff, 0xe3],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
mediaType: 'audio/mpeg' as const,
|
|
89
|
+
bytesPrefix: [0xff, 0xe2],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
mediaType: 'audio/wav' as const,
|
|
93
|
+
bytesPrefix: [
|
|
94
|
+
0x52, // R
|
|
95
|
+
0x49, // I
|
|
96
|
+
0x46, // F
|
|
97
|
+
0x46, // F
|
|
98
|
+
null,
|
|
99
|
+
null,
|
|
100
|
+
null,
|
|
101
|
+
null,
|
|
102
|
+
0x57, // W
|
|
103
|
+
0x41, // A
|
|
104
|
+
0x56, // V
|
|
105
|
+
0x45, // E
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
mediaType: 'audio/ogg' as const,
|
|
110
|
+
bytesPrefix: [0x4f, 0x67, 0x67, 0x53],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
mediaType: 'audio/flac' as const,
|
|
114
|
+
bytesPrefix: [0x66, 0x4c, 0x61, 0x43],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
mediaType: 'audio/aac' as const,
|
|
118
|
+
bytesPrefix: [0x40, 0x15, 0x00, 0x00],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
mediaType: 'audio/mp4' as const,
|
|
122
|
+
bytesPrefix: [0x66, 0x74, 0x79, 0x70],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
mediaType: 'audio/webm',
|
|
126
|
+
bytesPrefix: [0x1a, 0x45, 0xdf, 0xa3],
|
|
127
|
+
},
|
|
128
|
+
] as const;
|
|
129
|
+
|
|
130
|
+
const videoMediaTypeSignatures = [
|
|
131
|
+
{
|
|
132
|
+
mediaType: 'video/mp4' as const,
|
|
133
|
+
bytesPrefix: [
|
|
134
|
+
0x00,
|
|
135
|
+
0x00,
|
|
136
|
+
0x00,
|
|
137
|
+
null,
|
|
138
|
+
0x66,
|
|
139
|
+
0x74,
|
|
140
|
+
0x79,
|
|
141
|
+
0x70, // ftyp
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
mediaType: 'video/webm' as const,
|
|
146
|
+
bytesPrefix: [0x1a, 0x45, 0xdf, 0xa3], // EBML
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
mediaType: 'video/quicktime' as const,
|
|
150
|
+
bytesPrefix: [
|
|
151
|
+
0x00,
|
|
152
|
+
0x00,
|
|
153
|
+
0x00,
|
|
154
|
+
0x14,
|
|
155
|
+
0x66,
|
|
156
|
+
0x74,
|
|
157
|
+
0x79,
|
|
158
|
+
0x70,
|
|
159
|
+
0x71,
|
|
160
|
+
0x74, // ftypqt
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
mediaType: 'video/x-msvideo' as const,
|
|
165
|
+
bytesPrefix: [0x52, 0x49, 0x46, 0x46], // RIFF (AVI)
|
|
166
|
+
},
|
|
167
|
+
] as const;
|
|
168
|
+
|
|
169
|
+
const stripID3 = (data: Uint8Array | string) => {
|
|
170
|
+
const bytes =
|
|
171
|
+
typeof data === 'string' ? convertBase64ToUint8Array(data) : data;
|
|
172
|
+
const id3Size =
|
|
173
|
+
((bytes[6] & 0x7f) << 21) |
|
|
174
|
+
((bytes[7] & 0x7f) << 14) |
|
|
175
|
+
((bytes[8] & 0x7f) << 7) |
|
|
176
|
+
(bytes[9] & 0x7f);
|
|
177
|
+
|
|
178
|
+
// The raw MP3 starts here
|
|
179
|
+
return bytes.slice(id3Size + 10);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
function stripID3TagsIfPresent(data: Uint8Array | string): Uint8Array | string {
|
|
183
|
+
const hasId3 =
|
|
184
|
+
(typeof data === 'string' && data.startsWith('SUQz')) ||
|
|
185
|
+
(typeof data !== 'string' &&
|
|
186
|
+
data.length > 10 &&
|
|
187
|
+
data[0] === 0x49 && // 'I'
|
|
188
|
+
data[1] === 0x44 && // 'D'
|
|
189
|
+
data[2] === 0x33); // '3'
|
|
190
|
+
|
|
191
|
+
return hasId3 ? stripID3(data) : data;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
type MediaTypeSignatures = ReadonlyArray<{
|
|
195
|
+
readonly mediaType: string;
|
|
196
|
+
readonly bytesPrefix: ReadonlyArray<number | null>;
|
|
197
|
+
}>;
|
|
198
|
+
|
|
199
|
+
function detectMediaTypeBySignatures<T extends MediaTypeSignatures>({
|
|
200
|
+
data,
|
|
201
|
+
signatures,
|
|
202
|
+
}: {
|
|
203
|
+
data: Uint8Array | string;
|
|
204
|
+
signatures: T;
|
|
205
|
+
}): T[number]['mediaType'] | undefined {
|
|
206
|
+
const processedData = stripID3TagsIfPresent(data);
|
|
207
|
+
|
|
208
|
+
// Convert the first ~18 bytes (24 base64 chars) for consistent detection logic:
|
|
209
|
+
const bytes =
|
|
210
|
+
typeof processedData === 'string'
|
|
211
|
+
? convertBase64ToUint8Array(
|
|
212
|
+
processedData.substring(0, Math.min(processedData.length, 24)),
|
|
213
|
+
)
|
|
214
|
+
: processedData;
|
|
215
|
+
|
|
216
|
+
for (const signature of signatures) {
|
|
217
|
+
if (
|
|
218
|
+
bytes.length >= signature.bytesPrefix.length &&
|
|
219
|
+
signature.bytesPrefix.every(
|
|
220
|
+
(byte, index) => byte === null || bytes[index] === byte,
|
|
221
|
+
)
|
|
222
|
+
) {
|
|
223
|
+
return signature.mediaType;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const topLevelSignatureTables = {
|
|
231
|
+
image: imageMediaTypeSignatures,
|
|
232
|
+
audio: audioMediaTypeSignatures,
|
|
233
|
+
video: videoMediaTypeSignatures,
|
|
234
|
+
application: documentMediaTypeSignatures,
|
|
235
|
+
} as const;
|
|
236
|
+
|
|
237
|
+
type TopLevelMediaType = keyof typeof topLevelSignatureTables;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Detect the IANA media type of a file from its raw bytes or base64 string.
|
|
241
|
+
*
|
|
242
|
+
* - When `topLevelType` is omitted, every known signature is considered
|
|
243
|
+
* (image, audio, video, and application). Returns `undefined` when the
|
|
244
|
+
* bytes do not match any known signature.
|
|
245
|
+
* - When `topLevelType` is provided, only signatures for that top-level
|
|
246
|
+
* segment are considered. Returns `undefined` for unsupported segments
|
|
247
|
+
* (e.g. `"text"`) or when no signature matches.
|
|
248
|
+
*/
|
|
249
|
+
export function detectMediaType({
|
|
250
|
+
data,
|
|
251
|
+
topLevelType,
|
|
252
|
+
}: {
|
|
253
|
+
data: Uint8Array | string;
|
|
254
|
+
topLevelType?: string;
|
|
255
|
+
}): string | undefined {
|
|
256
|
+
if (topLevelType === undefined) {
|
|
257
|
+
return detectMediaTypeBySignatures({
|
|
258
|
+
data,
|
|
259
|
+
signatures: [
|
|
260
|
+
...imageMediaTypeSignatures,
|
|
261
|
+
...documentMediaTypeSignatures,
|
|
262
|
+
...audioMediaTypeSignatures,
|
|
263
|
+
...videoMediaTypeSignatures,
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const signatures = topLevelSignatureTables[topLevelType as TopLevelMediaType];
|
|
269
|
+
|
|
270
|
+
if (signatures === undefined) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return detectMediaTypeBySignatures({ data, signatures });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Returns the top-level segment of a media type (the portion before `/`).
|
|
279
|
+
*
|
|
280
|
+
* Examples:
|
|
281
|
+
* - `"image/png"` -> `"image"`
|
|
282
|
+
* - `"image/*"` -> `"image"`
|
|
283
|
+
* - `"image"` -> `"image"`
|
|
284
|
+
* - `"image/"` -> `"image"`
|
|
285
|
+
* - `""` -> `""`
|
|
286
|
+
* - `"/"` -> `""`
|
|
287
|
+
*/
|
|
288
|
+
export function getTopLevelMediaType(mediaType: string): string {
|
|
289
|
+
const slashIndex = mediaType.indexOf('/');
|
|
290
|
+
return slashIndex === -1 ? mediaType : mediaType.substring(0, slashIndex);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Returns `true` only when the given media type has a non-empty, non-wildcard
|
|
295
|
+
* subtype (i.e. matches the form `type/subtype`, and `subtype` is not `*`).
|
|
296
|
+
*
|
|
297
|
+
* Examples:
|
|
298
|
+
* - `"image/png"` -> `true`
|
|
299
|
+
* - `"image/*"` -> `false`
|
|
300
|
+
* - `"image"` -> `false`
|
|
301
|
+
* - `"image/"` -> `false`
|
|
302
|
+
* - `""` -> `false`
|
|
303
|
+
* - `"/"` -> `false`
|
|
304
|
+
*/
|
|
305
|
+
export function isFullMediaType(mediaType: string): boolean {
|
|
306
|
+
const slashIndex = mediaType.indexOf('/');
|
|
307
|
+
if (slashIndex === -1) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const subtype = mediaType.substring(slashIndex + 1);
|
|
311
|
+
return subtype.length > 0 && subtype !== '*';
|
|
312
|
+
}
|
package/src/download-blob.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { cancelResponseBody } from './cancel-response-body';
|
|
1
2
|
import { DownloadError } from './download-error';
|
|
3
|
+
import { fetchWithValidatedRedirects } from './fetch-with-validated-redirects';
|
|
2
4
|
import {
|
|
3
5
|
readResponseWithSizeLimit,
|
|
4
6
|
DEFAULT_MAX_DOWNLOAD_SIZE,
|
|
5
7
|
} from './read-response-with-size-limit';
|
|
6
|
-
import { validateDownloadUrl } from './validate-download-url';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Download a file from a URL and return it as a Blob.
|
|
@@ -20,18 +21,16 @@ export async function downloadBlob(
|
|
|
20
21
|
url: string,
|
|
21
22
|
options?: { maxBytes?: number; abortSignal?: AbortSignal },
|
|
22
23
|
): Promise<Blob> {
|
|
23
|
-
validateDownloadUrl(url);
|
|
24
24
|
try {
|
|
25
|
-
const response = await
|
|
26
|
-
|
|
25
|
+
const response = await fetchWithValidatedRedirects({
|
|
26
|
+
url,
|
|
27
|
+
abortSignal: options?.abortSignal,
|
|
27
28
|
});
|
|
28
29
|
|
|
29
|
-
// Validate final URL after redirects to prevent SSRF via open redirect
|
|
30
|
-
if (response.redirected) {
|
|
31
|
-
validateDownloadUrl(response.url);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
30
|
if (!response.ok) {
|
|
31
|
+
// Release the connection before rejecting so an error status from an
|
|
32
|
+
// attacker-controlled origin cannot leak open sockets.
|
|
33
|
+
await cancelResponseBody(response);
|
|
35
34
|
throw new DownloadError({
|
|
36
35
|
url,
|
|
37
36
|
statusCode: response.status,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts a 1-based inclusive line range from `text`, auto-detecting the
|
|
3
|
+
* file's line ending (`\r\n`, `\n`, or `\r`, in that priority).
|
|
4
|
+
*
|
|
5
|
+
* Mixed line endings are not supported: detection picks one and uses it for
|
|
6
|
+
* both the split and the rejoin, so files that mix conventions will not slice
|
|
7
|
+
* cleanly. When neither `startLine` nor `endLine` is provided, the input is
|
|
8
|
+
* returned unchanged. `endLine` past EOF clamps to the last line.
|
|
9
|
+
*/
|
|
10
|
+
export function extractLines({
|
|
11
|
+
text,
|
|
12
|
+
startLine,
|
|
13
|
+
endLine,
|
|
14
|
+
}: {
|
|
15
|
+
text: string;
|
|
16
|
+
startLine?: number;
|
|
17
|
+
endLine?: number;
|
|
18
|
+
}): string {
|
|
19
|
+
if (startLine == null && endLine == null) return text;
|
|
20
|
+
const lineEnding = text.includes('\r\n')
|
|
21
|
+
? '\r\n'
|
|
22
|
+
: text.includes('\n')
|
|
23
|
+
? '\n'
|
|
24
|
+
: text.includes('\r')
|
|
25
|
+
? '\r'
|
|
26
|
+
: '\n';
|
|
27
|
+
const lines = text.split(lineEnding);
|
|
28
|
+
const start = Math.max(1, startLine ?? 1) - 1;
|
|
29
|
+
const end = Math.min(lines.length, endLine ?? lines.length);
|
|
30
|
+
return lines.slice(start, end).join(lineEnding);
|
|
31
|
+
}
|