@holon-run/agentinbox 1.7.2 → 1.8.1
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/README.md +22 -0
- package/dist/src/cli.js +150 -8
- package/dist/src/client.js +52 -0
- package/dist/src/email_attachment.js +199 -0
- package/dist/src/email_attachment_content.js +759 -0
- package/dist/src/email_body.js +392 -0
- package/dist/src/http.js +147 -3
- package/dist/src/service.js +61 -3
- package/dist/src/source_schema.js +4 -1
- package/dist/src/sources/email.js +72 -3
- package/dist/src/sources/remote_modules.js +2 -1
- package/dist/src/store.js +451 -1
- package/drizzle/migrations/0006_email_body_cache.sql +19 -0
- package/drizzle/migrations/0007_email_attachment_materializations.sql +20 -0
- package/drizzle/migrations/0008_email_attachment_audit.sql +17 -0
- package/drizzle/schema.ts +62 -0
- package/package.json +2 -2
- package/skills/agentinbox/SKILL.md +20 -0
|
@@ -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") ||
|