@ai-sdk/provider-utils 5.0.0-beta.30 → 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 +153 -0
- package/dist/index.d.ts +419 -41
- package/dist/index.js +222 -93
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/cancel-response-body.ts +19 -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/index.ts +5 -0
- package/src/is-browser-runtime.ts +13 -0
- package/src/is-same-origin.ts +19 -0
- package/src/read-response-with-size-limit.ts +4 -0
- package/src/schema.ts +5 -1
- package/src/to-json-schema/zod3-to-json-schema/parsers/pipeline.ts +6 -4
- package/src/types/content-part.ts +67 -6
- package/src/types/index.ts +10 -7
- package/src/types/infer-tool-context.ts +33 -4
- package/src/types/infer-tool-set-context.ts +36 -7
- package/src/types/sandbox.ts +217 -0
- package/src/types/tool-approval-request.ts +6 -0
- package/src/types/tool-execute-function.ts +6 -0
- package/src/types/tool.ts +44 -18
- package/src/validate-download-url.ts +113 -31
- package/src/types/sensitive-context.ts +0 -9
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for executing a command in the sandbox via `run` or `spawn`.
|
|
3
|
+
*/
|
|
4
|
+
type SandboxProcessOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* Command to execute in the sandbox.
|
|
7
|
+
*/
|
|
8
|
+
command: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Working directory to execute the command in.
|
|
12
|
+
*/
|
|
13
|
+
workingDirectory?: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Environment variables to set for this command. Merged with the
|
|
17
|
+
* sandbox's default environment; values here take precedence.
|
|
18
|
+
* Supporting environment variables as an option is preferable from a
|
|
19
|
+
* security perspective, e.g. to avoid them leaking in logs.
|
|
20
|
+
*/
|
|
21
|
+
env?: Record<string, string>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Signal that can be used to abort the command. When aborted, the running
|
|
25
|
+
* process is killed; for `spawn`, `wait()` rejects with the abort reason.
|
|
26
|
+
*/
|
|
27
|
+
abortSignal?: AbortSignal;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for reading a file from the sandbox.
|
|
32
|
+
*/
|
|
33
|
+
type ReadFileOptions = {
|
|
34
|
+
/**
|
|
35
|
+
* Path of the file to read.
|
|
36
|
+
*/
|
|
37
|
+
path: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Signal that can be used to abort the read.
|
|
41
|
+
*/
|
|
42
|
+
abortSignal?: AbortSignal;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Options for writing a file to the sandbox. `CONTENT` is the payload written
|
|
47
|
+
* to the file: a byte stream, raw bytes, or a string.
|
|
48
|
+
*/
|
|
49
|
+
type WriteFileOptions<CONTENT> = {
|
|
50
|
+
/**
|
|
51
|
+
* Path of the file to write.
|
|
52
|
+
*/
|
|
53
|
+
path: string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Content to write to the file.
|
|
57
|
+
*/
|
|
58
|
+
content: CONTENT;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Signal that can be used to abort the write.
|
|
62
|
+
*/
|
|
63
|
+
abortSignal?: AbortSignal;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Sandbox session that can execute commands and read/write files.
|
|
68
|
+
*/
|
|
69
|
+
export type SandboxSession = {
|
|
70
|
+
/**
|
|
71
|
+
* Description of the sandbox environment that can be added to the agent's instructions
|
|
72
|
+
* so that the agent knows about relevant details such as the root directory, exposed
|
|
73
|
+
* ports, the public hostname, etc.
|
|
74
|
+
*/
|
|
75
|
+
readonly description: string;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read one file from the sandbox as a stream of bytes. Resolves to `null`
|
|
79
|
+
* when the file does not exist.
|
|
80
|
+
*
|
|
81
|
+
* Relative path handling is implementation-defined. This is the lowest-level
|
|
82
|
+
* read primitive; prefer `readBinaryFile` or `readTextFile` unless you need
|
|
83
|
+
* to stream bytes.
|
|
84
|
+
*/
|
|
85
|
+
readonly readFile: (
|
|
86
|
+
options: ReadFileOptions,
|
|
87
|
+
) => PromiseLike<ReadableStream<Uint8Array> | null>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read one file from the sandbox as raw bytes. Resolves to `null` when the
|
|
91
|
+
* file does not exist.
|
|
92
|
+
*/
|
|
93
|
+
readonly readBinaryFile: (
|
|
94
|
+
options: ReadFileOptions,
|
|
95
|
+
) => PromiseLike<Uint8Array | null>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read one text file from the sandbox, decoded using the requested encoding.
|
|
99
|
+
* Resolves to `null` when the file does not exist.
|
|
100
|
+
*
|
|
101
|
+
* Line ranges are 1-based and inclusive. When `endLine` is past EOF the read
|
|
102
|
+
* returns through EOF without error.
|
|
103
|
+
*/
|
|
104
|
+
readonly readTextFile: (
|
|
105
|
+
options: ReadFileOptions & {
|
|
106
|
+
/**
|
|
107
|
+
* Text encoding used to decode the file bytes. Defaults to `"utf-8"`.
|
|
108
|
+
*/
|
|
109
|
+
encoding?: string;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 1-based inclusive start line. Defaults to 1.
|
|
113
|
+
*/
|
|
114
|
+
startLine?: number;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 1-based inclusive end line. When past the file's line count, the read
|
|
118
|
+
* returns through EOF without error.
|
|
119
|
+
*/
|
|
120
|
+
endLine?: number;
|
|
121
|
+
},
|
|
122
|
+
) => PromiseLike<string | null>;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Write one file to the sandbox from a stream of bytes. Creates parent
|
|
126
|
+
* directories recursively and overwrites any existing file.
|
|
127
|
+
*
|
|
128
|
+
* This is the lowest-level write primitive; prefer `writeBinaryFile` or
|
|
129
|
+
* `writeTextFile` when the full content is already materialized in memory.
|
|
130
|
+
*/
|
|
131
|
+
readonly writeFile: (
|
|
132
|
+
options: WriteFileOptions<ReadableStream<Uint8Array>>,
|
|
133
|
+
) => PromiseLike<void>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Write one file to the sandbox from raw bytes. Creates parent directories
|
|
137
|
+
* recursively and overwrites any existing file.
|
|
138
|
+
*/
|
|
139
|
+
readonly writeBinaryFile: (
|
|
140
|
+
options: WriteFileOptions<Uint8Array>,
|
|
141
|
+
) => PromiseLike<void>;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Write one file to the sandbox from a string, encoded using the requested
|
|
145
|
+
* encoding. Creates parent directories recursively and overwrites any
|
|
146
|
+
* existing file.
|
|
147
|
+
*/
|
|
148
|
+
readonly writeTextFile: (
|
|
149
|
+
options: WriteFileOptions<string> & {
|
|
150
|
+
/**
|
|
151
|
+
* Text encoding used to encode the string to bytes. Defaults to `"utf-8"`.
|
|
152
|
+
*/
|
|
153
|
+
encoding?: string;
|
|
154
|
+
},
|
|
155
|
+
) => PromiseLike<void>;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Spawn a long-running process in the sandbox. Returns immediately with a
|
|
159
|
+
* handle that streams stdout/stderr, can be waited on, and can be killed.
|
|
160
|
+
*
|
|
161
|
+
* `run` is conceptually a thin wrapper over this primitive: spawn,
|
|
162
|
+
* collect both streams to strings, await `wait()`, return the result.
|
|
163
|
+
*/
|
|
164
|
+
readonly spawn: (
|
|
165
|
+
options: SandboxProcessOptions,
|
|
166
|
+
) => PromiseLike<SandboxProcess>;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Run a command in the sandbox.
|
|
170
|
+
*/
|
|
171
|
+
readonly run: (options: SandboxProcessOptions) => PromiseLike<{
|
|
172
|
+
/**
|
|
173
|
+
* Exit code returned by the command.
|
|
174
|
+
*/
|
|
175
|
+
exitCode: number;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Standard output produced by the command.
|
|
179
|
+
*/
|
|
180
|
+
stdout: string;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Standard error produced by the command.
|
|
184
|
+
*/
|
|
185
|
+
stderr: string;
|
|
186
|
+
}>;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Handle to a long-running process started via `SandboxSession.spawn`.
|
|
191
|
+
*/
|
|
192
|
+
export type SandboxProcess = {
|
|
193
|
+
/**
|
|
194
|
+
* Process identifier, if the sandbox implementation exposes one.
|
|
195
|
+
*/
|
|
196
|
+
readonly pid?: number;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Stream of bytes written by the process to standard output.
|
|
200
|
+
*/
|
|
201
|
+
readonly stdout: ReadableStream<Uint8Array>;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Stream of bytes written by the process to standard error.
|
|
205
|
+
*/
|
|
206
|
+
readonly stderr: ReadableStream<Uint8Array>;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resolve when the process exits, yielding its exit code.
|
|
210
|
+
*/
|
|
211
|
+
wait(): PromiseLike<{ exitCode: number }>;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Terminate the process. Idempotent.
|
|
215
|
+
*/
|
|
216
|
+
kill(): PromiseLike<void>;
|
|
217
|
+
};
|
|
@@ -20,4 +20,10 @@ export type ToolApprovalRequest = {
|
|
|
20
20
|
* @default false
|
|
21
21
|
*/
|
|
22
22
|
isAutomatic?: boolean;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* HMAC-SHA256 signature binding this approval to its tool call.
|
|
26
|
+
* Present only when `experimental_toolApprovalSecret` is configured.
|
|
27
|
+
*/
|
|
28
|
+
signature?: string;
|
|
23
29
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Context } from './context';
|
|
2
2
|
import type { ModelMessage } from './model-message';
|
|
3
|
+
import type { SandboxSession } from './sandbox';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Additional options that are sent into each tool execution.
|
|
@@ -35,6 +36,11 @@ export interface ToolExecutionOptions<
|
|
|
35
36
|
* in `prepareStep` and update it there.
|
|
36
37
|
*/
|
|
37
38
|
context: CONTEXT;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The sandbox environment that the tool is operating in.
|
|
42
|
+
*/
|
|
43
|
+
experimental_sandbox?: SandboxSession;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
/**
|
package/src/types/tool.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import type { JSONValue,
|
|
1
|
+
import type { JSONValue, JSONObject } from '@ai-sdk/provider';
|
|
2
2
|
import type { FlexibleSchema } from '../schema';
|
|
3
3
|
import type { ToolResultOutput } from './content-part';
|
|
4
4
|
import type { Context } from './context';
|
|
5
|
+
import type { ExecutableTool } from './executable-tool';
|
|
5
6
|
import type { NeverOptional } from './never-optional';
|
|
6
7
|
import type { ProviderOptions } from './provider-options';
|
|
7
|
-
import type { SensitiveContext } from './sensitive-context';
|
|
8
8
|
import type {
|
|
9
9
|
ToolExecuteFunction,
|
|
10
10
|
ToolExecutionOptions,
|
|
11
11
|
} from './tool-execute-function';
|
|
12
12
|
import type { ToolNeedsApprovalFunction } from './tool-needs-approval-function';
|
|
13
|
+
import type { SandboxSession } from './sandbox';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Helper type to determine the outputSchema and execute function properties of a tool.
|
|
@@ -59,6 +60,8 @@ type BaseTool<
|
|
|
59
60
|
> = {
|
|
60
61
|
/**
|
|
61
62
|
* An optional title of the tool.
|
|
63
|
+
*
|
|
64
|
+
* @deprecated Use `providerMetadata` for source-specific tool display metadata.
|
|
62
65
|
*/
|
|
63
66
|
title?: string;
|
|
64
67
|
|
|
@@ -74,11 +77,11 @@ type BaseTool<
|
|
|
74
77
|
*
|
|
75
78
|
* Unlike `providerOptions`, this metadata is not sent to the language
|
|
76
79
|
* model. Instead, it is propagated onto the resulting tool call's
|
|
77
|
-
* `
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
+
* `toolMetadata` so consumers can read it from tool call / result parts
|
|
81
|
+
* and UI message parts. This is useful for sources of dynamic tools (e.g.
|
|
82
|
+
* an MCP server) to identify themselves.
|
|
80
83
|
*/
|
|
81
|
-
|
|
84
|
+
metadata?: JSONObject;
|
|
82
85
|
|
|
83
86
|
/**
|
|
84
87
|
* The schema of the input that the tool expects.
|
|
@@ -96,12 +99,6 @@ type BaseTool<
|
|
|
96
99
|
*/
|
|
97
100
|
contextSchema?: FlexibleSchema<CONTEXT>;
|
|
98
101
|
|
|
99
|
-
/**
|
|
100
|
-
* Marks top-level context properties that contain sensitive data and should be excluded from telemetry.
|
|
101
|
-
* Properties marked as `true` are omitted from telemetry integrations.
|
|
102
|
-
*/
|
|
103
|
-
sensitiveContext?: SensitiveContext<CONTEXT>;
|
|
104
|
-
|
|
105
102
|
/**
|
|
106
103
|
* Whether the tool needs approval before it can be executed.
|
|
107
104
|
*
|
|
@@ -180,10 +177,21 @@ type BaseFunctionTool<
|
|
|
180
177
|
CONTEXT extends Context | unknown | never = any,
|
|
181
178
|
> = BaseTool<INPUT, OUTPUT, CONTEXT> & {
|
|
182
179
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
180
|
+
* Optional description of what the tool does.
|
|
181
|
+
*
|
|
182
|
+
* Included in the tool definition sent to the language model so it can
|
|
183
|
+
* decide when and how to call the tool.
|
|
184
|
+
*
|
|
185
|
+
* Provide a string for a fixed description, or a function that returns a
|
|
186
|
+
* string from the current `context` (and optional `experimental_sandbox`) when the
|
|
187
|
+
* description should vary per call.
|
|
185
188
|
*/
|
|
186
|
-
description?:
|
|
189
|
+
description?:
|
|
190
|
+
| string
|
|
191
|
+
| ((options: {
|
|
192
|
+
context: NoInfer<CONTEXT>;
|
|
193
|
+
experimental_sandbox?: SandboxSession;
|
|
194
|
+
}) => string);
|
|
187
195
|
|
|
188
196
|
/**
|
|
189
197
|
* Strict mode setting for the tool.
|
|
@@ -208,7 +216,7 @@ type BaseFunctionTool<
|
|
|
208
216
|
};
|
|
209
217
|
|
|
210
218
|
/**
|
|
211
|
-
* Tool with user-defined input and output schemas.
|
|
219
|
+
* Tool with user-defined input and output schemas that is executed by the AI SDK.
|
|
212
220
|
*/
|
|
213
221
|
export type FunctionTool<
|
|
214
222
|
INPUT extends JSONValue | unknown | never = any,
|
|
@@ -219,8 +227,10 @@ export type FunctionTool<
|
|
|
219
227
|
};
|
|
220
228
|
|
|
221
229
|
/**
|
|
222
|
-
* Tool that is defined at runtime
|
|
230
|
+
* Tool that is defined at runtime.
|
|
223
231
|
* The types of input and output are not known at development time.
|
|
232
|
+
*
|
|
233
|
+
* For example, MCP tools that are not known at development time.
|
|
224
234
|
*/
|
|
225
235
|
export type DynamicTool<
|
|
226
236
|
INPUT extends JSONValue | unknown | never = any,
|
|
@@ -259,6 +269,8 @@ type BaseProviderTool<
|
|
|
259
269
|
/**
|
|
260
270
|
* Tool with provider-defined input and output schemas that is executed by the
|
|
261
271
|
* user.
|
|
272
|
+
*
|
|
273
|
+
* For example, shell tools that are executed in a local shell, but have provider-defined input and output schemas.
|
|
262
274
|
*/
|
|
263
275
|
export type ProviderDefinedTool<
|
|
264
276
|
INPUT extends JSONValue | unknown | never = any,
|
|
@@ -277,6 +289,8 @@ export type ProviderDefinedTool<
|
|
|
277
289
|
/**
|
|
278
290
|
* Tool with provider-defined input and output schemas that is executed by the
|
|
279
291
|
* provider.
|
|
292
|
+
*
|
|
293
|
+
* For example, web search tools and code execution tools that are executed by the provider itself.
|
|
280
294
|
*/
|
|
281
295
|
export type ProviderExecutedTool<
|
|
282
296
|
INPUT extends JSONValue | unknown | never = any,
|
|
@@ -325,8 +339,20 @@ export type Tool<
|
|
|
325
339
|
* Infer the tool type from a tool object.
|
|
326
340
|
*
|
|
327
341
|
* This is useful for type inference when working with tool objects.
|
|
342
|
+
*
|
|
343
|
+
* When the input has an `execute` function, the return type narrows to
|
|
344
|
+
* `ExecutableTool<Tool<...>>` so that `.execute` is non-nullable without
|
|
345
|
+
* needing `isExecutableTool` or a `!` assertion at the call site.
|
|
328
346
|
*/
|
|
329
|
-
// Note: overload order is important for auto-completion
|
|
347
|
+
// Note: overload order is important for auto-completion.
|
|
348
|
+
// The "with execute" overload comes first so calls that include an
|
|
349
|
+
// `execute` function get the narrowed return type. Calls without
|
|
350
|
+
// `execute` fall through to the overloads below.
|
|
351
|
+
export function tool<INPUT, OUTPUT, CONTEXT extends Context>(
|
|
352
|
+
tool: Tool<INPUT, OUTPUT, CONTEXT> & {
|
|
353
|
+
execute: ToolExecuteFunction<INPUT, OUTPUT, CONTEXT>;
|
|
354
|
+
},
|
|
355
|
+
): ExecutableTool<Tool<INPUT, OUTPUT, CONTEXT>>;
|
|
330
356
|
export function tool<INPUT, OUTPUT, CONTEXT extends Context>(
|
|
331
357
|
tool: Tool<INPUT, OUTPUT, CONTEXT>,
|
|
332
358
|
): Tool<INPUT, OUTPUT, CONTEXT>;
|
|
@@ -4,6 +4,10 @@ import { DownloadError } from './download-error';
|
|
|
4
4
|
* Validates that a URL is safe to download from, blocking private/internal addresses
|
|
5
5
|
* to prevent SSRF attacks.
|
|
6
6
|
*
|
|
7
|
+
* Note: this performs string/literal-IP checks only. It does not resolve DNS, so a
|
|
8
|
+
* hostname that resolves to a private address is not blocked here (see callers, which
|
|
9
|
+
* should additionally constrain egress at the network layer when handling untrusted URLs).
|
|
10
|
+
*
|
|
7
11
|
* @param url - The URL string to validate.
|
|
8
12
|
* @throws DownloadError if the URL is unsafe.
|
|
9
13
|
*/
|
|
@@ -31,7 +35,9 @@ export function validateDownloadUrl(url: string): void {
|
|
|
31
35
|
});
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
// Strip a trailing dot so a fully-qualified name like `localhost.` (which resolves
|
|
39
|
+
// identically to `localhost`) cannot bypass the hostname blocklist below.
|
|
40
|
+
const hostname = parsed.hostname.toLowerCase().replace(/\.+$/, '');
|
|
35
41
|
|
|
36
42
|
// Block empty hostname
|
|
37
43
|
if (!hostname) {
|
|
@@ -90,59 +96,135 @@ function isIPv4(hostname: string): boolean {
|
|
|
90
96
|
|
|
91
97
|
function isPrivateIPv4(ip: string): boolean {
|
|
92
98
|
const parts = ip.split('.').map(Number);
|
|
93
|
-
const [a, b] = parts;
|
|
99
|
+
const [a, b, c] = parts;
|
|
94
100
|
|
|
95
101
|
// 0.0.0.0/8
|
|
96
102
|
if (a === 0) return true;
|
|
97
103
|
// 10.0.0.0/8
|
|
98
104
|
if (a === 10) return true;
|
|
105
|
+
// 100.64.0.0/10 (CGNAT, used by some cloud providers for internal traffic)
|
|
106
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
99
107
|
// 127.0.0.0/8
|
|
100
108
|
if (a === 127) return true;
|
|
101
109
|
// 169.254.0.0/16
|
|
102
110
|
if (a === 169 && b === 254) return true;
|
|
103
111
|
// 172.16.0.0/12
|
|
104
112
|
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
113
|
+
// 192.0.0.0/24 (IETF protocol assignments)
|
|
114
|
+
if (a === 192 && b === 0 && c === 0) return true;
|
|
105
115
|
// 192.168.0.0/16
|
|
106
116
|
if (a === 192 && b === 168) return true;
|
|
117
|
+
// 198.18.0.0/15 (benchmarking)
|
|
118
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
119
|
+
// 240.0.0.0/4 (reserved, includes 255.255.255.255 broadcast)
|
|
120
|
+
if (a >= 240) return true;
|
|
107
121
|
|
|
108
122
|
return false;
|
|
109
123
|
}
|
|
110
124
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Expands an IPv6 address string into its 8 16-bit groups, handling `::`
|
|
127
|
+
* compression and an optional dotted-decimal IPv4 tail (e.g. `::ffff:127.0.0.1`).
|
|
128
|
+
*
|
|
129
|
+
* @returns the 8 groups, or null if the input is not a parseable IPv6 address.
|
|
130
|
+
*/
|
|
131
|
+
function parseIPv6(ip: string): number[] | null {
|
|
132
|
+
// Strip an optional zone id (e.g. `fe80::1%eth0`).
|
|
133
|
+
let address = ip.toLowerCase();
|
|
134
|
+
const zoneIndex = address.indexOf('%');
|
|
135
|
+
if (zoneIndex !== -1) {
|
|
136
|
+
address = address.slice(0, zoneIndex);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// At most one `::` compression marker is allowed.
|
|
140
|
+
const halves = address.split('::');
|
|
141
|
+
if (halves.length > 2) return null;
|
|
142
|
+
|
|
143
|
+
const toGroups = (segment: string): number[] | null => {
|
|
144
|
+
if (segment === '') return [];
|
|
145
|
+
const groups: number[] = [];
|
|
146
|
+
const parts = segment.split(':');
|
|
147
|
+
for (let i = 0; i < parts.length; i++) {
|
|
148
|
+
const part = parts[i];
|
|
149
|
+
// A dotted-decimal IPv4 tail is only valid as the final part.
|
|
150
|
+
if (part.includes('.')) {
|
|
151
|
+
if (i !== parts.length - 1 || !isIPv4(part)) return null;
|
|
152
|
+
const [a, b, c, d] = part.split('.').map(Number);
|
|
153
|
+
groups.push((a << 8) | b, (c << 8) | d);
|
|
154
|
+
continue;
|
|
137
155
|
}
|
|
156
|
+
if (!/^[0-9a-f]{1,4}$/.test(part)) return null;
|
|
157
|
+
groups.push(parseInt(part, 16));
|
|
138
158
|
}
|
|
159
|
+
return groups;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const head = toGroups(halves[0]);
|
|
163
|
+
if (head === null) return null;
|
|
164
|
+
|
|
165
|
+
if (halves.length === 2) {
|
|
166
|
+
const tail = toGroups(halves[1]);
|
|
167
|
+
if (tail === null) return null;
|
|
168
|
+
const fill = 8 - head.length - tail.length;
|
|
169
|
+
if (fill < 0) return null;
|
|
170
|
+
return [...head, ...new Array<number>(fill).fill(0), ...tail];
|
|
139
171
|
}
|
|
140
172
|
|
|
141
|
-
//
|
|
142
|
-
|
|
173
|
+
// No `::` compression: the address must contain exactly 8 groups.
|
|
174
|
+
return head.length === 8 ? head : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function isPrivateIPv6(ip: string): boolean {
|
|
178
|
+
const groups = parseIPv6(ip);
|
|
179
|
+
|
|
180
|
+
// Fail closed: if the address cannot be parsed, treat it as unsafe.
|
|
181
|
+
if (groups === null) return true;
|
|
182
|
+
|
|
183
|
+
const topZero = (count: number) =>
|
|
184
|
+
groups.slice(0, count).every(group => group === 0);
|
|
185
|
+
|
|
186
|
+
// ::1 (loopback) and :: (unspecified)
|
|
187
|
+
if (topZero(7) && (groups[7] === 0 || groups[7] === 1)) return true;
|
|
188
|
+
|
|
189
|
+
// fc00::/7 (unique local addresses)
|
|
190
|
+
if ((groups[0] & 0xfe00) === 0xfc00) return true;
|
|
143
191
|
|
|
144
192
|
// fe80::/10 (link-local)
|
|
145
|
-
if (
|
|
193
|
+
if ((groups[0] & 0xffc0) === 0xfe80) return true;
|
|
194
|
+
|
|
195
|
+
// fec0::/10 (site-local, deprecated but still routable internally)
|
|
196
|
+
if ((groups[0] & 0xffc0) === 0xfec0) return true;
|
|
197
|
+
|
|
198
|
+
// ff00::/8 (multicast)
|
|
199
|
+
if ((groups[0] & 0xff00) === 0xff00) return true;
|
|
200
|
+
|
|
201
|
+
// Addresses that embed an IPv4 address in their last 32 bits. For these we
|
|
202
|
+
// extract the embedded IPv4 and reuse the IPv4 private-range checks, so that
|
|
203
|
+
// e.g. ::ffff:127.0.0.1 or 64:ff9b::169.254.169.254 are blocked.
|
|
204
|
+
const embedsIPv4 =
|
|
205
|
+
// ::/96 — IPv4-compatible (deprecated)
|
|
206
|
+
topZero(6) ||
|
|
207
|
+
// ::ffff:0:0/96 — IPv4-mapped (ffff in group 5)
|
|
208
|
+
(topZero(5) && groups[5] === 0xffff) ||
|
|
209
|
+
// ::ffff:0:0/96 — IPv4-translated form (ffff in group 4, group 5 zero)
|
|
210
|
+
(topZero(4) && groups[4] === 0xffff && groups[5] === 0) ||
|
|
211
|
+
// 64:ff9b::/96 — NAT64 well-known prefix
|
|
212
|
+
(groups[0] === 0x0064 &&
|
|
213
|
+
groups[1] === 0xff9b &&
|
|
214
|
+
groups[2] === 0 &&
|
|
215
|
+
groups[3] === 0 &&
|
|
216
|
+
groups[4] === 0 &&
|
|
217
|
+
groups[5] === 0) ||
|
|
218
|
+
// 64:ff9b:1::/48 — NAT64 local-use prefix
|
|
219
|
+
(groups[0] === 0x0064 && groups[1] === 0xff9b && groups[2] === 0x0001);
|
|
220
|
+
|
|
221
|
+
if (embedsIPv4) {
|
|
222
|
+
const a = (groups[6] >> 8) & 0xff;
|
|
223
|
+
const b = groups[6] & 0xff;
|
|
224
|
+
const c = (groups[7] >> 8) & 0xff;
|
|
225
|
+
const d = groups[7] & 0xff;
|
|
226
|
+
return isPrivateIPv4(`${a}.${b}.${c}.${d}`);
|
|
227
|
+
}
|
|
146
228
|
|
|
147
229
|
return false;
|
|
148
230
|
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { Context } from './context';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Top-level context properties that contain sensitive data and should be
|
|
5
|
-
* excluded from telemetry.
|
|
6
|
-
*/
|
|
7
|
-
export type SensitiveContext<CONTEXT extends Context | unknown | never> =
|
|
8
|
-
| { [KEY in keyof CONTEXT]?: boolean }
|
|
9
|
-
| undefined;
|