@holon-run/agentinbox 1.7.1 → 1.8.0

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.
@@ -0,0 +1,392 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.EmailBodyReader = exports.EMAIL_BODY_MAX_BYTES = exports.EMAIL_BODY_DEFAULT_MAX_BYTES = void 0;
7
+ const node_crypto_1 = __importDefault(require("node:crypto"));
8
+ const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
9
+ const email_attachment_1 = require("./email_attachment");
10
+ const util_1 = require("./util");
11
+ exports.EMAIL_BODY_DEFAULT_MAX_BYTES = 32 * 1024;
12
+ exports.EMAIL_BODY_MAX_BYTES = 1024 * 1024;
13
+ const EMAIL_BODY_CACHE_MAX_BYTES = 128 * 1024 * 1024;
14
+ const EMAIL_BODY_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
15
+ const EMAIL_BODY_SCHEMA_VERSION = 1;
16
+ class EmailBodyReader {
17
+ store;
18
+ uxc;
19
+ inFlight = new Map();
20
+ cursorSecret = node_crypto_1.default.randomBytes(32);
21
+ constructor(store, uxc = new uxc_daemon_client_1.UxcDaemonClient({ env: process.env })) {
22
+ this.store = store;
23
+ this.uxc = uxc;
24
+ }
25
+ async read(inboxId, entry, source, options = {}) {
26
+ const maxBytes = options.maxBytes ?? exports.EMAIL_BODY_DEFAULT_MAX_BYTES;
27
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > exports.EMAIL_BODY_MAX_BYTES) {
28
+ throw new Error(`email body max_bytes must be an integer between 1 and ${exports.EMAIL_BODY_MAX_BYTES}`);
29
+ }
30
+ if (source.sourceType !== "email_mailbox" || entry.eventVariant !== "email.message.received") {
31
+ return unavailable(entry.entryId, "unsupported", false, "This inbox entry is not a supported email message.");
32
+ }
33
+ const contentVersion = emailBodyContentVersion(entry);
34
+ const cursor = options.cursor
35
+ ? decodeCursor(options.cursor, inboxId, entry.entryId, this.cursorSecret)
36
+ : null;
37
+ if (cursor) {
38
+ const cached = this.store.getEmailBodyCache(inboxId, entry.entryId);
39
+ if (!cached ||
40
+ cached.contentVersion !== contentVersion ||
41
+ cached.expiresAt <= (0, util_1.nowIso)()) {
42
+ throw new Error("email body cursor is invalid or expired");
43
+ }
44
+ const snapshot = snapshotFromCache(cached);
45
+ if (snapshotVersion(snapshot) !== cursor.snapshotVersion) {
46
+ throw new Error("email body cursor is invalid or expired");
47
+ }
48
+ this.store.touchEmailBodyCache(entry.entryId, (0, util_1.nowIso)());
49
+ const attachments = (0, email_attachment_1.publicEmailAttachmentCollection)(entry.itemId, entry.metadata, (itemId, selector) => this.store.getEmailAttachmentMaterialization(itemId, selector));
50
+ return {
51
+ status: "available",
52
+ entryId: entry.entryId,
53
+ subject: optionalString(entry.metadata?.subject),
54
+ from: optionalString(entry.metadata?.from),
55
+ ...attachments,
56
+ body: pageSnapshot(snapshot, maxBytes, cursor.offset, inboxId, entry.entryId, this.cursorSecret),
57
+ };
58
+ }
59
+ const snapshot = await this.resolveSnapshot(inboxId, entry, source, contentVersion, options.fetch !== false);
60
+ if ("status" in snapshot) {
61
+ return snapshot;
62
+ }
63
+ const attachments = (0, email_attachment_1.publicEmailAttachmentCollection)(entry.itemId, entry.metadata, (itemId, selector) => this.store.getEmailAttachmentMaterialization(itemId, selector));
64
+ return {
65
+ status: "available",
66
+ entryId: entry.entryId,
67
+ subject: optionalString(entry.metadata?.subject),
68
+ from: optionalString(entry.metadata?.from),
69
+ ...attachments,
70
+ body: pageSnapshot(snapshot, maxBytes, 0, inboxId, entry.entryId, this.cursorSecret),
71
+ };
72
+ }
73
+ async resolveSnapshot(inboxId, entry, source, contentVersion, allowFetch) {
74
+ const now = (0, util_1.nowIso)();
75
+ const cached = this.store.getEmailBodyCache(inboxId, entry.entryId);
76
+ if (cached &&
77
+ cached.contentVersion === contentVersion &&
78
+ cached.expiresAt > now &&
79
+ (cached.completeness === "complete" || !allowFetch)) {
80
+ this.store.touchEmailBodyCache(entry.entryId, now);
81
+ return snapshotFromCache(cached);
82
+ }
83
+ const key = `${inboxId}:${entry.entryId}:${contentVersion}:${allowFetch ? "fetch" : "local"}`;
84
+ const existing = this.inFlight.get(key);
85
+ if (existing) {
86
+ return existing;
87
+ }
88
+ const pending = this.loadSnapshot(inboxId, entry, source, contentVersion, allowFetch, cached)
89
+ .finally(() => {
90
+ this.inFlight.delete(key);
91
+ });
92
+ this.inFlight.set(key, pending);
93
+ return pending;
94
+ }
95
+ async loadSnapshot(inboxId, entry, source, contentVersion, allowFetch, cached) {
96
+ const payload = entry.rawPayload ?? {};
97
+ const local = await this.readLocal(payload);
98
+ if (local?.completeness === "complete") {
99
+ return this.cacheSnapshot(inboxId, entry, contentVersion, local, "local");
100
+ }
101
+ const messageRef = optionalString(payload.message_ref);
102
+ if (allowFetch && messageRef) {
103
+ const fetched = await this.callUxc({ kind: "message_ref", messageRef });
104
+ if (!isUnavailable(fetched)) {
105
+ return this.cacheSnapshot(inboxId, entry, contentVersion, fetched, "fetched");
106
+ }
107
+ if (local) {
108
+ return this.cacheSnapshot(inboxId, entry, contentVersion, {
109
+ ...local,
110
+ reasons: uniqueStrings([...local.reasons, `fetch_${fetched.code}`]),
111
+ }, "local");
112
+ }
113
+ if (cached &&
114
+ cached.contentVersion === contentVersion &&
115
+ cached.expiresAt > (0, util_1.nowIso)()) {
116
+ return snapshotFromCache(cached);
117
+ }
118
+ return { ...fetched, entryId: entry.entryId };
119
+ }
120
+ if (local) {
121
+ return this.cacheSnapshot(inboxId, entry, contentVersion, local, "local");
122
+ }
123
+ if (cached && cached.contentVersion === contentVersion && cached.expiresAt > (0, util_1.nowIso)()) {
124
+ return snapshotFromCache(cached);
125
+ }
126
+ return {
127
+ status: "unavailable",
128
+ entryId: entry.entryId,
129
+ code: allowFetch ? (messageRef ? "provider_unavailable" : "identity_missing") : "local_body_unavailable",
130
+ retryable: allowFetch && Boolean(messageRef),
131
+ message: allowFetch
132
+ ? "No readable email body is available for this entry."
133
+ : "No local email body is available; retry without --no-fetch.",
134
+ };
135
+ }
136
+ async readLocal(payload) {
137
+ const embedded = bodyResultFromUnknown(payload.body);
138
+ if (embedded) {
139
+ return { ...embedded, contentVersion: "", origin: "local" };
140
+ }
141
+ const raw = objectValue(payload.raw);
142
+ const mimeBase64 = optionalString(raw.mime_inline_base64);
143
+ const originalBytes = optionalInteger(raw.original_bytes);
144
+ if (mimeBase64 && originalBytes != null) {
145
+ const result = await this.callUxc({
146
+ kind: "inline_mime",
147
+ mimeBase64,
148
+ originalBytes,
149
+ complete: raw.complete === true,
150
+ });
151
+ return isUnavailable(result) ? null : result;
152
+ }
153
+ const mimeText = optionalString(raw.mime_inline);
154
+ if (mimeText != null) {
155
+ const result = await this.callUxc({
156
+ kind: "legacy_mime",
157
+ mimeText,
158
+ sourceTruncated: raw.mime_truncated === true,
159
+ });
160
+ return isUnavailable(result) ? null : result;
161
+ }
162
+ return null;
163
+ }
164
+ async callUxc(input) {
165
+ try {
166
+ const status = objectValue(await this.uxc.request("daemon.status"));
167
+ const capability = objectValue(status.email_body);
168
+ const inputKinds = Array.isArray(capability.input_kinds) ? capability.input_kinds : [];
169
+ if (capability.schema_version !== EMAIL_BODY_SCHEMA_VERSION ||
170
+ !inputKinds.includes(input.kind)) {
171
+ return unavailable("", "capability_unavailable", false, "The installed UXC daemon does not support this email body input.");
172
+ }
173
+ const result = await this.uxc.request("email.body.read", {
174
+ input: emailBodyInputParams(input),
175
+ });
176
+ const parsed = bodyResultFromUnknown(result);
177
+ if (!parsed) {
178
+ return unavailable("", "parse_failed", false, "UXC returned an invalid email body result.");
179
+ }
180
+ return { ...parsed, contentVersion: "", origin: input.kind === "message_ref" ? "fetched" : "local" };
181
+ }
182
+ catch (error) {
183
+ const message = error instanceof Error ? error.message : String(error);
184
+ const timeout = /timed out|timeout/i.test(message);
185
+ return unavailable("", timeout ? "timeout" : "provider_unavailable", true, timeout ? "Email body retrieval timed out." : "Email body retrieval is temporarily unavailable.");
186
+ }
187
+ }
188
+ cacheSnapshot(inboxId, entry, contentVersion, snapshot, origin) {
189
+ if (!this.store.getInboxEntryForInbox(inboxId, entry.entryId)) {
190
+ return unavailable(entry.entryId, "not_found", false, "The inbox entry is no longer available.");
191
+ }
192
+ const now = (0, util_1.nowIso)();
193
+ const normalized = { ...snapshot, contentVersion, origin };
194
+ this.store.putEmailBodyCache({
195
+ entryId: entry.entryId,
196
+ inboxId,
197
+ itemId: entry.itemId,
198
+ sourceId: entry.sourceId,
199
+ contentVersion,
200
+ schemaVersion: snapshot.schemaVersion,
201
+ parserVersion: snapshot.parserVersion,
202
+ text: snapshot.text,
203
+ bytes: snapshot.bytes,
204
+ completeness: snapshot.completeness,
205
+ reasons: snapshot.reasons,
206
+ createdAt: now,
207
+ expiresAt: new Date(Date.parse(now) + EMAIL_BODY_CACHE_TTL_MS).toISOString(),
208
+ lastAccessedAt: now,
209
+ }, EMAIL_BODY_CACHE_MAX_BYTES);
210
+ return normalized;
211
+ }
212
+ }
213
+ exports.EmailBodyReader = EmailBodyReader;
214
+ function bodyResultFromUnknown(value) {
215
+ const result = objectValue(value);
216
+ const completeness = result.completeness;
217
+ if (result.schema_version !== EMAIL_BODY_SCHEMA_VERSION ||
218
+ typeof result.parser_version !== "string" ||
219
+ result.format !== "text" ||
220
+ typeof result.text !== "string" ||
221
+ !Number.isSafeInteger(result.bytes) ||
222
+ (completeness !== "complete" && completeness !== "partial" && completeness !== "unverified") ||
223
+ !Array.isArray(result.reasons)) {
224
+ return null;
225
+ }
226
+ const actualBytes = Buffer.byteLength(result.text, "utf8");
227
+ if (actualBytes !== result.bytes || actualBytes > exports.EMAIL_BODY_MAX_BYTES) {
228
+ return null;
229
+ }
230
+ return {
231
+ schemaVersion: EMAIL_BODY_SCHEMA_VERSION,
232
+ parserVersion: result.parser_version,
233
+ text: result.text,
234
+ bytes: actualBytes,
235
+ completeness,
236
+ reasons: result.reasons.filter((reason) => typeof reason === "string"),
237
+ };
238
+ }
239
+ function snapshotFromCache(cache) {
240
+ return {
241
+ contentVersion: cache.contentVersion,
242
+ schemaVersion: cache.schemaVersion,
243
+ parserVersion: cache.parserVersion,
244
+ text: cache.text,
245
+ bytes: cache.bytes,
246
+ completeness: cache.completeness,
247
+ reasons: cache.reasons,
248
+ origin: "cache",
249
+ };
250
+ }
251
+ function pageSnapshot(snapshot, maxBytes, offset, inboxId, entryId, cursorSecret) {
252
+ const full = Buffer.from(snapshot.text, "utf8");
253
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > full.length || !isUtf8Boundary(full, offset)) {
254
+ throw new Error("email body cursor is invalid or expired");
255
+ }
256
+ let end = Math.min(full.length, offset + maxBytes);
257
+ while (end > offset && !isUtf8Boundary(full, end)) {
258
+ end -= 1;
259
+ }
260
+ if (end === offset && offset < full.length) {
261
+ let next = offset + 1;
262
+ while (next < full.length && !isUtf8Boundary(full, next)) {
263
+ next += 1;
264
+ }
265
+ throw new Error(`email body max_bytes is too small; at least ${next - offset} bytes are required for the next character`);
266
+ }
267
+ const hasMore = end < full.length;
268
+ const nextCursor = hasMore
269
+ ? encodeCursor({
270
+ v: 1,
271
+ inboxId,
272
+ entryId,
273
+ snapshotVersion: snapshotVersion(snapshot),
274
+ offset: end,
275
+ }, cursorSecret)
276
+ : undefined;
277
+ return {
278
+ format: "text",
279
+ text: full.subarray(offset, end).toString("utf8"),
280
+ returnedBytes: end - offset,
281
+ completeness: snapshot.completeness,
282
+ reasons: snapshot.reasons,
283
+ hasMore,
284
+ ...(nextCursor ? { nextCursor } : {}),
285
+ truncated: hasMore || snapshot.completeness !== "complete",
286
+ ...(snapshot.completeness === "complete" ? { totalBytes: snapshot.bytes } : {}),
287
+ origin: snapshot.origin,
288
+ };
289
+ }
290
+ function emailBodyContentVersion(entry) {
291
+ const payload = entry.rawPayload ?? {};
292
+ const raw = objectValue(payload.raw);
293
+ const message = objectValue(payload.message);
294
+ return node_crypto_1.default.createHash("sha256").update(JSON.stringify({
295
+ itemId: entry.itemId,
296
+ stableKey: message.stable_key ?? null,
297
+ messageRef: payload.message_ref ?? null,
298
+ body: payload.body ?? null,
299
+ rawRepresentation: raw.representation_version ?? null,
300
+ rawOriginalBytes: raw.original_bytes ?? raw.size_bytes ?? null,
301
+ rawComplete: raw.complete ?? null,
302
+ mimeBase64: raw.mime_inline_base64 ?? null,
303
+ legacyMime: raw.mime_inline ?? null,
304
+ legacyTruncated: raw.mime_truncated ?? null,
305
+ })).digest("base64url");
306
+ }
307
+ function encodeCursor(cursor, cursorSecret) {
308
+ const payload = Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
309
+ return `${payload}.${signCursor(payload, cursorSecret)}`;
310
+ }
311
+ function decodeCursor(token, inboxId, entryId, cursorSecret) {
312
+ try {
313
+ const [payload, signature, extra] = token.split(".");
314
+ if (!payload || !signature || extra || !validCursorSignature(payload, signature, cursorSecret)) {
315
+ throw new Error("invalid");
316
+ }
317
+ const value = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
318
+ if (value.v !== 1 ||
319
+ value.inboxId !== inboxId ||
320
+ value.entryId !== entryId ||
321
+ typeof value.snapshotVersion !== "string" ||
322
+ !Number.isSafeInteger(value.offset) ||
323
+ value.offset < 0) {
324
+ throw new Error("invalid");
325
+ }
326
+ return value;
327
+ }
328
+ catch {
329
+ throw new Error("email body cursor is invalid or expired");
330
+ }
331
+ }
332
+ function signCursor(payload, cursorSecret) {
333
+ return node_crypto_1.default.createHmac("sha256", cursorSecret).update(payload).digest("base64url");
334
+ }
335
+ function validCursorSignature(payload, signature, cursorSecret) {
336
+ const expected = Buffer.from(signCursor(payload, cursorSecret), "base64url");
337
+ const actual = Buffer.from(signature, "base64url");
338
+ return actual.length === expected.length && node_crypto_1.default.timingSafeEqual(actual, expected);
339
+ }
340
+ function snapshotVersion(snapshot) {
341
+ return node_crypto_1.default.createHash("sha256").update(JSON.stringify({
342
+ parserVersion: snapshot.parserVersion,
343
+ text: snapshot.text,
344
+ completeness: snapshot.completeness,
345
+ reasons: snapshot.reasons,
346
+ })).digest("base64url");
347
+ }
348
+ function emailBodyInputParams(input) {
349
+ switch (input.kind) {
350
+ case "inline_mime":
351
+ return {
352
+ kind: input.kind,
353
+ mime_base64: input.mimeBase64,
354
+ original_bytes: input.originalBytes,
355
+ complete: input.complete,
356
+ };
357
+ case "legacy_mime":
358
+ return {
359
+ kind: input.kind,
360
+ mime_text: input.mimeText,
361
+ ...(input.sourceTruncated === undefined ? {} : { source_truncated: input.sourceTruncated }),
362
+ };
363
+ case "message_ref":
364
+ return {
365
+ kind: input.kind,
366
+ message_ref: input.messageRef,
367
+ };
368
+ }
369
+ }
370
+ function isUnavailable(value) {
371
+ return "status" in value && value.status === "unavailable";
372
+ }
373
+ function isUtf8Boundary(buffer, offset) {
374
+ return offset === 0 || offset === buffer.length || (buffer[offset] & 0b1100_0000) !== 0b1000_0000;
375
+ }
376
+ function unavailable(entryId, code, retryable, message) {
377
+ return { status: "unavailable", entryId, code, retryable, message };
378
+ }
379
+ function objectValue(value) {
380
+ return value && typeof value === "object" && !Array.isArray(value)
381
+ ? value
382
+ : {};
383
+ }
384
+ function optionalString(value) {
385
+ return typeof value === "string" ? value : null;
386
+ }
387
+ function optionalInteger(value) {
388
+ return Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : null;
389
+ }
390
+ function uniqueStrings(values) {
391
+ return [...new Set(values)];
392
+ }
package/dist/src/http.js CHANGED
@@ -1640,9 +1640,130 @@ function buildFastifyServer(service) {
1640
1640
  afterEntryId: query.after_entry_id,
1641
1641
  includeAcked: query.include_acked ? query.include_acked === "true" : undefined,
1642
1642
  limit: parseOptionalPositiveInteger(query.limit),
1643
- }),
1643
+ }).map((entry) => service.projectPublicInboxEntry(entry)),
1644
1644
  };
