agents 0.10.0 → 0.10.2
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/dist/chat/index.js +24 -4
- package/dist/chat/index.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/compaction-helpers-BPE1_ziA.js.map +1 -1
- package/dist/compaction-helpers-BdQbZiML.d.ts +441 -0
- package/dist/email.js +1 -2
- package/dist/email.js.map +1 -1
- package/dist/experimental/memory/session/index.d.ts +61 -268
- package/dist/experimental/memory/session/index.js +238 -34
- package/dist/experimental/memory/session/index.js.map +1 -1
- package/dist/experimental/memory/utils/index.d.ts +3 -4
- package/dist/experimental/memory/utils/index.js.map +1 -1
- package/dist/{index-BPkkIqMn.d.ts → index-D2lfljd3.d.ts} +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/client.d.ts +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +3 -12
- package/dist/mcp/index.js.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js.map +1 -1
- package/dist/{src-B8NZxxsO.js → src-f7-4oW_C.js} +6 -6
- package/dist/{src-B8NZxxsO.js.map → src-f7-4oW_C.js.map} +1 -1
- package/dist/workflows.d.ts +1 -1
- package/dist/workflows.js +1 -1
- package/package.json +7 -7
- package/dist/compaction-helpers-CHNQeyRm.d.ts +0 -139
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { ToolSet } from "ai";
|
|
2
|
+
|
|
3
|
+
//#region src/experimental/memory/session/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Minimal message part shape used by Session internals.
|
|
6
|
+
* Vercel AI SDK's `UIMessagePart` is structurally compatible.
|
|
7
|
+
*/
|
|
8
|
+
interface SessionMessagePart {
|
|
9
|
+
type: string;
|
|
10
|
+
text?: string;
|
|
11
|
+
toolCallId?: string;
|
|
12
|
+
toolName?: string;
|
|
13
|
+
input?: unknown;
|
|
14
|
+
output?: unknown;
|
|
15
|
+
state?: string;
|
|
16
|
+
result?: unknown;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Minimal message shape used by Session internals.
|
|
20
|
+
* Vercel AI SDK's `UIMessage` is structurally compatible — you can pass
|
|
21
|
+
* `UIMessage` objects directly without conversion.
|
|
22
|
+
*/
|
|
23
|
+
interface SessionMessage {
|
|
24
|
+
id: string;
|
|
25
|
+
role: string;
|
|
26
|
+
parts: SessionMessagePart[];
|
|
27
|
+
createdAt?: Date;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Options for creating a Session.
|
|
31
|
+
*/
|
|
32
|
+
interface SessionOptions {
|
|
33
|
+
/** Context blocks for the system prompt. */
|
|
34
|
+
context?: ContextConfig[];
|
|
35
|
+
/** Provider for persisting the frozen system prompt. */
|
|
36
|
+
promptStore?: WritableContextProvider;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/experimental/memory/session/provider.d.ts
|
|
40
|
+
interface SearchResult {
|
|
41
|
+
id: string;
|
|
42
|
+
role: string;
|
|
43
|
+
content: string;
|
|
44
|
+
createdAt?: string;
|
|
45
|
+
sessionId?: string;
|
|
46
|
+
}
|
|
47
|
+
interface StoredCompaction {
|
|
48
|
+
id: string;
|
|
49
|
+
summary: string;
|
|
50
|
+
fromMessageId: string;
|
|
51
|
+
toMessageId: string;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Session storage provider.
|
|
56
|
+
* Messages are tree-structured via parentId for branching.
|
|
57
|
+
*/
|
|
58
|
+
interface SessionProvider {
|
|
59
|
+
getMessage(id: string): SessionMessage | null;
|
|
60
|
+
/**
|
|
61
|
+
* Get conversation as a path from root to leaf.
|
|
62
|
+
* Applies compaction overlays. If leafId is null, uses the latest leaf.
|
|
63
|
+
*/
|
|
64
|
+
getHistory(leafId?: string | null): SessionMessage[];
|
|
65
|
+
getLatestLeaf(): SessionMessage | null;
|
|
66
|
+
getBranches(messageId: string): SessionMessage[];
|
|
67
|
+
getPathLength(leafId?: string | null): number;
|
|
68
|
+
/**
|
|
69
|
+
* Append a message. Parented to the latest leaf unless parentId is provided.
|
|
70
|
+
* Idempotent — same message.id twice is a no-op.
|
|
71
|
+
*/
|
|
72
|
+
appendMessage(message: SessionMessage, parentId?: string | null): void;
|
|
73
|
+
updateMessage(message: SessionMessage): void;
|
|
74
|
+
deleteMessages(messageIds: string[]): void;
|
|
75
|
+
clearMessages(): void;
|
|
76
|
+
addCompaction(
|
|
77
|
+
summary: string,
|
|
78
|
+
fromMessageId: string,
|
|
79
|
+
toMessageId: string
|
|
80
|
+
): StoredCompaction;
|
|
81
|
+
getCompactions(): StoredCompaction[];
|
|
82
|
+
searchMessages?(query: string, limit?: number): SearchResult[];
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/experimental/memory/session/providers/agent.d.ts
|
|
86
|
+
interface SqlProvider {
|
|
87
|
+
sql<T = Record<string, string | number | boolean | null>>(
|
|
88
|
+
strings: TemplateStringsArray,
|
|
89
|
+
...values: (string | number | boolean | null)[]
|
|
90
|
+
): T[];
|
|
91
|
+
}
|
|
92
|
+
declare class AgentSessionProvider implements SessionProvider {
|
|
93
|
+
private agent;
|
|
94
|
+
private initialized;
|
|
95
|
+
private sessionId;
|
|
96
|
+
/**
|
|
97
|
+
* @param agent - Agent or any object with a `sql` tagged template method
|
|
98
|
+
* @param sessionId - Optional session ID to isolate multiple sessions in the same DO.
|
|
99
|
+
* Messages are filtered by session_id within shared tables.
|
|
100
|
+
*/
|
|
101
|
+
constructor(agent: SqlProvider, sessionId?: string);
|
|
102
|
+
private ensureTable;
|
|
103
|
+
getMessage(id: string): SessionMessage | null;
|
|
104
|
+
getHistory(leafId?: string | null): SessionMessage[];
|
|
105
|
+
getLatestLeaf(): SessionMessage | null;
|
|
106
|
+
getBranches(messageId: string): SessionMessage[];
|
|
107
|
+
getPathLength(leafId?: string | null): number;
|
|
108
|
+
appendMessage(message: SessionMessage, parentId?: string | null): void;
|
|
109
|
+
updateMessage(message: SessionMessage): void;
|
|
110
|
+
deleteMessages(messageIds: string[]): void;
|
|
111
|
+
clearMessages(): void;
|
|
112
|
+
addCompaction(
|
|
113
|
+
summary: string,
|
|
114
|
+
fromMessageId: string,
|
|
115
|
+
toMessageId: string
|
|
116
|
+
): StoredCompaction;
|
|
117
|
+
getCompactions(): StoredCompaction[];
|
|
118
|
+
searchMessages(query: string, limit?: number): SearchResult[];
|
|
119
|
+
private latestLeafRow;
|
|
120
|
+
private indexFTS;
|
|
121
|
+
private deleteFTS;
|
|
122
|
+
private applyCompactions;
|
|
123
|
+
private parse;
|
|
124
|
+
private parseRows;
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/experimental/memory/session/search.d.ts
|
|
128
|
+
/**
|
|
129
|
+
* Storage interface for searchable context.
|
|
130
|
+
*
|
|
131
|
+
* - `get()` returns a summary of indexed content (rendered into system prompt)
|
|
132
|
+
* - `search(query)` full-text search (via search_context tool)
|
|
133
|
+
* - `set(key, content)` indexes content under a key (via set_context tool)
|
|
134
|
+
*/
|
|
135
|
+
interface SearchProvider extends ContextProvider {
|
|
136
|
+
search(query: string): Promise<string | null>;
|
|
137
|
+
set?(key: string, content: string): Promise<void>;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Check if a provider is a SearchProvider (has a `search` method).
|
|
141
|
+
*/
|
|
142
|
+
declare function isSearchProvider(
|
|
143
|
+
provider: unknown
|
|
144
|
+
): provider is SearchProvider;
|
|
145
|
+
/**
|
|
146
|
+
* SearchProvider backed by Durable Object SQLite with FTS5.
|
|
147
|
+
*
|
|
148
|
+
* - `get()` returns a count of indexed entries
|
|
149
|
+
* - `search(query)` full-text search using FTS5
|
|
150
|
+
* - `set(key, content)` indexes or replaces content under a key
|
|
151
|
+
*
|
|
152
|
+
* Each instance uses a namespaced FTS5 table to avoid collisions
|
|
153
|
+
* with the session message search.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* Session.create(this)
|
|
158
|
+
* .withContext("knowledge", {
|
|
159
|
+
* provider: new AgentSearchProvider(this)
|
|
160
|
+
* })
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
declare class AgentSearchProvider implements SearchProvider {
|
|
164
|
+
private agent;
|
|
165
|
+
private label;
|
|
166
|
+
private initialized;
|
|
167
|
+
constructor(agent: SqlProvider);
|
|
168
|
+
init(label: string): void;
|
|
169
|
+
private ensureTable;
|
|
170
|
+
get(): Promise<string | null>;
|
|
171
|
+
search(query: string): Promise<string | null>;
|
|
172
|
+
set(key: string, content: string): Promise<void>;
|
|
173
|
+
private deleteFTS;
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/experimental/memory/session/skills.d.ts
|
|
177
|
+
/**
|
|
178
|
+
* Storage interface for skill collections.
|
|
179
|
+
*
|
|
180
|
+
* - `get()` returns metadata listing (rendered into system prompt)
|
|
181
|
+
* - `load(key)` fetches full content (via load_context tool)
|
|
182
|
+
* - `set(key, content, description?)` writes an entry (via set_context tool)
|
|
183
|
+
*/
|
|
184
|
+
interface SkillProvider extends ContextProvider {
|
|
185
|
+
load(key: string): Promise<string | null>;
|
|
186
|
+
set?(key: string, content: string, description?: string): Promise<void>;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Check if a provider is a SkillProvider (has a `load` method).
|
|
190
|
+
*/
|
|
191
|
+
declare function isSkillProvider(provider: unknown): provider is SkillProvider;
|
|
192
|
+
/**
|
|
193
|
+
* SkillProvider backed by an R2 bucket.
|
|
194
|
+
*
|
|
195
|
+
* - `get()` returns a metadata listing of all skills (key + description)
|
|
196
|
+
* - `load(key)` fetches a skill's full content
|
|
197
|
+
* - `set(key, content, description?)` writes a skill
|
|
198
|
+
*
|
|
199
|
+
* Descriptions are pulled from R2 custom metadata (`description` key).
|
|
200
|
+
* If a prefix is provided, it is prepended on storage operations and
|
|
201
|
+
* stripped from keys in metadata.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```ts
|
|
205
|
+
* const skills = new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" });
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
declare class R2SkillProvider implements SkillProvider {
|
|
209
|
+
private bucket;
|
|
210
|
+
private prefix;
|
|
211
|
+
constructor(
|
|
212
|
+
bucket: R2Bucket,
|
|
213
|
+
options?: {
|
|
214
|
+
prefix?: string;
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
get(): Promise<string | null>;
|
|
218
|
+
load(key: string): Promise<string | null>;
|
|
219
|
+
set(key: string, content: string, description?: string): Promise<void>;
|
|
220
|
+
}
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/experimental/memory/session/context.d.ts
|
|
223
|
+
/**
|
|
224
|
+
* Base storage interface for a context block.
|
|
225
|
+
* A provider with only `get()` is readonly.
|
|
226
|
+
*/
|
|
227
|
+
interface ContextProvider {
|
|
228
|
+
get(): Promise<string | null>;
|
|
229
|
+
/** Called by the context system to provide the block label before first use. */
|
|
230
|
+
init?(label: string): void;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Writable context provider — extends ContextProvider with `set()`.
|
|
234
|
+
* Blocks backed by this provider are writable via the `set_context` tool.
|
|
235
|
+
*/
|
|
236
|
+
interface WritableContextProvider extends ContextProvider {
|
|
237
|
+
set(content: string): Promise<void>;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Check if a provider is writable (has a `set` method).
|
|
241
|
+
*/
|
|
242
|
+
declare function isWritableProvider(
|
|
243
|
+
provider: unknown
|
|
244
|
+
): provider is WritableContextProvider;
|
|
245
|
+
/**
|
|
246
|
+
* Configuration for a context block.
|
|
247
|
+
*/
|
|
248
|
+
interface ContextConfig {
|
|
249
|
+
/** Block label — used as key and in tool descriptions */
|
|
250
|
+
label: string;
|
|
251
|
+
/** Human-readable description (shown to AI in tool) */
|
|
252
|
+
description?: string;
|
|
253
|
+
/** Maximum tokens allowed. Enforced on set. */
|
|
254
|
+
maxTokens?: number;
|
|
255
|
+
/** Storage provider. Determines block behavior:
|
|
256
|
+
* - ContextProvider (get only) → readonly
|
|
257
|
+
* - WritableContextProvider (get+set) → writable via set_context
|
|
258
|
+
* - SkillProvider (get+load+set?) → on-demand via load_context
|
|
259
|
+
* - SearchProvider (get+search+set?) → searchable via search_context
|
|
260
|
+
* If omitted, auto-wired to writable SQLite when using builder. */
|
|
261
|
+
provider?:
|
|
262
|
+
| ContextProvider
|
|
263
|
+
| WritableContextProvider
|
|
264
|
+
| SkillProvider
|
|
265
|
+
| SearchProvider;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* A loaded context block with computed token count.
|
|
269
|
+
*/
|
|
270
|
+
interface ContextBlock {
|
|
271
|
+
label: string;
|
|
272
|
+
description?: string;
|
|
273
|
+
content: string;
|
|
274
|
+
tokens: number;
|
|
275
|
+
maxTokens?: number;
|
|
276
|
+
/** True if provider is writable (has set) */
|
|
277
|
+
writable: boolean;
|
|
278
|
+
/** True if backed by a SkillProvider */
|
|
279
|
+
isSkill: boolean;
|
|
280
|
+
/** True if backed by a SearchProvider */
|
|
281
|
+
isSearchable: boolean;
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/experimental/memory/utils/compaction-helpers.d.ts
|
|
285
|
+
/** Prefix for all compaction messages (overlays and summaries) */
|
|
286
|
+
declare const COMPACTION_PREFIX = "compaction_";
|
|
287
|
+
/** Check if a message is a compaction message */
|
|
288
|
+
declare function isCompactionMessage(msg: SessionMessage): boolean;
|
|
289
|
+
/**
|
|
290
|
+
* Align a boundary index forward to avoid splitting tool call/result groups.
|
|
291
|
+
* If the boundary falls between an assistant message with tool calls and its
|
|
292
|
+
* tool results, move it forward past the results.
|
|
293
|
+
*/
|
|
294
|
+
declare function alignBoundaryForward(
|
|
295
|
+
messages: SessionMessage[],
|
|
296
|
+
idx: number
|
|
297
|
+
): number;
|
|
298
|
+
/**
|
|
299
|
+
* Align a boundary index backward to avoid splitting tool call/result groups.
|
|
300
|
+
* If the boundary falls in the middle of tool results, move it backward to
|
|
301
|
+
* include the assistant message that made the calls.
|
|
302
|
+
*/
|
|
303
|
+
declare function alignBoundaryBackward(
|
|
304
|
+
messages: SessionMessage[],
|
|
305
|
+
idx: number
|
|
306
|
+
): number;
|
|
307
|
+
/**
|
|
308
|
+
* Find the compression end boundary using a token budget for the tail.
|
|
309
|
+
* Walks backward from the end, accumulating tokens until budget is reached.
|
|
310
|
+
* Returns the index where compression should stop (everything from this
|
|
311
|
+
* index onward is protected).
|
|
312
|
+
*
|
|
313
|
+
* @param messages All messages
|
|
314
|
+
* @param headEnd Index where the protected head ends (compression starts here)
|
|
315
|
+
* @param tailTokenBudget Maximum tokens to keep in the tail
|
|
316
|
+
* @param minTailMessages Minimum messages to protect in the tail (fallback)
|
|
317
|
+
*/
|
|
318
|
+
declare function findTailCutByTokens(
|
|
319
|
+
messages: SessionMessage[],
|
|
320
|
+
headEnd: number,
|
|
321
|
+
tailTokenBudget?: number,
|
|
322
|
+
minTailMessages?: number
|
|
323
|
+
): number;
|
|
324
|
+
/**
|
|
325
|
+
* Fix orphaned tool call/result pairs after compaction.
|
|
326
|
+
*
|
|
327
|
+
* Two failure modes:
|
|
328
|
+
* 1. Tool result references a call_id whose assistant tool_call was removed
|
|
329
|
+
* → Remove the orphaned result
|
|
330
|
+
* 2. Assistant has tool_calls whose results were dropped
|
|
331
|
+
* → Add stub results so the API doesn't error
|
|
332
|
+
*
|
|
333
|
+
* @param messages Messages after compaction
|
|
334
|
+
* @returns Sanitized messages with no orphaned pairs
|
|
335
|
+
*/
|
|
336
|
+
declare function sanitizeToolPairs(
|
|
337
|
+
messages: SessionMessage[]
|
|
338
|
+
): SessionMessage[];
|
|
339
|
+
/**
|
|
340
|
+
* Compute a summary token budget based on the content being compressed.
|
|
341
|
+
* 20% of the compressed content, clamped to 2K-8K tokens.
|
|
342
|
+
*/
|
|
343
|
+
declare function computeSummaryBudget(messages: SessionMessage[]): number;
|
|
344
|
+
/**
|
|
345
|
+
* Build a prompt for LLM summarization of compressed messages.
|
|
346
|
+
*
|
|
347
|
+
* @param messages Messages to summarize
|
|
348
|
+
* @param previousSummary Previous summary for iterative updates (or null for first compaction)
|
|
349
|
+
* @param budget Target token count for the summary
|
|
350
|
+
*/
|
|
351
|
+
declare function buildSummaryPrompt(
|
|
352
|
+
messages: SessionMessage[],
|
|
353
|
+
previousSummary: string | null,
|
|
354
|
+
budget: number
|
|
355
|
+
): string;
|
|
356
|
+
/**
|
|
357
|
+
* Result of a compaction function — describes the overlay to store.
|
|
358
|
+
*/
|
|
359
|
+
interface CompactResult {
|
|
360
|
+
/** First message ID in the compacted range */
|
|
361
|
+
fromMessageId: string;
|
|
362
|
+
/** Last message ID in the compacted range */
|
|
363
|
+
toMessageId: string;
|
|
364
|
+
/** Summary text to store as the overlay */
|
|
365
|
+
summary: string;
|
|
366
|
+
}
|
|
367
|
+
interface CompactOptions {
|
|
368
|
+
/**
|
|
369
|
+
* Function to call the LLM for summarization.
|
|
370
|
+
* Takes a user prompt string, returns the LLM's text response.
|
|
371
|
+
*/
|
|
372
|
+
summarize: (prompt: string) => Promise<string>;
|
|
373
|
+
/** Number of head messages to protect (default: 2) */
|
|
374
|
+
protectHead?: number;
|
|
375
|
+
/** Token budget for tail protection (default: 20000) */
|
|
376
|
+
tailTokenBudget?: number;
|
|
377
|
+
/** Minimum tail messages to protect (default: 2) */
|
|
378
|
+
minTailMessages?: number;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Reference compaction implementation.
|
|
382
|
+
*
|
|
383
|
+
* Implements the full hermes-style compaction algorithm:
|
|
384
|
+
* 1. Protect head messages (first N)
|
|
385
|
+
* 2. Protect tail by token budget (walk backward)
|
|
386
|
+
* 3. Align boundaries to tool call groups
|
|
387
|
+
* 4. Summarize middle section with LLM (structured format)
|
|
388
|
+
* 5. Sanitize orphaned tool pairs
|
|
389
|
+
* 6. Iterative summary updates on subsequent compactions
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* ```typescript
|
|
393
|
+
* import { createCompactFunction } from "agents/experimental/memory/utils";
|
|
394
|
+
*
|
|
395
|
+
* const session = new Session(provider, {
|
|
396
|
+
* compaction: {
|
|
397
|
+
* tokenThreshold: 100000,
|
|
398
|
+
* fn: createCompactFunction({
|
|
399
|
+
* summarize: (prompt) => generateText({ model, prompt }).then(r => r.text)
|
|
400
|
+
* })
|
|
401
|
+
* }
|
|
402
|
+
* });
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
405
|
+
declare function createCompactFunction(
|
|
406
|
+
opts: CompactOptions
|
|
407
|
+
): (messages: SessionMessage[]) => Promise<CompactResult | null>;
|
|
408
|
+
//#endregion
|
|
409
|
+
export {
|
|
410
|
+
SessionOptions as A,
|
|
411
|
+
AgentSessionProvider as C,
|
|
412
|
+
StoredCompaction as D,
|
|
413
|
+
SessionProvider as E,
|
|
414
|
+
SessionMessage as O,
|
|
415
|
+
isSearchProvider as S,
|
|
416
|
+
SearchResult as T,
|
|
417
|
+
R2SkillProvider as _,
|
|
418
|
+
alignBoundaryForward as a,
|
|
419
|
+
AgentSearchProvider as b,
|
|
420
|
+
createCompactFunction as c,
|
|
421
|
+
sanitizeToolPairs as d,
|
|
422
|
+
ContextBlock as f,
|
|
423
|
+
isWritableProvider as g,
|
|
424
|
+
WritableContextProvider as h,
|
|
425
|
+
alignBoundaryBackward as i,
|
|
426
|
+
SessionMessagePart as k,
|
|
427
|
+
findTailCutByTokens as l,
|
|
428
|
+
ContextProvider as m,
|
|
429
|
+
CompactOptions as n,
|
|
430
|
+
buildSummaryPrompt as o,
|
|
431
|
+
ContextConfig as p,
|
|
432
|
+
CompactResult as r,
|
|
433
|
+
computeSummaryBudget as s,
|
|
434
|
+
COMPACTION_PREFIX as t,
|
|
435
|
+
isCompactionMessage as u,
|
|
436
|
+
SkillProvider as v,
|
|
437
|
+
SqlProvider as w,
|
|
438
|
+
SearchProvider as x,
|
|
439
|
+
isSkillProvider as y
|
|
440
|
+
};
|
|
441
|
+
//# sourceMappingURL=compaction-helpers-BdQbZiML.d.ts.map
|
package/dist/email.js
CHANGED
|
@@ -114,11 +114,10 @@ async function signAgentHeaders(secret, agentName, agentId) {
|
|
|
114
114
|
if (agentName.includes(":")) throw new Error("agentName cannot contain colons");
|
|
115
115
|
if (agentId.includes(":")) throw new Error("agentId cannot contain colons");
|
|
116
116
|
const timestamp = Math.floor(Date.now() / 1e3).toString();
|
|
117
|
-
const signature = await computeAgentSignature(secret, agentName, agentId, timestamp);
|
|
118
117
|
return {
|
|
119
118
|
"X-Agent-Name": agentName,
|
|
120
119
|
"X-Agent-ID": agentId,
|
|
121
|
-
"X-Agent-Sig":
|
|
120
|
+
"X-Agent-Sig": await computeAgentSignature(secret, agentName, agentId, timestamp),
|
|
122
121
|
"X-Agent-Sig-Ts": timestamp
|
|
123
122
|
};
|
|
124
123
|
}
|
package/dist/email.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email.js","names":[],"sources":["../src/email.ts"],"sourcesContent":["/**\n * Email routing types, resolvers, and signing utilities for Agents\n */\n\n// Re-export AgentEmail type\nexport type { AgentEmail } from \"./internal_context\";\n\n// ============================================================================\n// Email header utilities\n// ============================================================================\n\n/**\n * Header object as returned by postal-mime and similar email parsing libraries.\n * Each header has a lowercase key and a string value.\n */\nexport type EmailHeader = {\n /** Lowercase header name (e.g., \"content-type\", \"x-custom-header\") */\n key: string;\n /** Header value */\n value: string;\n};\n\n/**\n * Check if an email appears to be an auto-reply based on standard headers.\n * Checks for Auto-Submitted (RFC 3834), X-Auto-Response-Suppress, and Precedence headers.\n *\n * @param headers - Headers array from postal-mime Email.headers or similar format\n * @returns true if email appears to be an auto-reply\n *\n * @example\n * ```typescript\n * if (isAutoReplyEmail(parsed.headers)) {\n * // Skip processing auto-replies\n * return;\n * }\n * ```\n */\nexport function isAutoReplyEmail(headers: EmailHeader[]): boolean {\n return headers.some((h) => {\n const key = h.key.toLowerCase();\n const value = h.value.toLowerCase();\n\n // RFC 3834: Auto-Submitted header\n // \"no\" means normal (human-sent) email, anything else indicates auto-reply\n if (key === \"auto-submitted\") {\n return value !== \"no\";\n }\n\n // X-Auto-Response-Suppress: any value indicates sender doesn't want auto-replies\n if (key === \"x-auto-response-suppress\") {\n return true;\n }\n\n // Precedence: only bulk/junk/list indicate automated/mass mail\n if (key === \"precedence\") {\n return value === \"bulk\" || value === \"junk\" || value === \"list\";\n }\n\n return false;\n });\n}\n\n// ============================================================================\n// Signing utilities\n// ============================================================================\n\n/** Default signature expiration: 30 days in seconds */\nexport const DEFAULT_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;\n\n/** Maximum allowed clock skew for future timestamps: 5 minutes */\nconst MAX_CLOCK_SKEW_SECONDS = 5 * 60;\n\n/**\n * Compute HMAC-SHA256 signature for agent routing headers\n * @param secret - Secret key for HMAC\n * @param agentName - Name of the agent\n * @param agentId - ID of the agent instance\n * @param timestamp - Unix timestamp in seconds\n * @returns Base64-encoded HMAC signature\n */\nasync function computeAgentSignature(\n secret: string,\n agentName: string,\n agentId: string,\n timestamp: string\n): Promise<string> {\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"]\n );\n const data = encoder.encode(`${agentName}:${agentId}:${timestamp}`);\n const signature = await crypto.subtle.sign(\"HMAC\", key, data);\n return btoa(String.fromCharCode(...new Uint8Array(signature)));\n}\n\n/**\n * Result of signature verification\n */\ntype SignatureVerificationResult =\n | { valid: true }\n | { valid: false; reason: \"expired\" | \"invalid\" | \"malformed_timestamp\" };\n\n/**\n * Verify HMAC-SHA256 signature for agent routing headers\n * @param secret - Secret key for HMAC\n * @param agentName - Name of the agent\n * @param agentId - ID of the agent instance\n * @param signature - Base64-encoded signature to verify\n * @param timestamp - Unix timestamp in seconds when signature was created\n * @param maxAgeSeconds - Maximum age of signature in seconds (default: 30 days)\n * @returns Verification result with reason if invalid\n */\nasync function verifyAgentSignature(\n secret: string,\n agentName: string,\n agentId: string,\n signature: string,\n timestamp: string,\n maxAgeSeconds: number = DEFAULT_MAX_AGE_SECONDS\n): Promise<SignatureVerificationResult> {\n try {\n // Validate timestamp format\n const timestampNum = Number.parseInt(timestamp, 10);\n if (Number.isNaN(timestampNum)) {\n return { valid: false, reason: \"malformed_timestamp\" };\n }\n\n // Check timestamp validity\n const now = Math.floor(Date.now() / 1000);\n\n // Reject timestamps too far in the future (prevents extending signature validity)\n if (timestampNum > now + MAX_CLOCK_SKEW_SECONDS) {\n return { valid: false, reason: \"invalid\" };\n }\n\n // Check if signature has expired\n if (now - timestampNum > maxAgeSeconds) {\n return { valid: false, reason: \"expired\" };\n }\n\n const expected = await computeAgentSignature(\n secret,\n agentName,\n agentId,\n timestamp\n );\n // Constant-time comparison to prevent timing attacks\n if (expected.length !== signature.length) {\n return { valid: false, reason: \"invalid\" };\n }\n let result = 0;\n for (let i = 0; i < expected.length; i++) {\n result |= expected.charCodeAt(i) ^ signature.charCodeAt(i);\n }\n if (result !== 0) {\n return { valid: false, reason: \"invalid\" };\n }\n return { valid: true };\n } catch (error) {\n console.warn(\"[agents] Signature verification error:\", error);\n return { valid: false, reason: \"invalid\" };\n }\n}\n\n/**\n * Sign agent routing headers for secure reply flows.\n * Use this when sending outbound emails to ensure replies can be securely routed back.\n *\n * @param secret - Secret key for HMAC signing (store in environment variables)\n * @param agentName - Name of the agent\n * @param agentId - ID of the agent instance\n * @returns Headers object with X-Agent-Name, X-Agent-ID, X-Agent-Sig, and X-Agent-Sig-Ts\n *\n * @example\n * ```typescript\n * const headers = await signAgentHeaders(env.EMAIL_SECRET, \"MyAgent\", this.name);\n * // Use these headers when sending outbound emails\n * ```\n */\nexport async function signAgentHeaders(\n secret: string,\n agentName: string,\n agentId: string\n): Promise<Record<string, string>> {\n if (!secret) {\n throw new Error(\"secret is required for signing agent headers\");\n }\n if (!agentName) {\n throw new Error(\"agentName is required for signing agent headers\");\n }\n if (!agentId) {\n throw new Error(\"agentId is required for signing agent headers\");\n }\n // Reject colons to prevent signature confusion attacks\n // (signature payload uses colon as delimiter: \"agentName:agentId:timestamp\")\n if (agentName.includes(\":\")) {\n throw new Error(\"agentName cannot contain colons\");\n }\n if (agentId.includes(\":\")) {\n throw new Error(\"agentId cannot contain colons\");\n }\n\n const timestamp = Math.floor(Date.now() / 1000).toString();\n const signature = await computeAgentSignature(\n secret,\n agentName,\n agentId,\n timestamp\n );\n return {\n \"X-Agent-Name\": agentName,\n \"X-Agent-ID\": agentId,\n \"X-Agent-Sig\": signature,\n \"X-Agent-Sig-Ts\": timestamp\n };\n}\n\n// ============================================================================\n// Email routing types and resolvers\n// ============================================================================\n\nexport type EmailResolverResult = {\n agentName: string;\n agentId: string;\n /** @internal Indicates this resolver requires secure reply signing */\n _secureRouted?: boolean;\n} | null;\n\nexport type EmailResolver<Env> = (\n email: ForwardableEmailMessage,\n env: Env\n) => Promise<EmailResolverResult>;\n\n/**\n * Reason for signature verification failure\n */\nexport type SignatureFailureReason =\n | \"missing_headers\"\n | \"expired\"\n | \"invalid\"\n | \"malformed_timestamp\";\n\n/**\n * Options for createSecureReplyEmailResolver\n */\nexport type SecureReplyResolverOptions = {\n /**\n * Maximum age of signature in seconds.\n * Signatures older than this will be rejected.\n * Default: 30 days (2592000 seconds)\n */\n maxAge?: number;\n /**\n * Callback invoked when signature verification fails.\n * Useful for logging and debugging.\n */\n onInvalidSignature?: (\n email: ForwardableEmailMessage,\n reason: SignatureFailureReason\n ) => void;\n};\n\n/**\n * @deprecated REMOVED due to security vulnerability (IDOR via spoofed headers).\n * @throws Always throws an error with migration guidance.\n */\nexport function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {\n throw new Error(\n \"createHeaderBasedEmailResolver has been removed due to a security vulnerability. \" +\n \"It trusted attacker-controlled email headers for routing, enabling IDOR attacks.\\n\\n\" +\n \"Migration options:\\n\" +\n \" - For inbound mail: use createAddressBasedEmailResolver(agentName)\\n\" +\n \" - For reply flows: use createSecureReplyEmailResolver(secret) with signed headers\\n\\n\" +\n \"See https://github.com/cloudflare/agents/blob/main/docs/email.md for details.\"\n );\n}\n\n/**\n * Create a resolver for routing email replies with signature verification.\n * This resolver verifies that replies contain a valid HMAC signature, preventing\n * attackers from routing emails to arbitrary agent instances.\n *\n * @param secret - Secret key for HMAC verification (must match the key used with signAgentHeaders)\n * @param options - Optional configuration for signature verification\n * @returns A function that resolves the agent to route the email to, or null if signature is invalid\n *\n * @example\n * ```typescript\n * // In your email handler\n * const secureResolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {\n * maxAge: 7 * 24 * 60 * 60, // 7 days\n * onInvalidSignature: (email, reason) => {\n * console.warn(`Invalid signature from ${email.from}: ${reason}`);\n * }\n * });\n * const addressResolver = createAddressBasedEmailResolver(\"MyAgent\");\n *\n * await routeAgentEmail(email, env, {\n * resolver: async (email, env) => {\n * // Try secure reply routing first\n * const replyRouting = await secureResolver(email, env);\n * if (replyRouting) return replyRouting;\n * // Fall back to address-based routing\n * return addressResolver(email, env);\n * }\n * });\n * ```\n */\nexport function createSecureReplyEmailResolver<Env>(\n secret: string,\n options?: SecureReplyResolverOptions\n): EmailResolver<Env> {\n if (!secret) {\n throw new Error(\"secret is required for createSecureReplyEmailResolver\");\n }\n\n const maxAge = options?.maxAge ?? DEFAULT_MAX_AGE_SECONDS;\n const onInvalidSignature = options?.onInvalidSignature;\n\n return async (email: ForwardableEmailMessage, _env: Env) => {\n const agentName = email.headers.get(\"x-agent-name\");\n const agentId = email.headers.get(\"x-agent-id\");\n const signature = email.headers.get(\"x-agent-sig\");\n const timestamp = email.headers.get(\"x-agent-sig-ts\");\n\n if (!agentName || !agentId || !signature || !timestamp) {\n onInvalidSignature?.(email, \"missing_headers\");\n return null;\n }\n\n const result = await verifyAgentSignature(\n secret,\n agentName,\n agentId,\n signature,\n timestamp,\n maxAge\n );\n\n if (!result.valid) {\n onInvalidSignature?.(email, result.reason);\n return null;\n }\n\n return { agentName, agentId, _secureRouted: true };\n };\n}\n\n/**\n * Create a resolver that uses the email address to determine the agent to route the email to\n * @param defaultAgentName The default agent name to use if the email address does not contain a sub-address\n * @returns A function that resolves the agent to route the email to\n */\nexport function createAddressBasedEmailResolver<Env>(\n defaultAgentName: string\n): EmailResolver<Env> {\n return async (email: ForwardableEmailMessage, _env: Env) => {\n // Length limits per RFC 5321: local part max 64 chars, domain max 253 chars\n const emailMatch = email.to.match(\n /^([^+@]{1,64})(?:\\+([^@]{1,64}))?@(.{1,253})$/\n );\n if (!emailMatch) {\n return null;\n }\n\n const [, localPart, subAddress] = emailMatch;\n\n if (subAddress) {\n return {\n agentName: localPart,\n agentId: subAddress\n };\n }\n\n // Option 2: Use defaultAgentName namespace, localPart as agentId\n // Common for catch-all email routing to a single EmailAgent namespace\n return {\n agentName: defaultAgentName,\n agentId: localPart\n };\n };\n}\n\n/**\n * Create a resolver that uses the agentName and agentId to determine the agent to route the email to\n * @param agentName The name of the agent to route the email to\n * @param agentId The id of the agent to route the email to\n * @returns A function that resolves the agent to route the email to\n */\nexport function createCatchAllEmailResolver<Env>(\n agentName: string,\n agentId: string\n): EmailResolver<Env> {\n return async () => ({ agentName, agentId });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"email.js","names":[],"sources":["../src/email.ts"],"sourcesContent":["/**\n * Email routing types, resolvers, and signing utilities for Agents\n */\n\n// Re-export AgentEmail type\nexport type { AgentEmail } from \"./internal_context\";\n\n// ============================================================================\n// Email header utilities\n// ============================================================================\n\n/**\n * Header object as returned by postal-mime and similar email parsing libraries.\n * Each header has a lowercase key and a string value.\n */\nexport type EmailHeader = {\n /** Lowercase header name (e.g., \"content-type\", \"x-custom-header\") */\n key: string;\n /** Header value */\n value: string;\n};\n\n/**\n * Check if an email appears to be an auto-reply based on standard headers.\n * Checks for Auto-Submitted (RFC 3834), X-Auto-Response-Suppress, and Precedence headers.\n *\n * @param headers - Headers array from postal-mime Email.headers or similar format\n * @returns true if email appears to be an auto-reply\n *\n * @example\n * ```typescript\n * if (isAutoReplyEmail(parsed.headers)) {\n * // Skip processing auto-replies\n * return;\n * }\n * ```\n */\nexport function isAutoReplyEmail(headers: EmailHeader[]): boolean {\n return headers.some((h) => {\n const key = h.key.toLowerCase();\n const value = h.value.toLowerCase();\n\n // RFC 3834: Auto-Submitted header\n // \"no\" means normal (human-sent) email, anything else indicates auto-reply\n if (key === \"auto-submitted\") {\n return value !== \"no\";\n }\n\n // X-Auto-Response-Suppress: any value indicates sender doesn't want auto-replies\n if (key === \"x-auto-response-suppress\") {\n return true;\n }\n\n // Precedence: only bulk/junk/list indicate automated/mass mail\n if (key === \"precedence\") {\n return value === \"bulk\" || value === \"junk\" || value === \"list\";\n }\n\n return false;\n });\n}\n\n// ============================================================================\n// Signing utilities\n// ============================================================================\n\n/** Default signature expiration: 30 days in seconds */\nexport const DEFAULT_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;\n\n/** Maximum allowed clock skew for future timestamps: 5 minutes */\nconst MAX_CLOCK_SKEW_SECONDS = 5 * 60;\n\n/**\n * Compute HMAC-SHA256 signature for agent routing headers\n * @param secret - Secret key for HMAC\n * @param agentName - Name of the agent\n * @param agentId - ID of the agent instance\n * @param timestamp - Unix timestamp in seconds\n * @returns Base64-encoded HMAC signature\n */\nasync function computeAgentSignature(\n secret: string,\n agentName: string,\n agentId: string,\n timestamp: string\n): Promise<string> {\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"]\n );\n const data = encoder.encode(`${agentName}:${agentId}:${timestamp}`);\n const signature = await crypto.subtle.sign(\"HMAC\", key, data);\n return btoa(String.fromCharCode(...new Uint8Array(signature)));\n}\n\n/**\n * Result of signature verification\n */\ntype SignatureVerificationResult =\n | { valid: true }\n | { valid: false; reason: \"expired\" | \"invalid\" | \"malformed_timestamp\" };\n\n/**\n * Verify HMAC-SHA256 signature for agent routing headers\n * @param secret - Secret key for HMAC\n * @param agentName - Name of the agent\n * @param agentId - ID of the agent instance\n * @param signature - Base64-encoded signature to verify\n * @param timestamp - Unix timestamp in seconds when signature was created\n * @param maxAgeSeconds - Maximum age of signature in seconds (default: 30 days)\n * @returns Verification result with reason if invalid\n */\nasync function verifyAgentSignature(\n secret: string,\n agentName: string,\n agentId: string,\n signature: string,\n timestamp: string,\n maxAgeSeconds: number = DEFAULT_MAX_AGE_SECONDS\n): Promise<SignatureVerificationResult> {\n try {\n // Validate timestamp format\n const timestampNum = Number.parseInt(timestamp, 10);\n if (Number.isNaN(timestampNum)) {\n return { valid: false, reason: \"malformed_timestamp\" };\n }\n\n // Check timestamp validity\n const now = Math.floor(Date.now() / 1000);\n\n // Reject timestamps too far in the future (prevents extending signature validity)\n if (timestampNum > now + MAX_CLOCK_SKEW_SECONDS) {\n return { valid: false, reason: \"invalid\" };\n }\n\n // Check if signature has expired\n if (now - timestampNum > maxAgeSeconds) {\n return { valid: false, reason: \"expired\" };\n }\n\n const expected = await computeAgentSignature(\n secret,\n agentName,\n agentId,\n timestamp\n );\n // Constant-time comparison to prevent timing attacks\n if (expected.length !== signature.length) {\n return { valid: false, reason: \"invalid\" };\n }\n let result = 0;\n for (let i = 0; i < expected.length; i++) {\n result |= expected.charCodeAt(i) ^ signature.charCodeAt(i);\n }\n if (result !== 0) {\n return { valid: false, reason: \"invalid\" };\n }\n return { valid: true };\n } catch (error) {\n console.warn(\"[agents] Signature verification error:\", error);\n return { valid: false, reason: \"invalid\" };\n }\n}\n\n/**\n * Sign agent routing headers for secure reply flows.\n * Use this when sending outbound emails to ensure replies can be securely routed back.\n *\n * @param secret - Secret key for HMAC signing (store in environment variables)\n * @param agentName - Name of the agent\n * @param agentId - ID of the agent instance\n * @returns Headers object with X-Agent-Name, X-Agent-ID, X-Agent-Sig, and X-Agent-Sig-Ts\n *\n * @example\n * ```typescript\n * const headers = await signAgentHeaders(env.EMAIL_SECRET, \"MyAgent\", this.name);\n * // Use these headers when sending outbound emails\n * ```\n */\nexport async function signAgentHeaders(\n secret: string,\n agentName: string,\n agentId: string\n): Promise<Record<string, string>> {\n if (!secret) {\n throw new Error(\"secret is required for signing agent headers\");\n }\n if (!agentName) {\n throw new Error(\"agentName is required for signing agent headers\");\n }\n if (!agentId) {\n throw new Error(\"agentId is required for signing agent headers\");\n }\n // Reject colons to prevent signature confusion attacks\n // (signature payload uses colon as delimiter: \"agentName:agentId:timestamp\")\n if (agentName.includes(\":\")) {\n throw new Error(\"agentName cannot contain colons\");\n }\n if (agentId.includes(\":\")) {\n throw new Error(\"agentId cannot contain colons\");\n }\n\n const timestamp = Math.floor(Date.now() / 1000).toString();\n const signature = await computeAgentSignature(\n secret,\n agentName,\n agentId,\n timestamp\n );\n return {\n \"X-Agent-Name\": agentName,\n \"X-Agent-ID\": agentId,\n \"X-Agent-Sig\": signature,\n \"X-Agent-Sig-Ts\": timestamp\n };\n}\n\n// ============================================================================\n// Email routing types and resolvers\n// ============================================================================\n\nexport type EmailResolverResult = {\n agentName: string;\n agentId: string;\n /** @internal Indicates this resolver requires secure reply signing */\n _secureRouted?: boolean;\n} | null;\n\nexport type EmailResolver<Env> = (\n email: ForwardableEmailMessage,\n env: Env\n) => Promise<EmailResolverResult>;\n\n/**\n * Reason for signature verification failure\n */\nexport type SignatureFailureReason =\n | \"missing_headers\"\n | \"expired\"\n | \"invalid\"\n | \"malformed_timestamp\";\n\n/**\n * Options for createSecureReplyEmailResolver\n */\nexport type SecureReplyResolverOptions = {\n /**\n * Maximum age of signature in seconds.\n * Signatures older than this will be rejected.\n * Default: 30 days (2592000 seconds)\n */\n maxAge?: number;\n /**\n * Callback invoked when signature verification fails.\n * Useful for logging and debugging.\n */\n onInvalidSignature?: (\n email: ForwardableEmailMessage,\n reason: SignatureFailureReason\n ) => void;\n};\n\n/**\n * @deprecated REMOVED due to security vulnerability (IDOR via spoofed headers).\n * @throws Always throws an error with migration guidance.\n */\nexport function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {\n throw new Error(\n \"createHeaderBasedEmailResolver has been removed due to a security vulnerability. \" +\n \"It trusted attacker-controlled email headers for routing, enabling IDOR attacks.\\n\\n\" +\n \"Migration options:\\n\" +\n \" - For inbound mail: use createAddressBasedEmailResolver(agentName)\\n\" +\n \" - For reply flows: use createSecureReplyEmailResolver(secret) with signed headers\\n\\n\" +\n \"See https://github.com/cloudflare/agents/blob/main/docs/email.md for details.\"\n );\n}\n\n/**\n * Create a resolver for routing email replies with signature verification.\n * This resolver verifies that replies contain a valid HMAC signature, preventing\n * attackers from routing emails to arbitrary agent instances.\n *\n * @param secret - Secret key for HMAC verification (must match the key used with signAgentHeaders)\n * @param options - Optional configuration for signature verification\n * @returns A function that resolves the agent to route the email to, or null if signature is invalid\n *\n * @example\n * ```typescript\n * // In your email handler\n * const secureResolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {\n * maxAge: 7 * 24 * 60 * 60, // 7 days\n * onInvalidSignature: (email, reason) => {\n * console.warn(`Invalid signature from ${email.from}: ${reason}`);\n * }\n * });\n * const addressResolver = createAddressBasedEmailResolver(\"MyAgent\");\n *\n * await routeAgentEmail(email, env, {\n * resolver: async (email, env) => {\n * // Try secure reply routing first\n * const replyRouting = await secureResolver(email, env);\n * if (replyRouting) return replyRouting;\n * // Fall back to address-based routing\n * return addressResolver(email, env);\n * }\n * });\n * ```\n */\nexport function createSecureReplyEmailResolver<Env>(\n secret: string,\n options?: SecureReplyResolverOptions\n): EmailResolver<Env> {\n if (!secret) {\n throw new Error(\"secret is required for createSecureReplyEmailResolver\");\n }\n\n const maxAge = options?.maxAge ?? DEFAULT_MAX_AGE_SECONDS;\n const onInvalidSignature = options?.onInvalidSignature;\n\n return async (email: ForwardableEmailMessage, _env: Env) => {\n const agentName = email.headers.get(\"x-agent-name\");\n const agentId = email.headers.get(\"x-agent-id\");\n const signature = email.headers.get(\"x-agent-sig\");\n const timestamp = email.headers.get(\"x-agent-sig-ts\");\n\n if (!agentName || !agentId || !signature || !timestamp) {\n onInvalidSignature?.(email, \"missing_headers\");\n return null;\n }\n\n const result = await verifyAgentSignature(\n secret,\n agentName,\n agentId,\n signature,\n timestamp,\n maxAge\n );\n\n if (!result.valid) {\n onInvalidSignature?.(email, result.reason);\n return null;\n }\n\n return { agentName, agentId, _secureRouted: true };\n };\n}\n\n/**\n * Create a resolver that uses the email address to determine the agent to route the email to\n * @param defaultAgentName The default agent name to use if the email address does not contain a sub-address\n * @returns A function that resolves the agent to route the email to\n */\nexport function createAddressBasedEmailResolver<Env>(\n defaultAgentName: string\n): EmailResolver<Env> {\n return async (email: ForwardableEmailMessage, _env: Env) => {\n // Length limits per RFC 5321: local part max 64 chars, domain max 253 chars\n const emailMatch = email.to.match(\n /^([^+@]{1,64})(?:\\+([^@]{1,64}))?@(.{1,253})$/\n );\n if (!emailMatch) {\n return null;\n }\n\n const [, localPart, subAddress] = emailMatch;\n\n if (subAddress) {\n return {\n agentName: localPart,\n agentId: subAddress\n };\n }\n\n // Option 2: Use defaultAgentName namespace, localPart as agentId\n // Common for catch-all email routing to a single EmailAgent namespace\n return {\n agentName: defaultAgentName,\n agentId: localPart\n };\n };\n}\n\n/**\n * Create a resolver that uses the agentName and agentId to determine the agent to route the email to\n * @param agentName The name of the agent to route the email to\n * @param agentId The id of the agent to route the email to\n * @returns A function that resolves the agent to route the email to\n */\nexport function createCatchAllEmailResolver<Env>(\n agentName: string,\n agentId: string\n): EmailResolver<Env> {\n return async () => ({ agentName, agentId });\n}\n"],"mappings":"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"}
|