ai 6.0.201 → 6.0.202

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.
@@ -152,7 +152,7 @@ function detectMediaType({
152
152
  var import_provider_utils2 = require("@ai-sdk/provider-utils");
153
153
 
154
154
  // src/version.ts
155
- var VERSION = true ? "6.0.201" : "0.0.0-test";
155
+ var VERSION = true ? "6.0.202" : "0.0.0-test";
156
156
 
157
157
  // src/util/download/download.ts
158
158
  var download = async ({
@@ -131,7 +131,7 @@ import {
131
131
  } from "@ai-sdk/provider-utils";
132
132
 
133
133
  // src/version.ts
134
- var VERSION = true ? "6.0.201" : "0.0.0-test";
134
+ var VERSION = true ? "6.0.202" : "0.0.0-test";
135
135
 
136
136
  // src/util/download/download.ts
137
137
  var download = async ({
@@ -124,6 +124,38 @@ transports by providing an `authProvider`.
124
124
 
125
125
  </Note>
126
126
 
127
+ ### OAuth Authorization Server Validation
128
+
129
+ When using MCP OAuth in server-side applications, the MCP server can advertise
130
+ the OAuth authorization server to use. If you connect to MCP servers outside
131
+ your control, implement `validateAuthorizationServerURL` on your
132
+ `authProvider` to allow only the authorization server origins you trust:
133
+
134
+ ```typescript
135
+ const allowedAuthorizationServerOrigins = new Set([
136
+ 'https://accounts.example.com',
137
+ 'https://tenant.auth0.com',
138
+ ]);
139
+
140
+ const myOAuthClientProvider = {
141
+ // ...other OAuthClientProvider methods
142
+
143
+ validateAuthorizationServerURL(serverUrl, authorizationServerUrl) {
144
+ const origin = new URL(authorizationServerUrl).origin;
145
+
146
+ if (!allowedAuthorizationServerOrigins.has(origin)) {
147
+ throw new Error(
148
+ `Unexpected OAuth authorization server for ${serverUrl}: ${origin}`,
149
+ );
150
+ }
151
+ },
152
+ };
153
+ ```
154
+
155
+ This hook is called before the SDK fetches authorization server metadata, so a
156
+ rejected URL is not requested. It is optional and does not change existing OAuth
157
+ flows unless you implement it.
158
+
127
159
  ### Closing the MCP Client
128
160
 
129
161
  After initialization, you should close the MCP client based on your usage pattern:
@@ -40,6 +40,9 @@ import { existsSync, mkdirSync } from 'fs';
40
40
  import { writeFile } from 'fs/promises';
41
41
  import path from 'path';
42
42
 
43
+ // Treat chat IDs as opaque tokens before using them in file paths.
44
+ const chatIdRegex = /^[A-Za-z0-9_-]+$/;
45
+
43
46
  export async function createChat(): Promise<string> {
44
47
  const id = generateId(); // generate a unique chat ID
45
48
  await writeFile(getChatFile(id), '[]'); // create an empty chat file
@@ -47,12 +50,27 @@ export async function createChat(): Promise<string> {
47
50
  }
48
51
 
49
52
  function getChatFile(id: string): string {
50
- const chatDir = path.join(process.cwd(), '.chats');
53
+ if (!chatIdRegex.test(id)) {
54
+ throw new Error('Invalid chat ID');
55
+ }
56
+
57
+ const chatDir = path.resolve(process.cwd(), '.chats');
58
+ const chatFile = path.resolve(chatDir, `${id}.json`);
59
+
60
+ // Defense in depth: keep the resolved file inside the chat directory.
61
+ if (!chatFile.startsWith(`${chatDir}${path.sep}`)) {
62
+ throw new Error('Invalid chat ID');
63
+ }
64
+
51
65
  if (!existsSync(chatDir)) mkdirSync(chatDir, { recursive: true });
52
- return path.join(chatDir, `${id}.json`);
66
+ return chatFile;
53
67
  }
54
68
  ```
55
69
 
70
+ The chat ID can come from the URL or the request body, so validate it as an
71
+ opaque token before using it in a file path. The resolved path check ensures the
72
+ file stays inside the intended `.chats` directory.
73
+
56
74
  ## Loading an existing chat
57
75
 
58
76
  When the user navigates to the chat page with a chat ID, we need to load the chat messages from storage.
@@ -106,7 +106,7 @@ It currently does not support accepting notifications from an MCP server, and cu
106
106
  type: 'OAuthClientProvider',
107
107
  isOptional: true,
108
108
  description:
109
- 'Optional OAuth provider for authorization to access protected remote MCP servers.',
109
+ 'Optional OAuth provider for authorization to access protected remote MCP servers. Implement `validateAuthorizationServerURL` on the provider to allowlist discovered OAuth authorization server URLs before metadata is fetched.',
110
110
  },
111
111
  {
112
112
  name: 'redirect',
@@ -0,0 +1,31 @@
1
+ ---
2
+ title: AI_InvalidToolApprovalSignatureError
3
+ description: Learn how to fix AI_InvalidToolApprovalSignatureError
4
+ ---
5
+
6
+ # AI_InvalidToolApprovalSignatureError
7
+
8
+ This error occurs when `experimental_toolApprovalSecret` is configured and a tool approval replayed from the message history has a missing or invalid HMAC signature. The server rejects the approval before the tool executes.
9
+
10
+ Common causes:
11
+
12
+ - The approval was fabricated by the client (no signature present)
13
+ - The tool arguments were modified after the approval was signed
14
+ - The server secret changed between issuance and verification (e.g. after key rotation without overlap)
15
+
16
+ ## Properties
17
+
18
+ - `approvalId`: The approval ID that failed verification
19
+ - `toolCallId`: The tool call ID the approval was for
20
+
21
+ ## Checking for this Error
22
+
23
+ You can check if an error is an instance of `AI_InvalidToolApprovalSignatureError` using:
24
+
25
+ ```typescript
26
+ import { InvalidToolApprovalSignatureError } from 'ai';
27
+
28
+ if (InvalidToolApprovalSignatureError.isInstance(error)) {
29
+ // Handle the error
30
+ }
31
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "6.0.201",
3
+ "version": "6.0.202",
4
4
  "description": "AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -45,9 +45,9 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@opentelemetry/api": "^1.9.0",
48
- "@ai-sdk/gateway": "3.0.127",
49
- "@ai-sdk/provider": "3.0.10",
50
- "@ai-sdk/provider-utils": "4.0.27"
48
+ "@ai-sdk/gateway": "3.0.128",
49
+ "@ai-sdk/provider-utils": "4.0.28",
50
+ "@ai-sdk/provider": "3.0.10"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@edge-runtime/vm": "^5.0.0",
@@ -58,8 +58,8 @@
58
58
  "tsx": "^4.19.2",
59
59
  "typescript": "5.8.3",
60
60
  "zod": "3.25.76",
61
- "@ai-sdk/test-server": "1.0.5",
62
- "@vercel/ai-tsconfig": "0.0.0"
61
+ "@vercel/ai-tsconfig": "0.0.0",
62
+ "@ai-sdk/test-server": "1.0.5"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "zod": "^3.25.76 || ^4.1.8"
@@ -17,6 +17,7 @@ export {
17
17
  export { InvalidArgumentError } from './invalid-argument-error';
18
18
  export { InvalidStreamPartError } from './invalid-stream-part-error';
19
19
  export { InvalidToolApprovalError } from './invalid-tool-approval-error';
20
+ export { InvalidToolApprovalSignatureError } from './invalid-tool-approval-signature-error';
20
21
  export { InvalidToolInputError } from './invalid-tool-input-error';
21
22
  export { ToolCallNotFoundForApprovalError } from './tool-call-not-found-for-approval-error';
22
23
  export { MissingToolResultsError } from './missing-tool-result-error';
@@ -0,0 +1,35 @@
1
+ import { AISDKError } from '@ai-sdk/provider';
2
+
3
+ const name = 'AI_InvalidToolApprovalSignatureError';
4
+ const marker = `vercel.ai.error.${name}`;
5
+ const symbol = Symbol.for(marker);
6
+
7
+ export class InvalidToolApprovalSignatureError extends AISDKError {
8
+ private readonly [symbol] = true;
9
+
10
+ readonly approvalId: string;
11
+ readonly toolCallId: string;
12
+
13
+ constructor({
14
+ approvalId,
15
+ toolCallId,
16
+ reason,
17
+ }: {
18
+ approvalId: string;
19
+ toolCallId: string;
20
+ reason: string;
21
+ }) {
22
+ super({
23
+ name,
24
+ message: `Tool approval signature verification failed for approval "${approvalId}" (tool call "${toolCallId}"): ${reason}`,
25
+ });
26
+ this.approvalId = approvalId;
27
+ this.toolCallId = toolCallId;
28
+ }
29
+
30
+ static isInstance(
31
+ error: unknown,
32
+ ): error is InvalidToolApprovalSignatureError {
33
+ return AISDKError.hasMarker(error, marker);
34
+ }
35
+ }
@@ -69,6 +69,8 @@ import { extractTextContent } from './extract-text-content';
69
69
  import type { GenerateTextResult } from './generate-text-result';
70
70
  import { DefaultGeneratedFile } from './generated-file';
71
71
  import { isApprovalNeeded } from './is-approval-needed';
72
+ import { maybeSignApproval } from './tool-approval-signature';
73
+ import { validateApprovedToolApprovals } from './validate-tool-approvals';
72
74
  import { text, type Output } from './output';
73
75
  import type { InferCompleteOutput } from './output-utils';
74
76
  import { parseToolCall } from './parse-tool-call';
@@ -270,6 +272,7 @@ export async function generateText<
270
272
  experimental_repairToolCall: repairToolCall,
271
273
  experimental_download: download,
272
274
  experimental_context,
275
+ experimental_toolApprovalSecret,
273
276
  experimental_include: include,
274
277
  _internal: { generateId = originalGenerateId } = {},
275
278
  experimental_onStart: onStart,
@@ -411,6 +414,15 @@ export async function generateText<
411
414
  */
412
415
  experimental_context?: unknown;
413
416
 
417
+ /**
418
+ * Secret for HMAC-signing tool approval requests. When set, the server
419
+ * signs each approval request at issuance and verifies the signature when
420
+ * the approval is replayed, preventing client-forged approvals.
421
+ *
422
+ * Experimental (can break in patch releases).
423
+ */
424
+ experimental_toolApprovalSecret?: string | Uint8Array;
425
+
414
426
  /**
415
427
  * Settings for controlling what data is included in step results.
416
428
  * Disabling inclusion can help reduce memory usage when processing
@@ -548,12 +560,32 @@ export async function generateText<
548
560
  const initialMessages = initialPrompt.messages;
549
561
  const responseMessages: Array<ResponseMessage> = [];
550
562
 
551
- const { approvedToolApprovals, deniedToolApprovals } =
552
- collectToolApprovals<TOOLS>({ messages: initialMessages });
563
+ const {
564
+ approvedToolApprovals,
565
+ deniedToolApprovals: collectedDeniedToolApprovals,
566
+ } = collectToolApprovals<TOOLS>({ messages: initialMessages });
567
+
568
+ // Re-validate approvals reconstructed from the client-supplied message
569
+ // history before executing them: verify the HMAC signature (when a
570
+ // secret is configured), re-validate the input against the tool's
571
+ // schema, and re-resolve whether the tool requires approval.
572
+ const {
573
+ approvedToolApprovals: localApprovedToolApprovals,
574
+ deniedToolApprovals: revalidationDeniedToolApprovals,
575
+ } = await validateApprovedToolApprovals<TOOLS>({
576
+ approvedToolApprovals: approvedToolApprovals.filter(
577
+ toolApproval => !toolApproval.toolCall.providerExecuted,
578
+ ),
579
+ tools,
580
+ messages: initialMessages,
581
+ experimental_context,
582
+ toolApprovalSecret: experimental_toolApprovalSecret,
583
+ });
553
584
 
554
- const localApprovedToolApprovals = approvedToolApprovals.filter(
555
- toolApproval => !toolApproval.toolCall.providerExecuted,
556
- );
585
+ const deniedToolApprovals = [
586
+ ...collectedDeniedToolApprovals,
587
+ ...revalidationDeniedToolApprovals,
588
+ ];
557
589
 
558
590
  if (
559
591
  deniedToolApprovals.length > 0 ||
@@ -926,10 +958,20 @@ export async function generateText<
926
958
  experimental_context,
927
959
  })
928
960
  ) {
961
+ const approvalId = generateId();
962
+ const signature = await maybeSignApproval({
963
+ secret: experimental_toolApprovalSecret,
964
+ approvalId,
965
+ toolCallId: toolCall.toolCallId,
966
+ toolName: toolCall.toolName,
967
+ input: toolCall.input,
968
+ });
969
+
929
970
  toolApprovalRequests[toolCall.toolCallId] = {
930
971
  type: 'tool-approval-request',
931
- approvalId: generateId(),
972
+ approvalId,
932
973
  toolCall,
974
+ ...(signature != null ? { signature } : {}),
933
975
  };
934
976
  }
935
977
  }
@@ -28,6 +28,7 @@ import {
28
28
  type GeneratedFile,
29
29
  } from './generated-file';
30
30
  import { isApprovalNeeded } from './is-approval-needed';
31
+ import { maybeSignApproval } from './tool-approval-signature';
31
32
  import { parseToolCall } from './parse-tool-call';
32
33
  import type { ToolApprovalRequestOutput } from './tool-approval-request-output';
33
34
  import type { TypedToolCall } from './tool-call';
@@ -128,6 +129,7 @@ export function runToolsTransformation<TOOLS extends ToolSet>({
128
129
  abortSignal,
129
130
  repairToolCall,
130
131
  experimental_context,
132
+ toolApprovalSecret,
131
133
  generateId,
132
134
  stepNumber,
133
135
  model,
@@ -143,6 +145,7 @@ export function runToolsTransformation<TOOLS extends ToolSet>({
143
145
  abortSignal: AbortSignal | undefined;
144
146
  repairToolCall: ToolCallRepairFunction<TOOLS> | undefined;
145
147
  experimental_context: unknown;
148
+ toolApprovalSecret?: string | Uint8Array;
146
149
  generateId: IdGenerator;
147
150
  stepNumber?: number;
148
151
  model?: { provider: string; modelId: string };
@@ -328,10 +331,20 @@ export function runToolsTransformation<TOOLS extends ToolSet>({
328
331
  experimental_context,
329
332
  })
330
333
  ) {
334
+ const approvalId = generateId();
335
+ const signature = await maybeSignApproval({
336
+ secret: toolApprovalSecret,
337
+ approvalId,
338
+ toolCallId: toolCall.toolCallId,
339
+ toolName: toolCall.toolName,
340
+ input: toolCall.input,
341
+ });
342
+
331
343
  toolResultsStreamController!.enqueue({
332
344
  type: 'tool-approval-request',
333
- approvalId: generateId(),
345
+ approvalId,
334
346
  toolCall,
347
+ ...(signature != null ? { signature } : {}),
335
348
  });
336
349
  break;
337
350
  }
@@ -82,6 +82,7 @@ import { mergeObjects } from '../util/merge-objects';
82
82
  import { now as originalNow } from '../util/now';
83
83
  import { prepareRetries } from '../util/prepare-retries';
84
84
  import { collectToolApprovals } from './collect-tool-approvals';
85
+ import { validateApprovedToolApprovals } from './validate-tool-approvals';
85
86
  import type {
86
87
  OnFinishEvent,
87
88
  OnStartEvent,
@@ -359,6 +360,7 @@ export function streamText<
359
360
  experimental_onToolCallStart: onToolCallStart,
360
361
  experimental_onToolCallFinish: onToolCallFinish,
361
362
  experimental_context,
363
+ experimental_toolApprovalSecret,
362
364
  experimental_include: include,
363
365
  _internal: { now = originalNow, generateId = originalGenerateId } = {},
364
366
  ...settings
@@ -532,6 +534,15 @@ export function streamText<
532
534
  */
533
535
  experimental_context?: unknown;
534
536
 
537
+ /**
538
+ * Secret for HMAC-signing tool approval requests. When set, the server
539
+ * signs each approval request at issuance and verifies the signature when
540
+ * the approval is replayed, preventing client-forged approvals.
541
+ *
542
+ * Experimental (can break in patch releases).
543
+ */
544
+ experimental_toolApprovalSecret?: string | Uint8Array;
545
+
535
546
  /**
536
547
  * Settings for controlling what data is included in step results.
537
548
  * Disabling inclusion can help reduce memory usage when processing
@@ -608,6 +619,7 @@ export function streamText<
608
619
  now,
609
620
  generateId,
610
621
  experimental_context,
622
+ experimental_toolApprovalSecret,
611
623
  download,
612
624
  include,
613
625
  });
@@ -792,6 +804,7 @@ class DefaultStreamTextResult<
792
804
  onToolCallStart,
793
805
  onToolCallFinish,
794
806
  experimental_context,
807
+ experimental_toolApprovalSecret,
795
808
  download,
796
809
  include,
797
810
  }: {
@@ -828,6 +841,7 @@ class DefaultStreamTextResult<
828
841
  | undefined;
829
842
  originalAbortSignal: AbortSignal | undefined;
830
843
  experimental_context: unknown;
844
+ experimental_toolApprovalSecret: string | Uint8Array | undefined;
831
845
  download: DownloadFunction | undefined;
832
846
  include: { requestBody?: boolean } | undefined;
833
847
 
@@ -1402,12 +1416,29 @@ class DefaultStreamTextResult<
1402
1416
  deniedToolApprovals.length > 0 ||
1403
1417
  approvedToolApprovals.length > 0
1404
1418
  ) {
1405
- const localApprovedToolApprovals = approvedToolApprovals.filter(
1406
- toolApproval => !toolApproval.toolCall.providerExecuted,
1407
- );
1408
- const localDeniedToolApprovals = deniedToolApprovals.filter(
1409
- toolApproval => !toolApproval.toolCall.providerExecuted,
1410
- );
1419
+ // Re-validate approvals reconstructed from the client-supplied
1420
+ // message history before executing them: verify the HMAC signature
1421
+ // (when a secret is configured), re-validate the input against the
1422
+ // tool's schema, and re-resolve whether the tool requires approval.
1423
+ const {
1424
+ approvedToolApprovals: localApprovedToolApprovals,
1425
+ deniedToolApprovals: revalidationDeniedToolApprovals,
1426
+ } = await validateApprovedToolApprovals<TOOLS>({
1427
+ approvedToolApprovals: approvedToolApprovals.filter(
1428
+ toolApproval => !toolApproval.toolCall.providerExecuted,
1429
+ ),
1430
+ tools,
1431
+ messages: initialMessages,
1432
+ experimental_context,
1433
+ toolApprovalSecret: experimental_toolApprovalSecret,
1434
+ });
1435
+
1436
+ const localDeniedToolApprovals = [
1437
+ ...deniedToolApprovals.filter(
1438
+ toolApproval => !toolApproval.toolCall.providerExecuted,
1439
+ ),
1440
+ ...revalidationDeniedToolApprovals,
1441
+ ];
1411
1442
 
1412
1443
  const deniedProviderExecutedToolApprovals =
1413
1444
  deniedToolApprovals.filter(
@@ -1728,6 +1759,7 @@ class DefaultStreamTextResult<
1728
1759
  repairToolCall,
1729
1760
  abortSignal,
1730
1761
  experimental_context,
1762
+ toolApprovalSecret: experimental_toolApprovalSecret,
1731
1763
  generateId,
1732
1764
  stepNumber: recordedSteps.length,
1733
1765
  model: stepModelInfo,
@@ -2688,6 +2720,9 @@ class DefaultStreamTextResult<
2688
2720
  type: 'tool-approval-request',
2689
2721
  approvalId: part.approvalId,
2690
2722
  toolCallId: part.toolCall.toolCallId,
2723
+ ...(part.signature != null
2724
+ ? { signature: part.signature }
2725
+ : {}),
2691
2726
  });
2692
2727
  break;
2693
2728
  }
@@ -113,6 +113,7 @@ export async function toResponseMessages<TOOLS extends ToolSet>({
113
113
  type: 'tool-approval-request',
114
114
  approvalId: part.approvalId,
115
115
  toolCallId: part.toolCall.toolCallId,
116
+ ...(part.signature != null ? { signature: part.signature } : {}),
116
117
  });
117
118
  break;
118
119
  }
@@ -18,4 +18,9 @@ export type ToolApprovalRequestOutput<TOOLS extends ToolSet> = {
18
18
  * Tool call that the approval request is for.
19
19
  */
20
20
  toolCall: TypedToolCall<TOOLS>;
21
+
22
+ /**
23
+ * HMAC-SHA256 signature binding this approval request to its tool call.
24
+ */
25
+ signature?: string;
21
26
  };
@@ -0,0 +1,125 @@
1
+ import {
2
+ convertBase64ToUint8Array,
3
+ convertUint8ArrayToBase64,
4
+ } from '@ai-sdk/provider-utils';
5
+
6
+ const encoder = new TextEncoder();
7
+
8
+ function canonicalJSON(value: unknown): string {
9
+ if (value === null || value === undefined) {
10
+ return JSON.stringify(value);
11
+ }
12
+ if (typeof value !== 'object') {
13
+ return JSON.stringify(value);
14
+ }
15
+ if (Array.isArray(value)) {
16
+ return `[${value.map(canonicalJSON).join(',')}]`;
17
+ }
18
+ const keys = Object.keys(value as Record<string, unknown>).sort();
19
+ const entries = keys.map(
20
+ k =>
21
+ `${JSON.stringify(k)}:${canonicalJSON((value as Record<string, unknown>)[k])}`,
22
+ );
23
+ return `{${entries.join(',')}}`;
24
+ }
25
+
26
+ function toBase64url(bytes: Uint8Array): string {
27
+ return convertUint8ArrayToBase64(bytes)
28
+ .replace(/\+/g, '-')
29
+ .replace(/\//g, '_')
30
+ .replace(/=+$/g, '');
31
+ }
32
+
33
+ function fromBase64url(str: string): Uint8Array {
34
+ return convertBase64ToUint8Array(str);
35
+ }
36
+
37
+ async function importKey(secret: string | Uint8Array): Promise<CryptoKey> {
38
+ const keyData = typeof secret === 'string' ? encoder.encode(secret) : secret;
39
+ return crypto.subtle.importKey(
40
+ 'raw',
41
+ keyData,
42
+ { name: 'HMAC', hash: 'SHA-256' },
43
+ false,
44
+ ['sign', 'verify'],
45
+ );
46
+ }
47
+
48
+ async function hashInput(input: unknown): Promise<string> {
49
+ const canonical = canonicalJSON(input);
50
+ const digest = await crypto.subtle.digest(
51
+ 'SHA-256',
52
+ encoder.encode(canonical),
53
+ );
54
+ return toBase64url(new Uint8Array(digest));
55
+ }
56
+
57
+ function buildPayload(
58
+ approvalId: string,
59
+ toolCallId: string,
60
+ toolName: string,
61
+ inputDigest: string,
62
+ ): Uint8Array {
63
+ return encoder.encode(
64
+ `${approvalId}\n${toolCallId}\n${toolName}\n${inputDigest}`,
65
+ );
66
+ }
67
+
68
+ export async function signToolApproval({
69
+ secret,
70
+ approvalId,
71
+ toolCallId,
72
+ toolName,
73
+ input,
74
+ }: {
75
+ secret: string | Uint8Array;
76
+ approvalId: string;
77
+ toolCallId: string;
78
+ toolName: string;
79
+ input: unknown;
80
+ }): Promise<string> {
81
+ const key = await importKey(secret);
82
+ const inputDigest = await hashInput(input);
83
+ const payload = buildPayload(approvalId, toolCallId, toolName, inputDigest);
84
+ const sig = await crypto.subtle.sign('HMAC', key, payload);
85
+ return toBase64url(new Uint8Array(sig));
86
+ }
87
+
88
+ export async function verifyToolApprovalSignature({
89
+ secret,
90
+ signature,
91
+ approvalId,
92
+ toolCallId,
93
+ toolName,
94
+ input,
95
+ }: {
96
+ secret: string | Uint8Array;
97
+ signature: string;
98
+ approvalId: string;
99
+ toolCallId: string;
100
+ toolName: string;
101
+ input: unknown;
102
+ }): Promise<boolean> {
103
+ const key = await importKey(secret);
104
+ const inputDigest = await hashInput(input);
105
+ const payload = buildPayload(approvalId, toolCallId, toolName, inputDigest);
106
+ const sigBytes = fromBase64url(signature);
107
+ return crypto.subtle.verify('HMAC', key, sigBytes, payload);
108
+ }
109
+
110
+ export async function maybeSignApproval({
111
+ secret,
112
+ approvalId,
113
+ toolCallId,
114
+ toolName,
115
+ input,
116
+ }: {
117
+ secret: string | Uint8Array | undefined;
118
+ approvalId: string;
119
+ toolCallId: string;
120
+ toolName: string;
121
+ input: unknown;
122
+ }): Promise<string | undefined> {
123
+ if (secret == null) return undefined;
124
+ return signToolApproval({ secret, approvalId, toolCallId, toolName, input });
125
+ }