1645
1645
  });
1646
+ app.get("/agents/:agentId/inbox/attachments/:attachmentRef", {
1647
+ schema: {
1648
+ tags: ["inbox"],
1649
+ params: {
1650
+ type: "object",
1651
+ required: ["agentId", "attachmentRef"],
1652
+ properties: {
1653
+ agentId: { type: "string", minLength: 1 },
1654
+ attachmentRef: { type: "string", minLength: 1 },
1655
+ },
1656
+ },
1657
+ response: {
1658
+ 200: jsonObjectSchema,
1659
+ 404: errorResponseSchema,
1660
+ },
1661
+ },
1662
+ }, async (request) => {
1663
+ const params = request.params;
1664
+ return service.getInboxEmailAttachment(decodeURIComponent(params.agentId), decodeURIComponent(params.attachmentRef));
1665
+ });
1666
+ app.post("/agents/:agentId/inbox/attachments/:attachmentRef/materialize", {
1667
+ schema: {
1668
+ tags: ["inbox"],
1669
+ params: {
1670
+ type: "object",
1671
+ required: ["agentId", "attachmentRef"],
1672
+ properties: {
1673
+ agentId: { type: "string", minLength: 1 },
1674
+ attachmentRef: { type: "string", minLength: 1 },
1675
+ },
1676
+ },
1677
+ response: {
1678
+ 200: jsonObjectSchema,
1679
+ 400: errorResponseSchema,
1680
+ 404: errorResponseSchema,
1681
+ },
1682
+ },
1683
+ }, async (request) => {
1684
+ const params = request.params;
1685
+ return service.materializeInboxEmailAttachment(decodeURIComponent(params.agentId), decodeURIComponent(params.attachmentRef));
1686
+ });
1687
+ app.get("/agents/:agentId/inbox/attachments/:attachmentRef/content", {
1688
+ schema: {
1689
+ tags: ["inbox"],
1690
+ params: {
1691
+ type: "object",
1692
+ required: ["agentId", "attachmentRef"],
1693
+ properties: {
1694
+ agentId: { type: "string", minLength: 1 },
1695
+ attachmentRef: { type: "string", minLength: 1 },
1696
+ },
1697
+ },
1698
+ response: {
1699
+ 404: errorResponseSchema,
1700
+ },
1701
+ },
1702
+ }, async (request, reply) => {
1703
+ const params = request.params;
1704
+ const content = service.openInboxEmailAttachmentContent(decodeURIComponent(params.agentId), decodeURIComponent(params.attachmentRef));
1705
+ return reply
1706
+ .type(content.contentType)
1707
+ .header("content-length", String(content.size))
1708
+ .header("content-disposition", attachmentContentDisposition(content.attachment.filename))
1709
+ .header("cache-control", "private, no-store")
1710
+ .send(content.stream);
1711
+ });
1712
+ app.delete("/agents/:agentId/inbox/attachments/:attachmentRef/content", {
1713
+ schema: {
1714
+ tags: ["inbox"],
1715
+ params: {
1716
+ type: "object",
1717
+ required: ["agentId", "attachmentRef"],
1718
+ properties: {
1719
+ agentId: { type: "string", minLength: 1 },
1720
+ attachmentRef: { type: "string", minLength: 1 },
1721
+ },
1722
+ },
1723
+ response: {
1724
+ 200: jsonObjectSchema,
1725
+ 404: errorResponseSchema,
1726
+ },
1727
+ },
1728
+ }, async (request) => {
1729
+ const params = request.params;
1730
+ return service.deleteInboxEmailAttachmentContent(decodeURIComponent(params.agentId), decodeURIComponent(params.attachmentRef));
1731
+ });
1732
+ app.get("/agents/:agentId/inbox/entries/:entryId/body", {
1733
+ schema: {
1734
+ tags: ["inbox"],
1735
+ params: {
1736
+ type: "object",
1737
+ required: ["agentId", "entryId"],
1738
+ properties: {
1739
+ agentId: { type: "string", minLength: 1 },
1740
+ entryId: { type: "string", minLength: 1 },
1741
+ },
1742
+ },
1743
+ querystring: {
1744
+ type: "object",
1745
+ additionalProperties: false,
1746
+ properties: {
1747
+ max_bytes: { type: "string", pattern: "^[1-9][0-9]*$" },
1748
+ fetch: { type: "string", enum: ["true", "false"] },
1749
+ cursor: { type: "string", minLength: 1 },
1750
+ },
1751
+ },
1752
+ response: {
1753
+ 200: jsonObjectSchema,
1754
+ 400: errorResponseSchema,
1755
+ 404: errorResponseSchema,
1756
+ },
1757
+ },
1758
+ }, async (request) => {
1759
+ const params = request.params;
1760
+ const query = request.query;
1761
+ return service.readInboxEmailBody(decodeURIComponent(params.agentId), decodeURIComponent(params.entryId), {
1762
+ maxBytes: parseOptionalPositiveInteger(query.max_bytes),
1763
+ fetch: query.fetch ? query.fetch === "true" : undefined,
1764
+ cursor: query.cursor,
1765
+ });
1766
+ });
1646
1767
  app.post("/agents/:agentId/inbox/items", {
1647
1768
  schema: {
1648
1769
  tags: ["inbox"],
@@ -1716,12 +1837,14 @@ function buildFastifyServer(service) {
1716
1837
  connection: "keep-alive",
1717
1838
  });
1718
1839
  const session = service.watchInbox(agentId, watchOptions, (event) => {
1719
- sendSse(raw, event.event, event);
1840
+ sendSse(raw, event.event, event.event === "items"
1841
+ ? { ...event, entries: event.entries.map((entry) => service.projectPublicInboxEntry(entry)) }
1842
+ : event);
1720
1843
  });
1721
1844
  sendSse(raw, "items", {
1722
1845
  event: "items",
1723
1846
  agentId,
1724
- entries: session.initialItems,
1847
+ entries: session.initialItems.map((entry) => service.projectPublicInboxEntry(entry)),
1725
1848
  });
1726
1849
  session.start();
1727
1850
  const heartbeatMs = watchOptions.heartbeatMs ?? 15_000;
@@ -2018,6 +2141,14 @@ function buildFastifyServer(service) {
2018
2141
  void reply.code(403).send({ error: message });
2019
2142
  return;
2020
2143
  }
2144
+ const statusCode = error.statusCode;
2145
+ if (typeof statusCode === "number"
2146
+ && Number.isInteger(statusCode)
2147
+ && statusCode >= 400
2148
+ && statusCode < 600) {
2149
+ void reply.code(statusCode).send({ error: message });
2150
+ return;
2151
+ }
2021
2152
  if (isBadRequestError(message)) {
2022
2153
  void reply.code(400).send({ error: message });
2023
2154
  return;
@@ -2065,6 +2196,18 @@ function optionalString(value) {
2065
2196
  const trimmed = value.trim();
2066
2197
  return trimmed.length > 0 ? trimmed : undefined;
2067
2198
  }
2199
+ function attachmentContentDisposition(filename) {
2200
+ if (!filename) {
2201
+ return "attachment";
2202
+ }
2203
+ const sanitized = filename.replace(/[\r\n]/g, "").trim();
2204
+ if (!sanitized) {
2205
+ return "attachment";
2206
+ }
2207
+ const encoded = encodeURIComponent(sanitized)
2208
+ .replace(/['()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
2209
+ return `attachment; filename*=UTF-8''${encoded}`;
2210
+ }
2068
2211
  function assertDeliveryRouteAuthorized(service, routeId, authorization) {
2069
2212
  const binding = service.getOperatorBindingForReplyRoute(routeId);
2070
2213
  const deliveryAuth = binding?.deliveryAuth;
@@ -2155,6 +2298,7 @@ function isBadRequestError(message) {
2155
2298
  message.startsWith("expected boolean") ||
2156
2299
  message.startsWith("expected integer") ||
2157
2300
  message.startsWith("expected positive integer") ||
2301
+ message.startsWith("email body ") ||
2158
2302
  message.startsWith("webhook agent registration requires") ||
2159
2303
  message.startsWith("agent registration requires either") ||
2160
2304
  message.startsWith("notifyLeaseMs must be a positive integer") ||