@ai-sdk/provider-utils 5.0.6 → 5.0.9
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 +31 -0
- package/dist/index.d.ts +246 -40
- package/dist/index.js +399 -98
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/connect-to-websocket.ts +116 -0
- package/src/fetch-with-validated-redirects.ts +87 -22
- package/src/get-from-api.ts +64 -9
- package/src/index.ts +15 -0
- package/src/sanitize-request-headers.ts +53 -0
- package/src/transcription-stream-envelope.ts +341 -0
- package/src/validate-base-url.ts +12 -0
- package/src/validate-download-url.ts +14 -2
- package/src/websocket.ts +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/provider-utils",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@standard-schema/spec": "^1.1.0",
|
|
36
36
|
"@workflow/serde": "4.1.0",
|
|
37
37
|
"eventsource-parser": "^3.0.8",
|
|
38
|
-
"@ai-sdk/provider": "4.0.
|
|
38
|
+
"@ai-sdk/provider": "4.0.3"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "22.19.19",
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { removeUndefinedEntries } from './remove-undefined-entries';
|
|
2
|
+
import {
|
|
3
|
+
getWebSocketConstructor,
|
|
4
|
+
readWebSocketMessageText,
|
|
5
|
+
type WebSocketConstructor,
|
|
6
|
+
type WebSocketLike,
|
|
7
|
+
} from './websocket';
|
|
8
|
+
|
|
9
|
+
export interface WebSocketConnection {
|
|
10
|
+
/** Undefined when the constructor threw or the signal was already aborted. */
|
|
11
|
+
socket: WebSocketLike | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Unregisters the abort listener and closes the socket. Never throws.
|
|
14
|
+
* Handlers stay attached; callers guard their own terminal state.
|
|
15
|
+
*/
|
|
16
|
+
close: (code?: number) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Opens a WebSocket for a provider model, owning the transport-generic layer
|
|
21
|
+
* (analogous to `postToApi` for HTTP): constructor resolution, header hygiene,
|
|
22
|
+
* abort wiring, and message decoding. Callers own the URL, the auth channel
|
|
23
|
+
* (subprotocols vs headers), and the wire protocol.
|
|
24
|
+
*/
|
|
25
|
+
export function connectToWebSocket({
|
|
26
|
+
url,
|
|
27
|
+
protocols,
|
|
28
|
+
headers,
|
|
29
|
+
webSocket,
|
|
30
|
+
abortSignal,
|
|
31
|
+
onOpen,
|
|
32
|
+
onMessageText,
|
|
33
|
+
onProcessingError,
|
|
34
|
+
onSocketError,
|
|
35
|
+
onClose,
|
|
36
|
+
onAbort,
|
|
37
|
+
}: {
|
|
38
|
+
url: string | URL;
|
|
39
|
+
protocols?: string | string[];
|
|
40
|
+
headers?: Record<string, string | undefined>;
|
|
41
|
+
webSocket?: WebSocketConstructor;
|
|
42
|
+
abortSignal?: AbortSignal;
|
|
43
|
+
onOpen?: (socket: WebSocketLike) => void;
|
|
44
|
+
/** One decoded message. Throws and rejections go to `onProcessingError`. */
|
|
45
|
+
onMessageText: (text: string) => void | PromiseLike<void>;
|
|
46
|
+
/** Constructor throws and message decoding/processing failures. */
|
|
47
|
+
onProcessingError: (error: unknown) => void;
|
|
48
|
+
onSocketError?: () => void;
|
|
49
|
+
onClose?: () => void;
|
|
50
|
+
/** Also called (without opening a socket) when the signal is already aborted. */
|
|
51
|
+
onAbort?: (reason: unknown) => void;
|
|
52
|
+
}): WebSocketConnection {
|
|
53
|
+
let socket: WebSocketLike | undefined;
|
|
54
|
+
let abortListener: (() => void) | undefined;
|
|
55
|
+
|
|
56
|
+
const close = (code?: number) => {
|
|
57
|
+
if (abortListener != null) {
|
|
58
|
+
abortSignal?.removeEventListener('abort', abortListener);
|
|
59
|
+
abortListener = undefined;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
socket?.close(code);
|
|
63
|
+
} catch {
|
|
64
|
+
// socket may already be closed
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (abortSignal?.aborted) {
|
|
69
|
+
onAbort?.(abortSignal.reason ?? new Error('Aborted'));
|
|
70
|
+
return { socket: undefined, close };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const WebSocketConstructor = getWebSocketConstructor(webSocket);
|
|
75
|
+
// native `WebSocket` ignores the headers option; header-capable
|
|
76
|
+
// implementations like `ws` forward it and throw on undefined values:
|
|
77
|
+
socket = new WebSocketConstructor(url, protocols, {
|
|
78
|
+
headers: removeUndefinedEntries(headers ?? {}),
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
onProcessingError(error);
|
|
82
|
+
return { socket: undefined, close };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (abortSignal != null && onAbort != null) {
|
|
86
|
+
abortListener = () => onAbort(abortSignal.reason ?? new Error('Aborted'));
|
|
87
|
+
abortSignal.addEventListener('abort', abortListener, { once: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const openedSocket = socket;
|
|
91
|
+
socket.onopen = () => {
|
|
92
|
+
try {
|
|
93
|
+
onOpen?.(openedSocket);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
onProcessingError(error);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
// Messages are processed through a promise tail so async decoding (e.g.
|
|
99
|
+
// Blob frames) cannot reorder them, and error/close handling cannot
|
|
100
|
+
// overtake a still-decoding terminal frame.
|
|
101
|
+
let tail: Promise<void> = Promise.resolve();
|
|
102
|
+
socket.onmessage = event => {
|
|
103
|
+
tail = tail
|
|
104
|
+
.then(() => readWebSocketMessageText(event.data))
|
|
105
|
+
.then(text => onMessageText(text))
|
|
106
|
+
.catch(onProcessingError);
|
|
107
|
+
};
|
|
108
|
+
socket.onerror = () => {
|
|
109
|
+
tail = tail.then(() => onSocketError?.()).catch(onProcessingError);
|
|
110
|
+
};
|
|
111
|
+
socket.onclose = () => {
|
|
112
|
+
tail = tail.then(() => onClose?.()).catch(onProcessingError);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return { socket, close };
|
|
116
|
+
}
|
|
@@ -1,30 +1,61 @@
|
|
|
1
1
|
import { cancelResponseBody } from './cancel-response-body';
|
|
2
2
|
import { DownloadError } from './download-error';
|
|
3
|
+
import type { FetchFunction } from './fetch-function';
|
|
3
4
|
import { isBrowserRuntime } from './is-browser-runtime';
|
|
5
|
+
import { isSameOrigin } from './is-same-origin';
|
|
6
|
+
import { sanitizeRequestHeaders } from './sanitize-request-headers';
|
|
4
7
|
import { validateDownloadUrl } from './validate-download-url';
|
|
5
8
|
|
|
6
9
|
const MAX_DOWNLOAD_REDIRECTS = 10;
|
|
7
10
|
|
|
11
|
+
// Redirect status codes per the fetch spec (https://fetch.spec.whatwg.org/#redirect-status).
|
|
12
|
+
// Notably 300 (Multiple Choices) and 304 (Not Modified) are NOT redirects,
|
|
13
|
+
// even when a server attaches a Location header.
|
|
14
|
+
const REDIRECT_STATUS_CODES = new Set([301, 302, 303, 307, 308]);
|
|
15
|
+
|
|
8
16
|
/**
|
|
9
|
-
* Fetches a URL while enforcing the
|
|
17
|
+
* Fetches a URL while enforcing the download guard on every hop.
|
|
10
18
|
*
|
|
11
19
|
* Redirects are followed manually (`redirect: 'manual'`) so each hop is
|
|
12
20
|
* validated with {@link validateDownloadUrl} *before* it is requested. Relying
|
|
13
21
|
* on the default `redirect: 'follow'` would issue the request to a redirect
|
|
14
22
|
* target (e.g. an internal address) before we ever see its URL, defeating the
|
|
15
|
-
*
|
|
23
|
+
* guard.
|
|
24
|
+
*
|
|
25
|
+
* Request headers are also protected: {@link sanitizeRequestHeaders} strips
|
|
26
|
+
* proxy/metadata/cookie/hop-by-hop headers before the first request, and all
|
|
27
|
+
* caller headers except `User-Agent` are dropped on a cross-origin redirect.
|
|
28
|
+
* The fetch spec only strips `Authorization` on cross-origin redirects because
|
|
29
|
+
* in a browser, CORS preflighting protects custom headers; there is no CORS on
|
|
30
|
+
* the server, so provider API keys carried in custom headers (e.g. `x-key`)
|
|
31
|
+
* must be dropped here as well.
|
|
16
32
|
*
|
|
17
33
|
* A `redirect: 'manual'` request yields an unreadable opaque response in the
|
|
18
34
|
* browser (and in other spec-compliant fetch implementations), so the redirect
|
|
19
35
|
* target cannot be validated here. In a real browser this is safe to follow
|
|
20
|
-
* natively because
|
|
21
|
-
* cannot reach a server's internal network or
|
|
22
|
-
* runtime we cannot validate the hop, so we fail
|
|
23
|
-
* blindly and bypass the
|
|
36
|
+
* natively because reaching an internal network is not possible (fetch is
|
|
37
|
+
* constrained by CORS and cannot reach a server's internal network or
|
|
38
|
+
* cloud-metadata). On any other runtime we cannot validate the hop, so we fail
|
|
39
|
+
* closed rather than follow it blindly and bypass the guard.
|
|
40
|
+
*
|
|
41
|
+
* A hop that is same-origin with `trustedOrigin` (the developer-configured
|
|
42
|
+
* provider endpoint) skips target validation: that origin is exactly what an
|
|
43
|
+
* unvalidated, config-derived request would fetch anyway, and validating it
|
|
44
|
+
* would break legitimate self-hosted / localhost deployments whose response
|
|
45
|
+
* URLs point back at the configured host. Hops on any other origin are always
|
|
46
|
+
* validated.
|
|
24
47
|
*
|
|
25
48
|
* The returned response is the final (non-redirect) response. The caller is
|
|
26
49
|
* responsible for checking `response.ok` and reading the body.
|
|
27
50
|
*
|
|
51
|
+
* Not solved here: this does string/literal checks only and does not resolve
|
|
52
|
+
* DNS, so a hostname that *resolves* to a private address, and DNS rebinding
|
|
53
|
+
* (the resolved IP flipping between validation and connect), are not blocked.
|
|
54
|
+
* Server deployments fetching untrusted URLs should constrain egress at the
|
|
55
|
+
* network layer or inject a Node `fetch` that pins the resolved IP at connect
|
|
56
|
+
* time — those need DNS/socket APIs not available on all target runtimes
|
|
57
|
+
* (edge, browser, Bun), so they are intentionally not built in.
|
|
58
|
+
*
|
|
28
59
|
* @throws DownloadError if a hop is unsafe, the redirect limit is exceeded, or
|
|
29
60
|
* a redirect cannot be validated on a non-browser runtime.
|
|
30
61
|
*/
|
|
@@ -33,29 +64,49 @@ export async function fetchWithValidatedRedirects({
|
|
|
33
64
|
headers,
|
|
34
65
|
abortSignal,
|
|
35
66
|
maxRedirects = MAX_DOWNLOAD_REDIRECTS,
|
|
67
|
+
fetch = globalThis.fetch,
|
|
68
|
+
trustedOrigin,
|
|
36
69
|
}: {
|
|
37
70
|
url: string;
|
|
38
71
|
headers?: HeadersInit;
|
|
39
72
|
abortSignal?: AbortSignal;
|
|
40
73
|
maxRedirects?: number;
|
|
74
|
+
fetch?: FetchFunction;
|
|
75
|
+
/**
|
|
76
|
+
* A developer-configured origin (e.g. the provider's `baseURL`) whose hops
|
|
77
|
+
* skip target validation. Must never be derived from response data.
|
|
78
|
+
*/
|
|
79
|
+
trustedOrigin?: string;
|
|
41
80
|
}): Promise<Response> {
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
81
|
+
// Left undefined when no headers are provided (bare request); otherwise
|
|
82
|
+
// sanitized once and replaced on a cross-origin hop (credential drop below).
|
|
83
|
+
let currentHeaders =
|
|
84
|
+
headers === undefined ? undefined : sanitizeRequestHeaders(headers);
|
|
85
|
+
|
|
86
|
+
const perHopInit = (redirect: RequestRedirect): RequestInit => {
|
|
87
|
+
const init: RequestInit = { signal: abortSignal, redirect };
|
|
88
|
+
if (currentHeaders !== undefined) {
|
|
89
|
+
// Snapshot per hop: the platform fetch reads headers synchronously, but
|
|
90
|
+
// an injected fetch may defer, and hops between credential drops would
|
|
91
|
+
// otherwise share one instance.
|
|
92
|
+
init.headers = new Headers(currentHeaders);
|
|
93
|
+
}
|
|
94
|
+
return init;
|
|
95
|
+
};
|
|
49
96
|
|
|
50
97
|
let currentUrl = url;
|
|
51
98
|
// The bound also acts as a backstop against an unterminated redirect chain.
|
|
52
99
|
for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount++) {
|
|
53
|
-
|
|
100
|
+
// The developer-configured origin is trusted by definition; validating it
|
|
101
|
+
// would reject legitimate self-hosted / localhost deployments.
|
|
102
|
+
if (
|
|
103
|
+
trustedOrigin === undefined ||
|
|
104
|
+
!isSameOrigin(currentUrl, trustedOrigin)
|
|
105
|
+
) {
|
|
106
|
+
validateDownloadUrl(currentUrl);
|
|
107
|
+
}
|
|
54
108
|
|
|
55
|
-
const response = await fetch(currentUrl,
|
|
56
|
-
...baseInit,
|
|
57
|
-
redirect: 'manual',
|
|
58
|
-
});
|
|
109
|
+
const response = await fetch(currentUrl, perHopInit('manual'));
|
|
59
110
|
|
|
60
111
|
if (response.type === 'opaqueredirect') {
|
|
61
112
|
if (!isBrowserRuntime()) {
|
|
@@ -64,16 +115,30 @@ export async function fetchWithValidatedRedirects({
|
|
|
64
115
|
message: `Redirect from ${currentUrl} could not be validated and was blocked`,
|
|
65
116
|
});
|
|
66
117
|
}
|
|
67
|
-
return await fetch(currentUrl,
|
|
118
|
+
return await fetch(currentUrl, perHopInit('follow'));
|
|
68
119
|
}
|
|
69
120
|
|
|
70
121
|
const location = response.headers.get('location');
|
|
71
|
-
if (
|
|
122
|
+
if (REDIRECT_STATUS_CODES.has(response.status) && location) {
|
|
72
123
|
// Release the redirect response's connection before moving to the next
|
|
73
|
-
// hop. Whether that hop is followed or rejected by the
|
|
124
|
+
// hop. Whether that hop is followed or rejected by the guard, an
|
|
74
125
|
// unconsumed 3xx body would leak the underlying socket.
|
|
75
126
|
await cancelResponseBody(response);
|
|
76
|
-
|
|
127
|
+
const nextUrl = new URL(location, currentUrl).toString();
|
|
128
|
+
|
|
129
|
+
// Drop all caller headers except the user-agent before following a
|
|
130
|
+
// redirect that crosses origin. Only stripping Authorization (as the
|
|
131
|
+
// fetch spec does) is not enough on the server: providers authenticate
|
|
132
|
+
// with custom headers too (e.g. `x-key`), and without CORS there is
|
|
133
|
+
// nothing else stopping them from riding to a foreign host.
|
|
134
|
+
if (currentHeaders !== undefined && !isSameOrigin(nextUrl, currentUrl)) {
|
|
135
|
+
const userAgent = currentHeaders.get('user-agent');
|
|
136
|
+
currentHeaders = new Headers(
|
|
137
|
+
userAgent == null ? undefined : { 'user-agent': userAgent },
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
currentUrl = nextUrl;
|
|
77
142
|
continue;
|
|
78
143
|
}
|
|
79
144
|
|
package/src/get-from-api.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { APICallError } from '@ai-sdk/provider';
|
|
2
2
|
import { extractResponseHeaders } from './extract-response-headers';
|
|
3
|
+
import { fetchWithValidatedRedirects } from './fetch-with-validated-redirects';
|
|
3
4
|
import type { FetchFunction } from './fetch-function';
|
|
4
5
|
import { handleFetchError } from './handle-fetch-error';
|
|
5
6
|
import { isAbortError } from './is-abort-error';
|
|
7
|
+
import { isSameOrigin } from './is-same-origin';
|
|
6
8
|
import type { ResponseHandler } from './response-handler';
|
|
7
9
|
import { getRuntimeEnvironmentUserAgent } from './get-runtime-environment-user-agent';
|
|
8
10
|
import { withUserAgentSuffix } from './with-user-agent-suffix';
|
|
@@ -18,6 +20,9 @@ export const getFromApi = async <T>({
|
|
|
18
20
|
failedResponseHandler,
|
|
19
21
|
abortSignal,
|
|
20
22
|
fetch = getOriginalFetch(),
|
|
23
|
+
validateUrl,
|
|
24
|
+
credentialedOrigin,
|
|
25
|
+
trustedOrigin,
|
|
21
26
|
}: {
|
|
22
27
|
url: string;
|
|
23
28
|
headers?: Record<string, string | undefined>;
|
|
@@ -25,17 +30,67 @@ export const getFromApi = async <T>({
|
|
|
25
30
|
successfulResponseHandler: ResponseHandler<T>;
|
|
26
31
|
abortSignal?: AbortSignal;
|
|
27
32
|
fetch?: FetchFunction;
|
|
33
|
+
/**
|
|
34
|
+
* Set `true` when `url` is untrusted (e.g. taken from a provider response
|
|
35
|
+
* body): it is routed through {@link fetchWithValidatedRedirects}, which
|
|
36
|
+
* rejects private/loopback/link-local targets and re-validates redirect
|
|
37
|
+
* hops; blocked URLs throw `DownloadError`. Set `false` only for URLs built
|
|
38
|
+
* from a developer-configured endpoint.
|
|
39
|
+
*
|
|
40
|
+
* Optional for backwards compatibility with existing callers; omitting it
|
|
41
|
+
* behaves like `false` (no validation). Provider code in this repository
|
|
42
|
+
* must always pass it explicitly so every call site makes a visible trust
|
|
43
|
+
* decision — see `contributing/secure-url-handling.md`.
|
|
44
|
+
*/
|
|
45
|
+
validateUrl?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* When set, `headers` are sent only if `url` is same-origin with this origin
|
|
48
|
+
* (the user-agent suffix is always kept). Pass the provider's configured
|
|
49
|
+
* base URL alongside `validateUrl: true` so credentials never ride a request
|
|
50
|
+
* to a response-supplied host on a different origin (e.g. a CDN). Redirects
|
|
51
|
+
* that later cross origin drop all caller headers regardless (see
|
|
52
|
+
* {@link fetchWithValidatedRedirects}).
|
|
53
|
+
*/
|
|
54
|
+
credentialedOrigin?: string;
|
|
55
|
+
/**
|
|
56
|
+
* A developer-configured origin (e.g. the provider's `baseURL`) that is
|
|
57
|
+
* exempt from URL validation when `validateUrl` is `true`. A response URL
|
|
58
|
+
* (or redirect hop) that is same-origin with it is fetched without target
|
|
59
|
+
* validation — it points at exactly the host a config-derived
|
|
60
|
+
* `validateUrl: false` request would fetch anyway, so blocking it would
|
|
61
|
+
* only break legitimate self-hosted / localhost deployments whose response
|
|
62
|
+
* URLs point back at the configured host. Hops on any other origin are
|
|
63
|
+
* still validated. Must never be derived from response data.
|
|
64
|
+
*/
|
|
65
|
+
trustedOrigin?: string;
|
|
28
66
|
}) => {
|
|
29
67
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
68
|
+
// Withhold caller headers when the URL is not same-origin with the origin
|
|
69
|
+
// allowed to receive credentials; the user-agent suffix is still applied.
|
|
70
|
+
const outgoingHeaders =
|
|
71
|
+
credentialedOrigin !== undefined && !isSameOrigin(url, credentialedOrigin)
|
|
72
|
+
? {}
|
|
73
|
+
: headers;
|
|
74
|
+
|
|
75
|
+
const requestHeaders = withUserAgentSuffix(
|
|
76
|
+
outgoingHeaders,
|
|
77
|
+
`ai-sdk/provider-utils/${VERSION}`,
|
|
78
|
+
getRuntimeEnvironmentUserAgent(),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const response = validateUrl
|
|
82
|
+
? await fetchWithValidatedRedirects({
|
|
83
|
+
url,
|
|
84
|
+
headers: requestHeaders,
|
|
85
|
+
abortSignal,
|
|
86
|
+
fetch,
|
|
87
|
+
trustedOrigin,
|
|
88
|
+
})
|
|
89
|
+
: await fetch(url, {
|
|
90
|
+
method: 'GET',
|
|
91
|
+
headers: requestHeaders,
|
|
92
|
+
signal: abortSignal,
|
|
93
|
+
});
|
|
39
94
|
|
|
40
95
|
const responseHeaders = extractResponseHeaders(response);
|
|
41
96
|
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export { asArray } from './as-array';
|
|
2
2
|
export type { Arrayable } from './as-array';
|
|
3
3
|
export * from './combine-headers';
|
|
4
|
+
export {
|
|
5
|
+
connectToWebSocket,
|
|
6
|
+
type WebSocketConnection,
|
|
7
|
+
} from './connect-to-websocket';
|
|
4
8
|
export { convertAsyncIteratorToReadableStream } from './convert-async-iterator-to-readable-stream';
|
|
5
9
|
export { convertInlineFileDataToUint8Array } from './convert-inline-file-data-to-uint8-array';
|
|
6
10
|
export { convertImageModelFileToDataUri } from './convert-image-model-file-to-data-uri';
|
|
@@ -91,7 +95,17 @@ export {
|
|
|
91
95
|
type StreamingToolCallTrackerOptions,
|
|
92
96
|
} from './streaming-tool-call-tracker';
|
|
93
97
|
export { stripFileExtension } from './strip-file-extension';
|
|
98
|
+
export {
|
|
99
|
+
TRANSCRIPTION_STREAM_AUDIO_DONE_FRAME_TYPE as EXPERIMENTAL_TRANSCRIPTION_STREAM_AUDIO_DONE_FRAME_TYPE,
|
|
100
|
+
TRANSCRIPTION_STREAM_START_FRAME_TYPE as EXPERIMENTAL_TRANSCRIPTION_STREAM_START_FRAME_TYPE,
|
|
101
|
+
parseTranscriptionStreamClientFrame as experimental_parseTranscriptionStreamClientFrame,
|
|
102
|
+
parseTranscriptionStreamPart as experimental_parseTranscriptionStreamPart,
|
|
103
|
+
serializeTranscriptionStreamPart as experimental_serializeTranscriptionStreamPart,
|
|
104
|
+
type TranscriptionStreamClientFrame as Experimental_TranscriptionStreamClientFrame,
|
|
105
|
+
type TranscriptionStreamStartFrame as Experimental_TranscriptionStreamStartFrame,
|
|
106
|
+
} from './transcription-stream-envelope';
|
|
94
107
|
export * from './uint8-utils';
|
|
108
|
+
export { validateBaseURL } from './validate-base-url';
|
|
95
109
|
export { validateDownloadUrl } from './validate-download-url';
|
|
96
110
|
export * from './validate-types';
|
|
97
111
|
export { VERSION } from './version';
|
|
@@ -99,6 +113,7 @@ export {
|
|
|
99
113
|
getWebSocketConstructor,
|
|
100
114
|
readWebSocketMessageText,
|
|
101
115
|
toWebSocketUrl,
|
|
116
|
+
waitForWebSocketBufferDrain,
|
|
102
117
|
type WebSocketConstructor,
|
|
103
118
|
type WebSocketLike,
|
|
104
119
|
} from './websocket';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request headers stripped before fetching an untrusted URL: host/virtual-host
|
|
3
|
+
* routing, proxy/origin spoofing, cloud-metadata, cookies, and hop-by-hop
|
|
4
|
+
* transport headers (RFC 7230 §6.1).
|
|
5
|
+
*
|
|
6
|
+
* `Authorization` and other credential-bearing caller headers (e.g. `x-key`)
|
|
7
|
+
* are intentionally not listed — they're needed on the first hop of some
|
|
8
|
+
* provider polling calls. Instead, all caller headers except the user-agent are
|
|
9
|
+
* dropped on a cross-origin redirect (see `fetch-with-validated-redirects`).
|
|
10
|
+
*/
|
|
11
|
+
const BLOCKED_REQUEST_HEADERS: readonly string[] = [
|
|
12
|
+
// Hop-by-hop / transport (RFC 7230 §6.1)
|
|
13
|
+
'connection',
|
|
14
|
+
'keep-alive',
|
|
15
|
+
'te',
|
|
16
|
+
'trailer',
|
|
17
|
+
'transfer-encoding',
|
|
18
|
+
'upgrade',
|
|
19
|
+
|
|
20
|
+
// Host / virtual-host routing
|
|
21
|
+
'host',
|
|
22
|
+
|
|
23
|
+
// Proxy / origin spoofing
|
|
24
|
+
'forwarded',
|
|
25
|
+
'proxy-authorization',
|
|
26
|
+
'via',
|
|
27
|
+
'x-forwarded-for',
|
|
28
|
+
'x-forwarded-host',
|
|
29
|
+
'x-forwarded-proto',
|
|
30
|
+
'x-real-ip',
|
|
31
|
+
|
|
32
|
+
// Cloud metadata (GCP, AWS IMDSv1/v2, Azure, Alibaba, DigitalOcean)
|
|
33
|
+
'metadata',
|
|
34
|
+
'metadata-flavor',
|
|
35
|
+
'x-aws-ec2-metadata-token',
|
|
36
|
+
'x-metadata-token',
|
|
37
|
+
|
|
38
|
+
// Session / cookie
|
|
39
|
+
'cookie',
|
|
40
|
+
'set-cookie',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns a fresh `Headers` built from `input` with {@link BLOCKED_REQUEST_HEADERS}
|
|
45
|
+
* removed. The input is never mutated.
|
|
46
|
+
*/
|
|
47
|
+
export function sanitizeRequestHeaders(input: HeadersInit): Headers {
|
|
48
|
+
const headers = new Headers(input);
|
|
49
|
+
for (const name of BLOCKED_REQUEST_HEADERS) {
|
|
50
|
+
headers.delete(name);
|
|
51
|
+
}
|
|
52
|
+
return headers;
|
|
53
|
+
}
|