@abraca/resend 2.16.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,1638 @@
1
+ #!/usr/bin/env node
2
+ import { AbracadabraClient, AbracadabraProvider, Kind, SERVER_ROOT_ID, WebSocketStatus, makeEntryMap, patchEntry, toPlain } from "@abraca/dabra";
3
+ import * as Y from "yjs";
4
+ import { populateYDocFromMarkdown, yjsToHtml } from "@abraca/convert";
5
+ import { createHmac, timingSafeEqual } from "node:crypto";
6
+ import { createServer } from "node:http";
7
+ import { Resend } from "resend";
8
+ import * as ed from "@noble/ed25519";
9
+ import { existsSync } from "node:fs";
10
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
11
+ import { homedir } from "node:os";
12
+ import { dirname, join } from "node:path";
13
+
14
+ //#region packages/resend/src/bootstrap.ts
15
+ /**
16
+ * Find or create the Inbox + Outbox documents under the bound Space.
17
+ *
18
+ * Idempotent: any existing top-level doc labelled "Inbox" or "Outbox" is reused
19
+ * as-is — the user is allowed to rename, recolor, or restructure these docs
20
+ * after they're created. Missing columns under the Outbox kanban are filled in.
21
+ */
22
+ const INBOX_LABEL = "Inbox";
23
+ const OUTBOX_LABEL = "Outbox";
24
+ const OUTBOX_COLUMNS = [
25
+ "Draft",
26
+ "Ready",
27
+ "Sent",
28
+ "Failed"
29
+ ];
30
+ function readEntries(treeMap, selfId) {
31
+ const entries = [];
32
+ treeMap.forEach((raw, id) => {
33
+ if (selfId && id === selfId) return;
34
+ const value = toPlain(raw);
35
+ if (typeof value !== "object" || value === null) return;
36
+ entries.push({
37
+ id,
38
+ label: typeof value.label === "string" ? value.label : "Untitled",
39
+ parentId: value.parentId ?? null,
40
+ order: typeof value.order === "number" ? value.order : 0,
41
+ type: typeof value.type === "string" ? value.type : void 0
42
+ });
43
+ });
44
+ return entries;
45
+ }
46
+ async function createDoc(server, opts) {
47
+ const treeMap = server.getTreeMap();
48
+ const rootDoc = server.rootDocument;
49
+ if (!treeMap || !rootDoc) throw new Error("Cannot create doc — server is not connected.");
50
+ const id = crypto.randomUUID();
51
+ const now = Date.now();
52
+ await server.client.createChild(opts.restParentId, {
53
+ child_id: id,
54
+ label: opts.label,
55
+ doc_type: opts.type,
56
+ kind: "page"
57
+ });
58
+ rootDoc.transact(() => {
59
+ treeMap.set(id, makeEntryMap({
60
+ label: opts.label,
61
+ parentId: opts.parentTreeId,
62
+ order: opts.order ?? now,
63
+ type: opts.type,
64
+ meta: opts.meta,
65
+ createdAt: now,
66
+ updatedAt: now
67
+ }));
68
+ });
69
+ return id;
70
+ }
71
+ async function bootstrap(server) {
72
+ const treeMap = server.getTreeMap();
73
+ const spaceDocId = server.spaceDocId;
74
+ if (!treeMap || !spaceDocId) throw new Error("Cannot bootstrap — server is not connected.");
75
+ const topLevel = readEntries(treeMap, spaceDocId).filter((e) => e.parentId === null);
76
+ const existingInbox = topLevel.find((e) => e.label.trim().toLowerCase() === INBOX_LABEL.toLowerCase());
77
+ const existingOutbox = topLevel.find((e) => e.label.trim().toLowerCase() === OUTBOX_LABEL.toLowerCase());
78
+ let inboxId;
79
+ if (existingInbox) {
80
+ inboxId = existingInbox.id;
81
+ console.error(`[abracadabra-resend] Inbox found: ${inboxId} (${existingInbox.label})`);
82
+ } else {
83
+ inboxId = await createDoc(server, {
84
+ parentTreeId: null,
85
+ restParentId: spaceDocId,
86
+ label: INBOX_LABEL,
87
+ type: "gallery",
88
+ meta: { icon: "inbox" }
89
+ });
90
+ console.error(`[abracadabra-resend] Inbox created: ${inboxId}`);
91
+ }
92
+ let outboxId;
93
+ if (existingOutbox) {
94
+ outboxId = existingOutbox.id;
95
+ console.error(`[abracadabra-resend] Outbox found: ${outboxId} (${existingOutbox.label})`);
96
+ } else {
97
+ outboxId = await createDoc(server, {
98
+ parentTreeId: null,
99
+ restParentId: spaceDocId,
100
+ label: OUTBOX_LABEL,
101
+ type: "kanban",
102
+ meta: { icon: "send" }
103
+ });
104
+ console.error(`[abracadabra-resend] Outbox created: ${outboxId}`);
105
+ }
106
+ const existingColumns = readEntries(treeMap, spaceDocId).filter((e) => e.parentId === outboxId);
107
+ const columns = {};
108
+ for (const colLabel of OUTBOX_COLUMNS) {
109
+ const match = existingColumns.find((e) => e.label.trim().toLowerCase() === colLabel.toLowerCase());
110
+ if (match) {
111
+ columns[colLabel] = match.id;
112
+ continue;
113
+ }
114
+ const id = await createDoc(server, {
115
+ parentTreeId: outboxId,
116
+ restParentId: outboxId,
117
+ label: colLabel,
118
+ order: Date.now() + OUTBOX_COLUMNS.indexOf(colLabel)
119
+ });
120
+ columns[colLabel] = id;
121
+ console.error(`[abracadabra-resend] Outbox column "${colLabel}" created: ${id}`);
122
+ }
123
+ return {
124
+ inboxId,
125
+ outboxId,
126
+ columns
127
+ };
128
+ }
129
+
130
+ //#endregion
131
+ //#region packages/resend/src/utils.ts
132
+ /**
133
+ * Wait for a provider's `synced` event with a timeout.
134
+ *
135
+ * The `isSynced` short-circuit is load-bearing: providers that already synced
136
+ * (e.g. cached child providers returned from `loadChild`) won't re-emit
137
+ * `synced`, so without the short-circuit every later op on that doc times out.
138
+ */
139
+ function waitForSync(provider, timeoutMs = 15e3) {
140
+ if (provider.isSynced) return Promise.resolve();
141
+ return new Promise((resolve, reject) => {
142
+ const timer = setTimeout(() => {
143
+ provider.off("synced", handler);
144
+ reject(/* @__PURE__ */ new Error(`Sync timed out after ${timeoutMs}ms`));
145
+ }, timeoutMs);
146
+ function handler() {
147
+ clearTimeout(timer);
148
+ provider.off("synced", handler);
149
+ resolve();
150
+ }
151
+ provider.on("synced", handler);
152
+ });
153
+ }
154
+
155
+ //#endregion
156
+ //#region packages/resend/src/inbound-server.ts
157
+ /**
158
+ * Inbound webhook HTTP server.
159
+ *
160
+ * Single POST route: `/inbound`. Verifies Svix signature, parses Resend Inbound
161
+ * payload, creates a child doc under the Inbox, uploads attachments. Operator
162
+ * is responsible for exposing the port publicly (tunnel / reverse proxy) and
163
+ * configuring Resend Inbound to deliver here.
164
+ */
165
+ function addr(v) {
166
+ if (typeof v === "string") return v.trim() || void 0;
167
+ if (v && typeof v === "object") {
168
+ const obj = v;
169
+ if (typeof obj.email === "string") return obj.email.trim() || void 0;
170
+ }
171
+ }
172
+ function addrList(v) {
173
+ if (Array.isArray(v)) return v.map(addr).filter((s) => !!s);
174
+ const one = addr(v);
175
+ return one ? [one] : [];
176
+ }
177
+ function pickHeader(headers, name) {
178
+ if (!headers) return void 0;
179
+ const lower = name.toLowerCase();
180
+ for (const h of headers) if (typeof h?.name === "string" && h.name.toLowerCase() === lower) return h.value;
181
+ }
182
+ function decodeSecret(secret) {
183
+ const stripped = secret.startsWith("whsec_") ? secret.slice(6) : secret;
184
+ try {
185
+ return Buffer.from(stripped, "base64");
186
+ } catch {
187
+ return Buffer.from(stripped);
188
+ }
189
+ }
190
+ function constantTimeEquals(a, b) {
191
+ if (a.length !== b.length) return false;
192
+ return timingSafeEqual(a, b);
193
+ }
194
+ /**
195
+ * Verify Svix-style signature. Headers carry `svix-id`, `svix-timestamp`,
196
+ * `svix-signature` (space-separated `v1,<base64>` pairs). The signing input
197
+ * is `${id}.${timestamp}.${body}` HMAC-SHA256 with the decoded secret.
198
+ */
199
+ function verifySignature(body, headers, secret, toleranceSeconds) {
200
+ const id = headers["svix-id"];
201
+ const ts = headers["svix-timestamp"];
202
+ const sig = headers["svix-signature"];
203
+ if (typeof id !== "string" || typeof ts !== "string" || typeof sig !== "string") return {
204
+ ok: false,
205
+ reason: "missing Svix headers"
206
+ };
207
+ const tsNum = Number(ts);
208
+ if (!Number.isFinite(tsNum)) return {
209
+ ok: false,
210
+ reason: "invalid svix-timestamp"
211
+ };
212
+ const ageSec = Math.abs(Math.floor(Date.now() / 1e3) - tsNum);
213
+ if (ageSec > toleranceSeconds) return {
214
+ ok: false,
215
+ reason: `delivery too old (${ageSec}s)`
216
+ };
217
+ const toSign = `${id}.${ts}.${body}`;
218
+ const expected = createHmac("sha256", secret).update(toSign).digest();
219
+ const candidates = sig.split(" ").map((s) => s.trim()).filter(Boolean);
220
+ for (const candidate of candidates) {
221
+ const [version, value] = candidate.split(",");
222
+ if (version !== "v1" || !value) continue;
223
+ let provided;
224
+ try {
225
+ provided = Buffer.from(value, "base64");
226
+ } catch {
227
+ continue;
228
+ }
229
+ if (constantTimeEquals(expected, provided)) return { ok: true };
230
+ }
231
+ return {
232
+ ok: false,
233
+ reason: "signature mismatch"
234
+ };
235
+ }
236
+ function readBody(req, maxBytes = 25 * 1024 * 1024) {
237
+ return new Promise((resolve, reject) => {
238
+ const chunks = [];
239
+ let size = 0;
240
+ req.on("data", (chunk) => {
241
+ size += chunk.length;
242
+ if (size > maxBytes) {
243
+ reject(/* @__PURE__ */ new Error(`payload too large (>${maxBytes} bytes)`));
244
+ req.destroy();
245
+ return;
246
+ }
247
+ chunks.push(chunk);
248
+ });
249
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
250
+ req.on("error", reject);
251
+ });
252
+ }
253
+ var InboundServer = class {
254
+ constructor(opts) {
255
+ this.httpServer = null;
256
+ this.seenSvixIds = /* @__PURE__ */ new Set();
257
+ this.server = opts.server;
258
+ this.bootstrap = opts.bootstrap;
259
+ this.secret = decodeSecret(opts.secret);
260
+ this.toleranceSeconds = opts.toleranceSeconds ?? 300;
261
+ this.port = opts.port ?? 0;
262
+ this.host = opts.host ?? "0.0.0.0";
263
+ }
264
+ async start() {
265
+ this.httpServer = createServer((req, res) => {
266
+ this.handle(req, res);
267
+ });
268
+ await new Promise((resolve, reject) => {
269
+ this.httpServer.once("error", reject);
270
+ this.httpServer.listen(this.port, this.host, () => resolve());
271
+ });
272
+ const address = this.httpServer.address();
273
+ const boundPort = typeof address === "object" && address ? address.port : this.port;
274
+ console.error(`[abracadabra-resend] Inbound server listening on http://${this.host}:${boundPort}/inbound`);
275
+ return boundPort;
276
+ }
277
+ async stop() {
278
+ if (!this.httpServer) return;
279
+ await new Promise((resolve, reject) => {
280
+ this.httpServer.close((err) => err ? reject(err) : resolve());
281
+ });
282
+ this.httpServer = null;
283
+ }
284
+ async handle(req, res) {
285
+ try {
286
+ if (req.method === "GET" && (req.url === "/health" || req.url === "/")) {
287
+ res.writeHead(200, { "content-type": "text/plain" });
288
+ res.end("ok");
289
+ return;
290
+ }
291
+ if (req.method !== "POST" || req.url !== "/inbound") {
292
+ res.writeHead(404, { "content-type": "text/plain" });
293
+ res.end("not found");
294
+ return;
295
+ }
296
+ const body = await readBody(req);
297
+ const check = verifySignature(body, req.headers, this.secret, this.toleranceSeconds);
298
+ if (!check.ok) {
299
+ console.error(`[abracadabra-resend] inbound rejected: ${check.reason}`);
300
+ res.writeHead(401, { "content-type": "text/plain" });
301
+ res.end("unauthorized");
302
+ return;
303
+ }
304
+ const svixId = typeof req.headers["svix-id"] === "string" ? req.headers["svix-id"] : null;
305
+ if (svixId && this.seenSvixIds.has(svixId)) {
306
+ res.writeHead(200, { "content-type": "text/plain" });
307
+ res.end("duplicate");
308
+ return;
309
+ }
310
+ if (svixId) {
311
+ this.seenSvixIds.add(svixId);
312
+ if (this.seenSvixIds.size > 5e3) {
313
+ const first = this.seenSvixIds.values().next().value;
314
+ if (first) this.seenSvixIds.delete(first);
315
+ }
316
+ }
317
+ let env;
318
+ try {
319
+ env = JSON.parse(body);
320
+ } catch {
321
+ res.writeHead(400, { "content-type": "text/plain" });
322
+ res.end("invalid json");
323
+ return;
324
+ }
325
+ const data = env?.data ?? env;
326
+ await this.ingest(data);
327
+ res.writeHead(200, { "content-type": "text/plain" });
328
+ res.end("ok");
329
+ } catch (err) {
330
+ console.error(`[abracadabra-resend] inbound handler error: ${err?.message ?? err}`);
331
+ res.writeHead(500, { "content-type": "text/plain" });
332
+ res.end("error");
333
+ }
334
+ }
335
+ async ingest(data) {
336
+ await this.server.ensureConnected();
337
+ const treeMap = this.server.getTreeMap();
338
+ const rootDoc = this.server.rootDocument;
339
+ if (!treeMap || !rootDoc) throw new Error("Cannot ingest — server is not connected.");
340
+ const subjectRaw = typeof data.subject === "string" ? data.subject.trim() : "";
341
+ const subject = subjectRaw.length > 0 ? subjectRaw : "(no subject)";
342
+ const from = addr(data.from) ?? "(unknown)";
343
+ const to = addrList(data.to);
344
+ const cc = addrList(data.cc);
345
+ const inReplyTo = pickHeader(data.headers, "In-Reply-To");
346
+ const messageId = pickHeader(data.headers, "Message-ID") ?? data.id ?? null;
347
+ const inboxId = this.bootstrap.inboxId;
348
+ const id = crypto.randomUUID();
349
+ const now = Date.now();
350
+ const meta = {
351
+ icon: "mail",
352
+ from,
353
+ to,
354
+ cc: cc.length ? cc : void 0,
355
+ subject,
356
+ receivedAt: now,
357
+ messageId,
358
+ inReplyTo
359
+ };
360
+ if (typeof data.html === "string" && data.html.length > 0) meta.html = data.html;
361
+ await this.server.client.createChild(inboxId, {
362
+ child_id: id,
363
+ label: subject,
364
+ doc_type: "doc",
365
+ kind: "page"
366
+ });
367
+ rootDoc.transact(() => {
368
+ treeMap.set(id, makeEntryMap({
369
+ label: subject,
370
+ parentId: inboxId,
371
+ order: now,
372
+ type: "doc",
373
+ meta,
374
+ createdAt: now,
375
+ updatedAt: now
376
+ }));
377
+ });
378
+ const provider = await this.server.getChildProvider(id);
379
+ await waitForSync(provider);
380
+ populateYDocFromMarkdown(provider.document.getXmlFragment("default"), typeof data.text === "string" && data.text.length > 0 ? data.text : typeof data.html === "string" && data.html.length > 0 ? `<details><summary>HTML email (no text part)</summary>\n\n\`\`\`html\n${data.html}\n\`\`\`\n</details>` : "(empty body)", subject);
381
+ const attached = [];
382
+ const attachments = Array.isArray(data.attachments) ? data.attachments : [];
383
+ for (const att of attachments) {
384
+ if (typeof att?.content !== "string") continue;
385
+ const filename = typeof att.filename === "string" && att.filename.length > 0 ? att.filename : "attachment";
386
+ try {
387
+ const bytes = Buffer.from(att.content, "base64");
388
+ const blob = new Blob([bytes], { type: att.contentType ?? "application/octet-stream" });
389
+ const uploaded = await this.server.client.upload(id, blob, filename);
390
+ const uploadedId = uploaded?.id ?? uploaded?.uploadId ?? "";
391
+ attached.push({
392
+ id: uploadedId,
393
+ filename,
394
+ contentType: att.contentType,
395
+ size: bytes.length
396
+ });
397
+ } catch (err) {
398
+ console.error(`[abracadabra-resend] attachment upload failed (${filename}): ${err?.message ?? err}`);
399
+ }
400
+ }
401
+ if (attached.length > 0) {
402
+ const existingRaw = treeMap.get(id);
403
+ if (existingRaw) {
404
+ const existingMeta = (typeof existingRaw.toJSON === "function" ? existingRaw.toJSON()?.meta : existingRaw.meta) ?? {};
405
+ rootDoc.transact(() => {
406
+ treeMap.set(id, makeEntryMap({
407
+ label: subject,
408
+ parentId: inboxId,
409
+ order: now,
410
+ type: "doc",
411
+ meta: {
412
+ ...existingMeta,
413
+ attachments: attached
414
+ },
415
+ createdAt: now,
416
+ updatedAt: Date.now()
417
+ }));
418
+ });
419
+ }
420
+ }
421
+ console.error(`[abracadabra-resend] inbound stored: "${subject}" from ${from} → doc ${id}`);
422
+ }
423
+ };
424
+
425
+ //#endregion
426
+ //#region packages/resend/src/render.ts
427
+ /**
428
+ * Render an outbox document to an email-ready payload.
429
+ *
430
+ * Reads addressing from the doc's tree-entry `meta` (to/cc/bcc/subject/from/
431
+ * replyTo) and renders the body Y.XmlFragment via `@abraca/convert`'s
432
+ * `yjsToHtml` — which already emits a complete `<!DOCTYPE html>` doc with the
433
+ * doc label as `<h1>`, so the result is suitable as-is for Resend's `html`
434
+ * field.
435
+ */
436
+ var RenderError = class extends Error {
437
+ constructor(message) {
438
+ super(message);
439
+ this.name = "RenderError";
440
+ }
441
+ };
442
+ function asStringArray(value) {
443
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim());
444
+ if (typeof value === "string" && value.trim().length > 0) return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
445
+ return [];
446
+ }
447
+ function asOptionalString(value) {
448
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
449
+ }
450
+ function readEntry$1(server, docId) {
451
+ const treeMap = server.getTreeMap();
452
+ if (!treeMap) return null;
453
+ const raw = treeMap.get(docId);
454
+ if (!raw) return null;
455
+ const plain = toPlain(raw);
456
+ if (!plain || typeof plain !== "object") return null;
457
+ return {
458
+ label: typeof plain.label === "string" ? plain.label : "(no subject)",
459
+ meta: plain.meta && typeof plain.meta === "object" ? plain.meta : {}
460
+ };
461
+ }
462
+ async function renderEmail(server, docId, defaultFrom) {
463
+ const entry = readEntry$1(server, docId);
464
+ if (!entry) throw new RenderError(`Doc ${docId} has no tree entry.`);
465
+ const to = asStringArray(entry.meta.to);
466
+ if (to.length === 0) throw new RenderError(`Doc ${docId} has no recipients — set meta.to (array of email addresses).`);
467
+ const cc = asStringArray(entry.meta.cc);
468
+ const bcc = asStringArray(entry.meta.bcc);
469
+ const from = asOptionalString(entry.meta.from) ?? defaultFrom;
470
+ const replyTo = asOptionalString(entry.meta.replyTo);
471
+ const subject = asOptionalString(entry.meta.subject) ?? entry.label;
472
+ return {
473
+ subject,
474
+ html: yjsToHtml((await server.getChildProvider(docId)).document.getXmlFragment("default"), subject),
475
+ to,
476
+ cc: cc.length ? cc : void 0,
477
+ bcc: bcc.length ? bcc : void 0,
478
+ from,
479
+ replyTo
480
+ };
481
+ }
482
+
483
+ //#endregion
484
+ //#region packages/resend/src/outbox-watcher.ts
485
+ /**
486
+ * Outbox watcher. Observes the doc-tree map (deeply, so per-entry parentId
487
+ * updates surface) and dispatches any doc that lands under the "Ready" column.
488
+ *
489
+ * Dispatch flow per doc:
490
+ * render → resend.send → patch meta.resendId/sentAt → move to Sent
491
+ * On failure:
492
+ * patch meta.error/errorAt → move to Failed (left for human triage)
493
+ *
494
+ * Idempotency:
495
+ * - `meta.resendId` already set → skip (recovers from a restart that landed
496
+ * between Resend ack and the post-send move).
497
+ * - In-flight `Set<docId>` prevents the same observe burst from double-firing.
498
+ */
499
+ function readEntry(treeMap, id) {
500
+ const raw = treeMap.get(id);
501
+ if (!raw) return null;
502
+ const plain = toPlain(raw);
503
+ if (!plain || typeof plain !== "object") return null;
504
+ return {
505
+ id,
506
+ label: typeof plain.label === "string" ? plain.label : "Untitled",
507
+ parentId: plain.parentId ?? null,
508
+ meta: plain.meta && typeof plain.meta === "object" ? plain.meta : {}
509
+ };
510
+ }
511
+ var OutboxWatcher = class {
512
+ constructor(opts) {
513
+ this.inFlight = /* @__PURE__ */ new Set();
514
+ this.handled = /* @__PURE__ */ new Set();
515
+ this.observer = null;
516
+ this.treeMap = null;
517
+ this.rootDoc = null;
518
+ this.txHandler = null;
519
+ this.server = opts.server;
520
+ this.sender = opts.sender;
521
+ this.bootstrap = opts.bootstrap;
522
+ this.defaultFrom = opts.defaultFrom;
523
+ }
524
+ start() {
525
+ const treeMap = this.server.getTreeMap();
526
+ if (!treeMap) throw new Error("OutboxWatcher.start: server is not connected");
527
+ this.treeMap = treeMap;
528
+ const rootDoc = this.server.rootDocument;
529
+ if (!rootDoc) throw new Error("OutboxWatcher.start: root doc not connected");
530
+ this.rootDoc = rootDoc;
531
+ this.scan("init");
532
+ const obs = (events) => {
533
+ console.error(`[abracadabra-resend] observeDeep fired (${events.length} events)`);
534
+ this.scan("observeDeep");
535
+ };
536
+ treeMap.observeDeep(obs);
537
+ this.observer = obs;
538
+ const onTx = (tx) => {
539
+ const changed = [];
540
+ for (const [type, events] of tx.changedParentTypes.entries()) {
541
+ const name = type._item ? type._item.parentSub : type.constructor.name;
542
+ const keys = events.flatMap((ev) => Array.from(ev.keysChanged ?? []));
543
+ changed.push(`${name ?? "type"}[${keys.join(",")}]`);
544
+ }
545
+ console.error(`[abracadabra-resend] root afterTransaction (local=${tx.local}, origin=${String(tx.origin)}, changed=${changed.join(" | ") || "(none)"})`);
546
+ this.scan("afterTransaction");
547
+ };
548
+ rootDoc.on("afterTransaction", onTx);
549
+ this.txHandler = onTx;
550
+ const onSubdocs = (changes) => {
551
+ console.error(`[abracadabra-resend] subdocs: added=${changes.added.size} loaded=${changes.loaded.size}`);
552
+ for (const sub of [...changes.added, ...changes.loaded]) sub.on("afterTransaction", (tx) => {
553
+ console.error(`[abracadabra-resend] subdoc afterTransaction (guid=${sub.guid}, local=${tx.local})`);
554
+ this.scan("subdoc");
555
+ });
556
+ };
557
+ rootDoc.on("subdocs", onSubdocs);
558
+ rootDoc.on("update", (_update, origin) => {
559
+ console.error(`[abracadabra-resend] root update applied (origin=${String(origin)})`);
560
+ this.scan("update");
561
+ });
562
+ console.error(`[abracadabra-resend] Outbox watcher attached (ready column ${this.bootstrap.columns.Ready})`);
563
+ }
564
+ stop() {
565
+ if (this.rootDoc && this.txHandler) this.rootDoc.off("afterTransaction", this.txHandler);
566
+ if (this.treeMap && this.observer) this.treeMap.unobserveDeep(this.observer);
567
+ this.txHandler = null;
568
+ this.rootDoc = null;
569
+ this.observer = null;
570
+ this.treeMap = null;
571
+ }
572
+ async scan(reason) {
573
+ const treeMap = this.treeMap;
574
+ if (!treeMap) return;
575
+ const readyColId = this.bootstrap.columns.Ready;
576
+ const outboxId = this.bootstrap.outboxId;
577
+ const columnIds = new Set(Object.values(this.bootstrap.columns));
578
+ const candidates = [];
579
+ let inReadyCount = 0;
580
+ let totalEntries = 0;
581
+ const outboxSubtree = [];
582
+ treeMap.forEach((_raw, id) => {
583
+ totalEntries++;
584
+ const e = readEntry(treeMap, id);
585
+ if (!e) return;
586
+ if (e.parentId === outboxId || e.parentId && columnIds.has(e.parentId)) outboxSubtree.push({
587
+ id,
588
+ label: e.label,
589
+ parentId: e.parentId,
590
+ under: e.parentId === outboxId ? "Outbox" : e.parentId === this.bootstrap.columns.Draft ? "Draft" : e.parentId === this.bootstrap.columns.Ready ? "Ready" : e.parentId === this.bootstrap.columns.Sent ? "Sent" : e.parentId === this.bootstrap.columns.Failed ? "Failed" : "?"
591
+ });
592
+ if (e.parentId !== readyColId) return;
593
+ inReadyCount++;
594
+ if (this.inFlight.has(id) || this.handled.has(id)) return;
595
+ if (typeof e.meta.resendId === "string" && e.meta.resendId.length > 0) {
596
+ this.handled.add(id);
597
+ this.moveTo(id, this.bootstrap.columns.Sent);
598
+ return;
599
+ }
600
+ candidates.push(e);
601
+ });
602
+ console.error(`[abracadabra-resend] scan(${reason}): ${totalEntries} entries, ${inReadyCount} in Ready, ${candidates.length} to dispatch | Outbox subtree (${outboxSubtree.length}): ${outboxSubtree.map((e) => `${e.under}:"${e.label}"`).join(", ")}`);
603
+ for (const entry of candidates) {
604
+ this.inFlight.add(entry.id);
605
+ this.dispatch(entry).finally(() => {
606
+ this.inFlight.delete(entry.id);
607
+ });
608
+ }
609
+ }
610
+ async dispatch(entry) {
611
+ const { id, label } = entry;
612
+ try {
613
+ const payload = await renderEmail(this.server, id, this.defaultFrom);
614
+ console.error(`[abracadabra-resend] sending "${payload.subject}" → ${payload.to.join(", ")} (doc=${id})`);
615
+ const { id: resendId } = await this.sender.send(payload, { "X-Abra-Doc-Id": id });
616
+ this.handled.add(id);
617
+ this.patchMeta(id, {
618
+ resendId,
619
+ sentAt: Date.now(),
620
+ error: null
621
+ });
622
+ this.moveTo(id, this.bootstrap.columns.Sent);
623
+ console.error(`[abracadabra-resend] sent "${payload.subject}" (resend=${resendId}, doc=${id})`);
624
+ } catch (err) {
625
+ this.handled.add(id);
626
+ const message = err instanceof RenderError ? err.message : err?.message ? String(err.message) : String(err);
627
+ console.error(`[abracadabra-resend] send failed for "${label}" (${id}): ${message}`);
628
+ this.patchMeta(id, {
629
+ error: message,
630
+ errorAt: Date.now()
631
+ });
632
+ this.moveTo(id, this.bootstrap.columns.Failed);
633
+ }
634
+ }
635
+ patchMeta(id, patch) {
636
+ const treeMap = this.treeMap;
637
+ const rootDoc = this.server.rootDocument;
638
+ if (!treeMap || !rootDoc) return;
639
+ const current = readEntry(treeMap, id);
640
+ if (!current) return;
641
+ const nextMeta = {
642
+ ...current.meta,
643
+ ...patch
644
+ };
645
+ for (const [k, v] of Object.entries(patch)) if (v === null) delete nextMeta[k];
646
+ rootDoc.transact(() => {
647
+ patchEntry(treeMap, id, {
648
+ meta: nextMeta,
649
+ updatedAt: Date.now()
650
+ });
651
+ });
652
+ }
653
+ moveTo(id, newParentId) {
654
+ const treeMap = this.treeMap;
655
+ const rootDoc = this.server.rootDocument;
656
+ if (!treeMap || !rootDoc) return;
657
+ const now = Date.now();
658
+ rootDoc.transact(() => {
659
+ patchEntry(treeMap, id, {
660
+ parentId: newParentId,
661
+ order: now,
662
+ updatedAt: now
663
+ });
664
+ });
665
+ this.server.client.updateDocumentMeta(id, { parent_id: newParentId }).catch((e) => {
666
+ console.error(`[abracadabra-resend] REST reparent failed for ${id}: ${e?.message ?? e}`);
667
+ });
668
+ }
669
+ };
670
+
671
+ //#endregion
672
+ //#region packages/resend/src/resend-client.ts
673
+ /**
674
+ * Thin wrapper around the official `resend` SDK. Kept separate so tests can
675
+ * substitute a fake without mocking the SDK directly.
676
+ */
677
+ var ResendClient = class {
678
+ constructor(apiKey) {
679
+ this.resend = new Resend(apiKey);
680
+ }
681
+ async send(payload, headers) {
682
+ if (!payload.from) throw new Error("ResendClient.send: payload.from is required");
683
+ const { data, error } = await this.resend.emails.send({
684
+ from: payload.from,
685
+ to: payload.to,
686
+ cc: payload.cc,
687
+ bcc: payload.bcc,
688
+ subject: payload.subject,
689
+ html: payload.html,
690
+ replyTo: payload.replyTo,
691
+ headers
692
+ });
693
+ if (error) throw new Error(`Resend rejected the message: ${error.message ?? JSON.stringify(error)}`);
694
+ if (!data?.id) throw new Error("Resend returned no id for the dispatched message");
695
+ return { id: data.id };
696
+ }
697
+ };
698
+
699
+ //#endregion
700
+ //#region node_modules/@noble/hashes/utils.js
701
+ /**
702
+ * Checks if something is Uint8Array. Be careful: nodejs Buffer will return true.
703
+ * @param a - value to test
704
+ * @returns `true` when the value is a Uint8Array-compatible view.
705
+ * @example
706
+ * Check whether a value is a Uint8Array-compatible view.
707
+ * ```ts
708
+ * isBytes(new Uint8Array([1, 2, 3]));
709
+ * ```
710
+ */
711
+ function isBytes(a) {
712
+ return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
713
+ }
714
+ /**
715
+ * Asserts something is Uint8Array.
716
+ * @param value - value to validate
717
+ * @param length - optional exact length constraint
718
+ * @param title - label included in thrown errors
719
+ * @returns The validated byte array.
720
+ * @throws On wrong argument types. {@link TypeError}
721
+ * @throws On wrong argument ranges or values. {@link RangeError}
722
+ * @example
723
+ * Validate that a value is a byte array.
724
+ * ```ts
725
+ * abytes(new Uint8Array([1, 2, 3]));
726
+ * ```
727
+ */
728
+ function abytes(value, length, title = "") {
729
+ const bytes = isBytes(value);
730
+ const len = value?.length;
731
+ const needsLen = length !== void 0;
732
+ if (!bytes || needsLen && len !== length) {
733
+ const prefix = title && `"${title}" `;
734
+ const ofLen = needsLen ? ` of length ${length}` : "";
735
+ const got = bytes ? `length=${len}` : `type=${typeof value}`;
736
+ const message = prefix + "expected Uint8Array" + ofLen + ", got " + got;
737
+ if (!bytes) throw new TypeError(message);
738
+ throw new RangeError(message);
739
+ }
740
+ return value;
741
+ }
742
+ /**
743
+ * Asserts a hash instance has not been destroyed or finished.
744
+ * @param instance - hash instance to validate
745
+ * @param checkFinished - whether to reject finalized instances
746
+ * @throws If the hash instance has already been destroyed or finalized. {@link Error}
747
+ * @example
748
+ * Validate that a hash instance is still usable.
749
+ * ```ts
750
+ * import { aexists } from '@noble/hashes/utils.js';
751
+ * import { sha256 } from '@noble/hashes/sha2.js';
752
+ * const hash = sha256.create();
753
+ * aexists(hash);
754
+ * ```
755
+ */
756
+ function aexists(instance, checkFinished = true) {
757
+ if (instance.destroyed) throw new Error("Hash instance has been destroyed");
758
+ if (checkFinished && instance.finished) throw new Error("Hash#digest() has already been called");
759
+ }
760
+ /**
761
+ * Asserts output is a sufficiently-sized byte array.
762
+ * @param out - destination buffer
763
+ * @param instance - hash instance providing output length
764
+ * Oversized buffers are allowed; downstream code only promises to fill the first `outputLen` bytes.
765
+ * @throws On wrong argument types. {@link TypeError}
766
+ * @throws On wrong argument ranges or values. {@link RangeError}
767
+ * @example
768
+ * Validate a caller-provided digest buffer.
769
+ * ```ts
770
+ * import { aoutput } from '@noble/hashes/utils.js';
771
+ * import { sha256 } from '@noble/hashes/sha2.js';
772
+ * const hash = sha256.create();
773
+ * aoutput(new Uint8Array(hash.outputLen), hash);
774
+ * ```
775
+ */
776
+ function aoutput(out, instance) {
777
+ abytes(out, void 0, "digestInto() output");
778
+ const min = instance.outputLen;
779
+ if (out.length < min) throw new RangeError("\"digestInto() output\" expected to be of length >=" + min);
780
+ }
781
+ /**
782
+ * Zeroizes typed arrays in place. Warning: JS provides no guarantees.
783
+ * @param arrays - arrays to overwrite with zeros
784
+ * @example
785
+ * Zeroize sensitive buffers in place.
786
+ * ```ts
787
+ * clean(new Uint8Array([1, 2, 3]));
788
+ * ```
789
+ */
790
+ function clean(...arrays) {
791
+ for (let i = 0; i < arrays.length; i++) arrays[i].fill(0);
792
+ }
793
+ /**
794
+ * Creates a DataView for byte-level manipulation.
795
+ * @param arr - source typed array
796
+ * @returns DataView over the same buffer region.
797
+ * @example
798
+ * Create a DataView over an existing buffer.
799
+ * ```ts
800
+ * createView(new Uint8Array(4));
801
+ * ```
802
+ */
803
+ function createView(arr) {
804
+ return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
805
+ }
806
+ /** Whether the current platform is little-endian. */
807
+ const isLE = new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68;
808
+ const hasHexBuiltin = typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function";
809
+ /**
810
+ * Creates a callable hash function from a stateful class constructor.
811
+ * @param hashCons - hash constructor or factory
812
+ * @param info - optional metadata such as DER OID
813
+ * @returns Frozen callable hash wrapper with `.create()`.
814
+ * Wrapper construction eagerly calls `hashCons(undefined)` once to read
815
+ * `outputLen` / `blockLen`, so constructor side effects happen at module
816
+ * init time.
817
+ * @example
818
+ * Wrap a stateful hash constructor into a callable helper.
819
+ * ```ts
820
+ * import { createHasher } from '@noble/hashes/utils.js';
821
+ * import { sha256 } from '@noble/hashes/sha2.js';
822
+ * const wrapped = createHasher(sha256.create, { oid: sha256.oid });
823
+ * wrapped(new Uint8Array([1]));
824
+ * ```
825
+ */
826
+ function createHasher(hashCons, info = {}) {
827
+ const hashC = (msg, opts) => hashCons(opts).update(msg).digest();
828
+ const tmp = hashCons(void 0);
829
+ hashC.outputLen = tmp.outputLen;
830
+ hashC.blockLen = tmp.blockLen;
831
+ hashC.canXOF = tmp.canXOF;
832
+ hashC.create = (opts) => hashCons(opts);
833
+ Object.assign(hashC, info);
834
+ return Object.freeze(hashC);
835
+ }
836
+ /**
837
+ * Creates OID metadata for NIST hashes with prefix `06 09 60 86 48 01 65 03 04 02`.
838
+ * @param suffix - final OID byte for the selected hash.
839
+ * The helper accepts any byte even though only the documented NIST hash
840
+ * suffixes are meaningful downstream.
841
+ * @returns Object containing the DER-encoded OID.
842
+ * @example
843
+ * Build OID metadata for a NIST hash.
844
+ * ```ts
845
+ * oidNist(0x01);
846
+ * ```
847
+ */
848
+ const oidNist = (suffix) => ({ oid: Uint8Array.from([
849
+ 6,
850
+ 9,
851
+ 96,
852
+ 134,
853
+ 72,
854
+ 1,
855
+ 101,
856
+ 3,
857
+ 4,
858
+ 2,
859
+ suffix
860
+ ]) });
861
+
862
+ //#endregion
863
+ //#region node_modules/@noble/hashes/_md.js
864
+ /**
865
+ * Internal Merkle-Damgard hash utils.
866
+ * @module
867
+ */
868
+ /**
869
+ * Merkle-Damgard hash construction base class.
870
+ * Could be used to create MD5, RIPEMD, SHA1, SHA2.
871
+ * Accepts only byte-aligned `Uint8Array` input, even when the underlying spec describes bit
872
+ * strings with partial-byte tails.
873
+ * @param blockLen - internal block size in bytes
874
+ * @param outputLen - digest size in bytes
875
+ * @param padOffset - trailing length field size in bytes
876
+ * @param isLE - whether length and state words are encoded in little-endian
877
+ * @example
878
+ * Use a concrete subclass to get the shared Merkle-Damgard update/digest flow.
879
+ * ```ts
880
+ * import { _SHA1 } from '@noble/hashes/legacy.js';
881
+ * const hash = new _SHA1();
882
+ * hash.update(new Uint8Array([97, 98, 99]));
883
+ * hash.digest();
884
+ * ```
885
+ */
886
+ var HashMD = class {
887
+ blockLen;
888
+ outputLen;
889
+ canXOF = false;
890
+ padOffset;
891
+ isLE;
892
+ buffer;
893
+ view;
894
+ finished = false;
895
+ length = 0;
896
+ pos = 0;
897
+ destroyed = false;
898
+ constructor(blockLen, outputLen, padOffset, isLE) {
899
+ this.blockLen = blockLen;
900
+ this.outputLen = outputLen;
901
+ this.padOffset = padOffset;
902
+ this.isLE = isLE;
903
+ this.buffer = new Uint8Array(blockLen);
904
+ this.view = createView(this.buffer);
905
+ }
906
+ update(data) {
907
+ aexists(this);
908
+ abytes(data);
909
+ const { view, buffer, blockLen } = this;
910
+ const len = data.length;
911
+ for (let pos = 0; pos < len;) {
912
+ const take = Math.min(blockLen - this.pos, len - pos);
913
+ if (take === blockLen) {
914
+ const dataView = createView(data);
915
+ for (; blockLen <= len - pos; pos += blockLen) this.process(dataView, pos);
916
+ continue;
917
+ }
918
+ buffer.set(data.subarray(pos, pos + take), this.pos);
919
+ this.pos += take;
920
+ pos += take;
921
+ if (this.pos === blockLen) {
922
+ this.process(view, 0);
923
+ this.pos = 0;
924
+ }
925
+ }
926
+ this.length += data.length;
927
+ this.roundClean();
928
+ return this;
929
+ }
930
+ digestInto(out) {
931
+ aexists(this);
932
+ aoutput(out, this);
933
+ this.finished = true;
934
+ const { buffer, view, blockLen, isLE } = this;
935
+ let { pos } = this;
936
+ buffer[pos++] = 128;
937
+ clean(this.buffer.subarray(pos));
938
+ if (this.padOffset > blockLen - pos) {
939
+ this.process(view, 0);
940
+ pos = 0;
941
+ }
942
+ for (let i = pos; i < blockLen; i++) buffer[i] = 0;
943
+ view.setBigUint64(blockLen - 8, BigInt(this.length * 8), isLE);
944
+ this.process(view, 0);
945
+ const oview = createView(out);
946
+ const len = this.outputLen;
947
+ if (len % 4) throw new Error("_sha2: outputLen must be aligned to 32bit");
948
+ const outLen = len / 4;
949
+ const state = this.get();
950
+ if (outLen > state.length) throw new Error("_sha2: outputLen bigger than state");
951
+ for (let i = 0; i < outLen; i++) oview.setUint32(4 * i, state[i], isLE);
952
+ }
953
+ digest() {
954
+ const { buffer, outputLen } = this;
955
+ this.digestInto(buffer);
956
+ const res = buffer.slice(0, outputLen);
957
+ this.destroy();
958
+ return res;
959
+ }
960
+ _cloneInto(to) {
961
+ to ||= new this.constructor();
962
+ to.set(...this.get());
963
+ const { blockLen, buffer, length, finished, destroyed, pos } = this;
964
+ to.destroyed = destroyed;
965
+ to.finished = finished;
966
+ to.length = length;
967
+ to.pos = pos;
968
+ if (length % blockLen) to.buffer.set(buffer);
969
+ return to;
970
+ }
971
+ clone() {
972
+ return this._cloneInto();
973
+ }
974
+ };
975
+ /** Initial SHA512 state from RFC 6234 §6.3: eight RFC 64-bit `H(0)` words stored as sixteen
976
+ * big-endian 32-bit halves. Derived from the fractional parts of the square roots of the first
977
+ * eight prime numbers. Exported as a shared table; callers must treat it as read-only because
978
+ * constructors copy halves from it by index. */
979
+ const SHA512_IV = /* @__PURE__ */ Uint32Array.from([
980
+ 1779033703,
981
+ 4089235720,
982
+ 3144134277,
983
+ 2227873595,
984
+ 1013904242,
985
+ 4271175723,
986
+ 2773480762,
987
+ 1595750129,
988
+ 1359893119,
989
+ 2917565137,
990
+ 2600822924,
991
+ 725511199,
992
+ 528734635,
993
+ 4215389547,
994
+ 1541459225,
995
+ 327033209
996
+ ]);
997
+
998
+ //#endregion
999
+ //#region node_modules/@noble/hashes/_u64.js
1000
+ const U32_MASK64 = /* @__PURE__ */ BigInt(2 ** 32 - 1);
1001
+ const _32n = /* @__PURE__ */ BigInt(32);
1002
+ function fromBig(n, le = false) {
1003
+ if (le) return {
1004
+ h: Number(n & U32_MASK64),
1005
+ l: Number(n >> _32n & U32_MASK64)
1006
+ };
1007
+ return {
1008
+ h: Number(n >> _32n & U32_MASK64) | 0,
1009
+ l: Number(n & U32_MASK64) | 0
1010
+ };
1011
+ }
1012
+ function split(lst, le = false) {
1013
+ const len = lst.length;
1014
+ let Ah = new Uint32Array(len);
1015
+ let Al = new Uint32Array(len);
1016
+ for (let i = 0; i < len; i++) {
1017
+ const { h, l } = fromBig(lst[i], le);
1018
+ [Ah[i], Al[i]] = [h, l];
1019
+ }
1020
+ return [Ah, Al];
1021
+ }
1022
+ const shrSH = (h, _l, s) => h >>> s;
1023
+ const shrSL = (h, l, s) => h << 32 - s | l >>> s;
1024
+ const rotrSH = (h, l, s) => h >>> s | l << 32 - s;
1025
+ const rotrSL = (h, l, s) => h << 32 - s | l >>> s;
1026
+ const rotrBH = (h, l, s) => h << 64 - s | l >>> s - 32;
1027
+ const rotrBL = (h, l, s) => h >>> s - 32 | l << 64 - s;
1028
+ function add(Ah, Al, Bh, Bl) {
1029
+ const l = (Al >>> 0) + (Bl >>> 0);
1030
+ return {
1031
+ h: Ah + Bh + (l / 2 ** 32 | 0) | 0,
1032
+ l: l | 0
1033
+ };
1034
+ }
1035
+ const add3L = (Al, Bl, Cl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0);
1036
+ const add3H = (low, Ah, Bh, Ch) => Ah + Bh + Ch + (low / 2 ** 32 | 0) | 0;
1037
+ const add4L = (Al, Bl, Cl, Dl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0);
1038
+ const add4H = (low, Ah, Bh, Ch, Dh) => Ah + Bh + Ch + Dh + (low / 2 ** 32 | 0) | 0;
1039
+ const add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0) + (El >>> 0);
1040
+ const add5H = (low, Ah, Bh, Ch, Dh, Eh) => Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0;
1041
+
1042
+ //#endregion
1043
+ //#region node_modules/@noble/hashes/sha2.js
1044
+ /**
1045
+ * SHA2 hash function. A.k.a. sha256, sha384, sha512, sha512_224, sha512_256.
1046
+ * SHA256 is the fastest hash implementable in JS, even faster than Blake3.
1047
+ * Check out {@link https://www.rfc-editor.org/rfc/rfc4634 | RFC 4634} and
1048
+ * {@link https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf | FIPS 180-4}.
1049
+ * @module
1050
+ */
1051
+ const K512 = split([
1052
+ "0x428a2f98d728ae22",
1053
+ "0x7137449123ef65cd",
1054
+ "0xb5c0fbcfec4d3b2f",
1055
+ "0xe9b5dba58189dbbc",
1056
+ "0x3956c25bf348b538",
1057
+ "0x59f111f1b605d019",
1058
+ "0x923f82a4af194f9b",
1059
+ "0xab1c5ed5da6d8118",
1060
+ "0xd807aa98a3030242",
1061
+ "0x12835b0145706fbe",
1062
+ "0x243185be4ee4b28c",
1063
+ "0x550c7dc3d5ffb4e2",
1064
+ "0x72be5d74f27b896f",
1065
+ "0x80deb1fe3b1696b1",
1066
+ "0x9bdc06a725c71235",
1067
+ "0xc19bf174cf692694",
1068
+ "0xe49b69c19ef14ad2",
1069
+ "0xefbe4786384f25e3",
1070
+ "0x0fc19dc68b8cd5b5",
1071
+ "0x240ca1cc77ac9c65",
1072
+ "0x2de92c6f592b0275",
1073
+ "0x4a7484aa6ea6e483",
1074
+ "0x5cb0a9dcbd41fbd4",
1075
+ "0x76f988da831153b5",
1076
+ "0x983e5152ee66dfab",
1077
+ "0xa831c66d2db43210",
1078
+ "0xb00327c898fb213f",
1079
+ "0xbf597fc7beef0ee4",
1080
+ "0xc6e00bf33da88fc2",
1081
+ "0xd5a79147930aa725",
1082
+ "0x06ca6351e003826f",
1083
+ "0x142929670a0e6e70",
1084
+ "0x27b70a8546d22ffc",
1085
+ "0x2e1b21385c26c926",
1086
+ "0x4d2c6dfc5ac42aed",
1087
+ "0x53380d139d95b3df",
1088
+ "0x650a73548baf63de",
1089
+ "0x766a0abb3c77b2a8",
1090
+ "0x81c2c92e47edaee6",
1091
+ "0x92722c851482353b",
1092
+ "0xa2bfe8a14cf10364",
1093
+ "0xa81a664bbc423001",
1094
+ "0xc24b8b70d0f89791",
1095
+ "0xc76c51a30654be30",
1096
+ "0xd192e819d6ef5218",
1097
+ "0xd69906245565a910",
1098
+ "0xf40e35855771202a",
1099
+ "0x106aa07032bbd1b8",
1100
+ "0x19a4c116b8d2d0c8",
1101
+ "0x1e376c085141ab53",
1102
+ "0x2748774cdf8eeb99",
1103
+ "0x34b0bcb5e19b48a8",
1104
+ "0x391c0cb3c5c95a63",
1105
+ "0x4ed8aa4ae3418acb",
1106
+ "0x5b9cca4f7763e373",
1107
+ "0x682e6ff3d6b2b8a3",
1108
+ "0x748f82ee5defb2fc",
1109
+ "0x78a5636f43172f60",
1110
+ "0x84c87814a1f0ab72",
1111
+ "0x8cc702081a6439ec",
1112
+ "0x90befffa23631e28",
1113
+ "0xa4506cebde82bde9",
1114
+ "0xbef9a3f7b2c67915",
1115
+ "0xc67178f2e372532b",
1116
+ "0xca273eceea26619c",
1117
+ "0xd186b8c721c0c207",
1118
+ "0xeada7dd6cde0eb1e",
1119
+ "0xf57d4f7fee6ed178",
1120
+ "0x06f067aa72176fba",
1121
+ "0x0a637dc5a2c898a6",
1122
+ "0x113f9804bef90dae",
1123
+ "0x1b710b35131c471b",
1124
+ "0x28db77f523047d84",
1125
+ "0x32caab7b40c72493",
1126
+ "0x3c9ebe0a15c9bebc",
1127
+ "0x431d67c49c100d4c",
1128
+ "0x4cc5d4becb3e42b6",
1129
+ "0x597f299cfc657e2a",
1130
+ "0x5fcb6fab3ad6faec",
1131
+ "0x6c44198c4a475817"
1132
+ ].map((n) => BigInt(n)));
1133
+ const SHA512_Kh = K512[0];
1134
+ const SHA512_Kl = K512[1];
1135
+ const SHA512_W_H = /* @__PURE__ */ new Uint32Array(80);
1136
+ const SHA512_W_L = /* @__PURE__ */ new Uint32Array(80);
1137
+ /** Internal SHA-384 / SHA-512 compression engine from RFC 6234 §6.4. */
1138
+ var SHA2_64B = class extends HashMD {
1139
+ constructor(outputLen) {
1140
+ super(128, outputLen, 16, false);
1141
+ }
1142
+ get() {
1143
+ const { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
1144
+ return [
1145
+ Ah,
1146
+ Al,
1147
+ Bh,
1148
+ Bl,
1149
+ Ch,
1150
+ Cl,
1151
+ Dh,
1152
+ Dl,
1153
+ Eh,
1154
+ El,
1155
+ Fh,
1156
+ Fl,
1157
+ Gh,
1158
+ Gl,
1159
+ Hh,
1160
+ Hl
1161
+ ];
1162
+ }
1163
+ set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl) {
1164
+ this.Ah = Ah | 0;
1165
+ this.Al = Al | 0;
1166
+ this.Bh = Bh | 0;
1167
+ this.Bl = Bl | 0;
1168
+ this.Ch = Ch | 0;
1169
+ this.Cl = Cl | 0;
1170
+ this.Dh = Dh | 0;
1171
+ this.Dl = Dl | 0;
1172
+ this.Eh = Eh | 0;
1173
+ this.El = El | 0;
1174
+ this.Fh = Fh | 0;
1175
+ this.Fl = Fl | 0;
1176
+ this.Gh = Gh | 0;
1177
+ this.Gl = Gl | 0;
1178
+ this.Hh = Hh | 0;
1179
+ this.Hl = Hl | 0;
1180
+ }
1181
+ process(view, offset) {
1182
+ for (let i = 0; i < 16; i++, offset += 4) {
1183
+ SHA512_W_H[i] = view.getUint32(offset);
1184
+ SHA512_W_L[i] = view.getUint32(offset += 4);
1185
+ }
1186
+ for (let i = 16; i < 80; i++) {
1187
+ const W15h = SHA512_W_H[i - 15] | 0;
1188
+ const W15l = SHA512_W_L[i - 15] | 0;
1189
+ const s0h = rotrSH(W15h, W15l, 1) ^ rotrSH(W15h, W15l, 8) ^ shrSH(W15h, W15l, 7);
1190
+ const s0l = rotrSL(W15h, W15l, 1) ^ rotrSL(W15h, W15l, 8) ^ shrSL(W15h, W15l, 7);
1191
+ const W2h = SHA512_W_H[i - 2] | 0;
1192
+ const W2l = SHA512_W_L[i - 2] | 0;
1193
+ const s1h = rotrSH(W2h, W2l, 19) ^ rotrBH(W2h, W2l, 61) ^ shrSH(W2h, W2l, 6);
1194
+ const s1l = rotrSL(W2h, W2l, 19) ^ rotrBL(W2h, W2l, 61) ^ shrSL(W2h, W2l, 6);
1195
+ const SUMl = add4L(s0l, s1l, SHA512_W_L[i - 7], SHA512_W_L[i - 16]);
1196
+ SHA512_W_H[i] = add4H(SUMl, s0h, s1h, SHA512_W_H[i - 7], SHA512_W_H[i - 16]) | 0;
1197
+ SHA512_W_L[i] = SUMl | 0;
1198
+ }
1199
+ let { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
1200
+ for (let i = 0; i < 80; i++) {
1201
+ const sigma1h = rotrSH(Eh, El, 14) ^ rotrSH(Eh, El, 18) ^ rotrBH(Eh, El, 41);
1202
+ const sigma1l = rotrSL(Eh, El, 14) ^ rotrSL(Eh, El, 18) ^ rotrBL(Eh, El, 41);
1203
+ const CHIh = Eh & Fh ^ ~Eh & Gh;
1204
+ const CHIl = El & Fl ^ ~El & Gl;
1205
+ const T1ll = add5L(Hl, sigma1l, CHIl, SHA512_Kl[i], SHA512_W_L[i]);
1206
+ const T1h = add5H(T1ll, Hh, sigma1h, CHIh, SHA512_Kh[i], SHA512_W_H[i]);
1207
+ const T1l = T1ll | 0;
1208
+ const sigma0h = rotrSH(Ah, Al, 28) ^ rotrBH(Ah, Al, 34) ^ rotrBH(Ah, Al, 39);
1209
+ const sigma0l = rotrSL(Ah, Al, 28) ^ rotrBL(Ah, Al, 34) ^ rotrBL(Ah, Al, 39);
1210
+ const MAJh = Ah & Bh ^ Ah & Ch ^ Bh & Ch;
1211
+ const MAJl = Al & Bl ^ Al & Cl ^ Bl & Cl;
1212
+ Hh = Gh | 0;
1213
+ Hl = Gl | 0;
1214
+ Gh = Fh | 0;
1215
+ Gl = Fl | 0;
1216
+ Fh = Eh | 0;
1217
+ Fl = El | 0;
1218
+ ({h: Eh, l: El} = add(Dh | 0, Dl | 0, T1h | 0, T1l | 0));
1219
+ Dh = Ch | 0;
1220
+ Dl = Cl | 0;
1221
+ Ch = Bh | 0;
1222
+ Cl = Bl | 0;
1223
+ Bh = Ah | 0;
1224
+ Bl = Al | 0;
1225
+ const All = add3L(T1l, sigma0l, MAJl);
1226
+ Ah = add3H(All, T1h, sigma0h, MAJh);
1227
+ Al = All | 0;
1228
+ }
1229
+ ({h: Ah, l: Al} = add(this.Ah | 0, this.Al | 0, Ah | 0, Al | 0));
1230
+ ({h: Bh, l: Bl} = add(this.Bh | 0, this.Bl | 0, Bh | 0, Bl | 0));
1231
+ ({h: Ch, l: Cl} = add(this.Ch | 0, this.Cl | 0, Ch | 0, Cl | 0));
1232
+ ({h: Dh, l: Dl} = add(this.Dh | 0, this.Dl | 0, Dh | 0, Dl | 0));
1233
+ ({h: Eh, l: El} = add(this.Eh | 0, this.El | 0, Eh | 0, El | 0));
1234
+ ({h: Fh, l: Fl} = add(this.Fh | 0, this.Fl | 0, Fh | 0, Fl | 0));
1235
+ ({h: Gh, l: Gl} = add(this.Gh | 0, this.Gl | 0, Gh | 0, Gl | 0));
1236
+ ({h: Hh, l: Hl} = add(this.Hh | 0, this.Hl | 0, Hh | 0, Hl | 0));
1237
+ this.set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl);
1238
+ }
1239
+ roundClean() {
1240
+ clean(SHA512_W_H, SHA512_W_L);
1241
+ }
1242
+ destroy() {
1243
+ this.destroyed = true;
1244
+ clean(this.buffer);
1245
+ this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
1246
+ }
1247
+ };
1248
+ /** Internal SHA-512 hash class grounded in RFC 6234 §6.3 and §6.4. */
1249
+ var _SHA512 = class extends SHA2_64B {
1250
+ Ah = SHA512_IV[0] | 0;
1251
+ Al = SHA512_IV[1] | 0;
1252
+ Bh = SHA512_IV[2] | 0;
1253
+ Bl = SHA512_IV[3] | 0;
1254
+ Ch = SHA512_IV[4] | 0;
1255
+ Cl = SHA512_IV[5] | 0;
1256
+ Dh = SHA512_IV[6] | 0;
1257
+ Dl = SHA512_IV[7] | 0;
1258
+ Eh = SHA512_IV[8] | 0;
1259
+ El = SHA512_IV[9] | 0;
1260
+ Fh = SHA512_IV[10] | 0;
1261
+ Fl = SHA512_IV[11] | 0;
1262
+ Gh = SHA512_IV[12] | 0;
1263
+ Gl = SHA512_IV[13] | 0;
1264
+ Hh = SHA512_IV[14] | 0;
1265
+ Hl = SHA512_IV[15] | 0;
1266
+ constructor() {
1267
+ super(64);
1268
+ }
1269
+ };
1270
+ /**
1271
+ * SHA2-512 hash function from RFC 4634.
1272
+ * @param msg - message bytes to hash
1273
+ * @returns Digest bytes.
1274
+ * @example
1275
+ * Hash a message with SHA2-512.
1276
+ * ```ts
1277
+ * sha512(new Uint8Array([97, 98, 99]));
1278
+ * ```
1279
+ */
1280
+ const sha512 = /* @__PURE__ */ createHasher(() => new _SHA512(), /* @__PURE__ */ oidNist(3));
1281
+
1282
+ //#endregion
1283
+ //#region packages/resend/src/crypto.ts
1284
+ /**
1285
+ * Ed25519 key generation, persistence, and challenge signing for the Resend bridge.
1286
+ * Same shape as @abraca/mcp's crypto module; intentionally vendored so this package
1287
+ * doesn't depend on @abraca/mcp.
1288
+ */
1289
+ ed.hashes.sha512 = sha512;
1290
+ ed.hashes.sha512Async = (m) => Promise.resolve(sha512(m));
1291
+ const DEFAULT_KEY_PATH = join(homedir(), ".abracadabra", "resend.key");
1292
+ function toBase64url(bytes) {
1293
+ return Buffer.from(bytes).toString("base64url");
1294
+ }
1295
+ function fromBase64url(b64) {
1296
+ return new Uint8Array(Buffer.from(b64, "base64url"));
1297
+ }
1298
+ async function loadOrCreateKeypair(keyPath) {
1299
+ const path = keyPath || DEFAULT_KEY_PATH;
1300
+ if (existsSync(path)) {
1301
+ const seed = await readFile(path);
1302
+ if (seed.length !== 32) throw new Error(`Invalid key file at ${path}: expected 32 bytes, got ${seed.length}`);
1303
+ const privateKey = new Uint8Array(seed);
1304
+ return {
1305
+ privateKey,
1306
+ publicKeyB64: toBase64url(ed.getPublicKey(privateKey))
1307
+ };
1308
+ }
1309
+ const privateKey = ed.utils.randomSecretKey();
1310
+ const publicKey = ed.getPublicKey(privateKey);
1311
+ const dir = dirname(path);
1312
+ if (!existsSync(dir)) await mkdir(dir, {
1313
+ recursive: true,
1314
+ mode: 448
1315
+ });
1316
+ await writeFile(path, Buffer.from(privateKey), { mode: 384 });
1317
+ console.error(`[abracadabra-resend] Generated new agent keypair at ${path}`);
1318
+ console.error(`[abracadabra-resend] Public key: ${toBase64url(publicKey)}`);
1319
+ return {
1320
+ privateKey,
1321
+ publicKeyB64: toBase64url(publicKey)
1322
+ };
1323
+ }
1324
+ function signChallenge(challengeB64, privateKey) {
1325
+ const challenge = fromBase64url(challengeB64);
1326
+ return toBase64url(ed.sign(challenge, privateKey));
1327
+ }
1328
+
1329
+ //#endregion
1330
+ //#region packages/resend/src/server.ts
1331
+ const IDLE_TIMEOUT_MS = 300 * 1e3;
1332
+ var AbracadabraResendServer = class {
1333
+ constructor(config) {
1334
+ this._serverInfo = null;
1335
+ this._spaces = [];
1336
+ this._connection = null;
1337
+ this.childCache = /* @__PURE__ */ new Map();
1338
+ this.evictionTimer = null;
1339
+ this._userId = null;
1340
+ this._signFn = null;
1341
+ this._reconnecting = null;
1342
+ this.config = config;
1343
+ this.client = new AbracadabraClient({
1344
+ url: config.url,
1345
+ persistAuth: false
1346
+ });
1347
+ }
1348
+ get agentName() {
1349
+ return this.config.agentName || "Resend Bridge";
1350
+ }
1351
+ get serverInfo() {
1352
+ return this._serverInfo;
1353
+ }
1354
+ get spaceDocId() {
1355
+ return this._connection?.docId ?? null;
1356
+ }
1357
+ get rootDocument() {
1358
+ return this._connection?.doc ?? null;
1359
+ }
1360
+ get rootProvider() {
1361
+ return this._connection?.provider ?? null;
1362
+ }
1363
+ get userId() {
1364
+ return this._userId;
1365
+ }
1366
+ get spaces() {
1367
+ return this._spaces;
1368
+ }
1369
+ /** Authenticate, discover spaces, connect to the configured (or first) space. */
1370
+ async connect() {
1371
+ const keypair = await loadOrCreateKeypair(this.config.keyFile);
1372
+ this._userId = keypair.publicKeyB64;
1373
+ const signFn = (challenge) => Promise.resolve(signChallenge(challenge, keypair.privateKey));
1374
+ this._signFn = signFn;
1375
+ try {
1376
+ await this.client.loginWithKey(keypair.publicKeyB64, signFn);
1377
+ } catch (err) {
1378
+ const status = err?.status ?? err?.response?.status;
1379
+ const msg = String(err?.message ?? "").toLowerCase();
1380
+ if (!(status === 404 || status === 422 || status === 401 && /not registered|user not found|no such user/.test(msg))) throw err;
1381
+ console.error("[abracadabra-resend] Key not registered, creating new account...");
1382
+ await this.client.registerWithKey({
1383
+ publicKey: keypair.publicKeyB64,
1384
+ username: this.agentName.replace(/\s+/g, "-").toLowerCase(),
1385
+ displayName: this.agentName,
1386
+ deviceName: "Resend Bridge",
1387
+ inviteCode: this.config.inviteCode
1388
+ });
1389
+ await this.client.loginWithKey(keypair.publicKeyB64, signFn);
1390
+ }
1391
+ console.error(`[abracadabra-resend] Authenticated as ${this.agentName} (pubkey=${keypair.publicKeyB64})`);
1392
+ this._serverInfo = await this.client.serverInfo();
1393
+ const roots = await this.client.listChildren();
1394
+ this._spaces = roots.filter((d) => d.kind === Kind.Space);
1395
+ let targetId = this.config.spaceId;
1396
+ if (targetId) {
1397
+ if (!(this._spaces.find((s) => s.id === targetId) ?? roots.find((d) => d.id === targetId))) throw new Error(`Configured ABRA_SPACE_ID=${targetId} not found among server roots`);
1398
+ } else {
1399
+ targetId = this._spaces[0]?.id ?? roots[0]?.id;
1400
+ if (!targetId) throw new Error(`No entry point found: server has no top-level documents under ${SERVER_ROOT_ID}.`);
1401
+ }
1402
+ console.error(`[abracadabra-resend] Binding to space ${targetId}`);
1403
+ await this._connectToSpace(targetId);
1404
+ console.error("[abracadabra-resend] Space doc synced");
1405
+ this.evictionTimer = setInterval(() => this.evictIdle(), 6e4);
1406
+ }
1407
+ async _connectToSpace(docId) {
1408
+ if (!this.client.isTokenValid() && this._signFn && this._userId) {
1409
+ console.error("[abracadabra-resend] JWT expired, re-authenticating...");
1410
+ await this.client.loginWithKey(this._userId, this._signFn);
1411
+ }
1412
+ const doc = new Y.Doc({ guid: docId });
1413
+ const provider = new AbracadabraProvider({
1414
+ name: docId,
1415
+ document: doc,
1416
+ client: this.client,
1417
+ disableOfflineStore: true,
1418
+ subdocLoading: "lazy"
1419
+ });
1420
+ await waitForSync(provider);
1421
+ provider.awareness?.setLocalStateField("user", {
1422
+ name: this.agentName,
1423
+ color: "hsl(170, 70%, 45%)",
1424
+ publicKey: this._userId,
1425
+ isAgent: true
1426
+ });
1427
+ const conn = {
1428
+ doc,
1429
+ provider,
1430
+ docId
1431
+ };
1432
+ this._connection = conn;
1433
+ return conn;
1434
+ }
1435
+ _wsConnected(provider) {
1436
+ return provider.connectionStatus === WebSocketStatus.Connected;
1437
+ }
1438
+ /**
1439
+ * Heal a dropped socket / expired JWT before tool ops. De-duped across
1440
+ * concurrent callers; best-effort (never throws — a failed heal falls
1441
+ * through to the caller's normal error handling).
1442
+ */
1443
+ async ensureConnected() {
1444
+ if (this._reconnecting) return this._reconnecting;
1445
+ this._reconnecting = (async () => {
1446
+ try {
1447
+ if (!this.client.isTokenValid() && this._signFn && this._userId) try {
1448
+ await this.client.loginWithKey(this._userId, this._signFn);
1449
+ } catch (e) {
1450
+ console.error("[abracadabra-resend] Re-auth during heal failed:", e);
1451
+ }
1452
+ const conn = this._connection;
1453
+ if (!conn) return;
1454
+ if (this._wsConnected(conn.provider)) return;
1455
+ try {
1456
+ await waitForSync(conn.provider, 6e3);
1457
+ } catch {}
1458
+ if (this._wsConnected(conn.provider)) return;
1459
+ console.error("[abracadabra-resend] Active connection dead — rebuilding…");
1460
+ const docId = conn.docId;
1461
+ try {
1462
+ conn.provider.destroy();
1463
+ } catch {}
1464
+ for (const [, cached] of this.childCache) try {
1465
+ cached.provider.destroy();
1466
+ } catch {}
1467
+ this.childCache.clear();
1468
+ try {
1469
+ await this._connectToSpace(docId);
1470
+ console.error("[abracadabra-resend] Space provider rebuilt + synced");
1471
+ } catch (e) {
1472
+ console.error("[abracadabra-resend] Connection rebuild failed:", e);
1473
+ }
1474
+ } finally {
1475
+ this._reconnecting = null;
1476
+ }
1477
+ })();
1478
+ return this._reconnecting;
1479
+ }
1480
+ getTreeMap() {
1481
+ return this._connection?.doc.getMap("doc-tree") ?? null;
1482
+ }
1483
+ async getChildProvider(docId) {
1484
+ await this.ensureConnected();
1485
+ const cached = this.childCache.get(docId);
1486
+ if (cached && cached.provider.connectionStatus !== WebSocketStatus.Disconnected) {
1487
+ cached.lastAccessed = Date.now();
1488
+ return cached.provider;
1489
+ }
1490
+ if (cached) {
1491
+ try {
1492
+ cached.provider.destroy();
1493
+ } catch {}
1494
+ this.childCache.delete(docId);
1495
+ }
1496
+ const root = this._connection?.provider;
1497
+ if (!root) throw new Error("Not connected. Call connect() first.");
1498
+ if (!this.client.isTokenValid() && this._signFn && this._userId) await this.client.loginWithKey(this._userId, this._signFn);
1499
+ const childProvider = await root.loadChild(docId);
1500
+ await waitForSync(childProvider);
1501
+ this.childCache.set(docId, {
1502
+ provider: childProvider,
1503
+ lastAccessed: Date.now()
1504
+ });
1505
+ return childProvider;
1506
+ }
1507
+ evictIdle() {
1508
+ const now = Date.now();
1509
+ for (const [docId, cached] of this.childCache) if (now - cached.lastAccessed > IDLE_TIMEOUT_MS) {
1510
+ cached.provider.destroy();
1511
+ this.childCache.delete(docId);
1512
+ console.error(`[abracadabra-resend] Evicted idle provider: ${docId}`);
1513
+ }
1514
+ }
1515
+ async destroy() {
1516
+ if (this.evictionTimer) {
1517
+ clearInterval(this.evictionTimer);
1518
+ this.evictionTimer = null;
1519
+ }
1520
+ for (const [, cached] of this.childCache) try {
1521
+ cached.provider.destroy();
1522
+ } catch {}
1523
+ this.childCache.clear();
1524
+ if (this._connection) {
1525
+ try {
1526
+ this._connection.provider.destroy();
1527
+ } catch {}
1528
+ this._connection = null;
1529
+ }
1530
+ console.error("[abracadabra-resend] Shutdown complete");
1531
+ }
1532
+ };
1533
+
1534
+ //#endregion
1535
+ //#region packages/resend/src/index.ts
1536
+ /**
1537
+ * @abraca/resend — entry point.
1538
+ *
1539
+ * Environment variables:
1540
+ * ABRA_URL (required) — Abracadabra server URL
1541
+ * ABRA_SPACE_ID — Pin to a specific space (default: first visible)
1542
+ * ABRA_KEY_FILE — Ed25519 seed path (default ~/.abracadabra/resend.key)
1543
+ * ABRA_INVITE_CODE — Used only on first-run register
1544
+ * ABRA_AGENT_NAME — Display name (default "Resend Bridge")
1545
+ *
1546
+ * RESEND_API_KEY (required) — Resend API key
1547
+ * RESEND_FROM (required) — Default `from` address
1548
+ * RESEND_INBOUND_ENABLED — "false" to skip the webhook server (default true)
1549
+ * RESEND_INBOUND_PORT — Bind port for /inbound (default 0 = ephemeral)
1550
+ * RESEND_INBOUND_HOST — Bind host (default 0.0.0.0)
1551
+ * RESEND_INBOUND_SECRET — Required when inbound is enabled (Svix signing secret)
1552
+ */
1553
+ function required(name) {
1554
+ const value = process.env[name];
1555
+ if (!value || value.trim().length === 0) {
1556
+ console.error(`[abracadabra-resend] Missing required env var: ${name}`);
1557
+ process.exit(1);
1558
+ }
1559
+ return value;
1560
+ }
1561
+ function boolEnv(name, fallback) {
1562
+ const v = process.env[name];
1563
+ if (v === void 0) return fallback;
1564
+ const norm = v.trim().toLowerCase();
1565
+ if ([
1566
+ "0",
1567
+ "false",
1568
+ "no",
1569
+ "off",
1570
+ ""
1571
+ ].includes(norm)) return false;
1572
+ return true;
1573
+ }
1574
+ async function main() {
1575
+ const url = required("ABRA_URL");
1576
+ const resendApiKey = required("RESEND_API_KEY");
1577
+ const defaultFrom = required("RESEND_FROM");
1578
+ const inboundEnabled = boolEnv("RESEND_INBOUND_ENABLED", true);
1579
+ const inboundSecret = inboundEnabled ? required("RESEND_INBOUND_SECRET") : process.env.RESEND_INBOUND_SECRET ?? "";
1580
+ const inboundPort = process.env.RESEND_INBOUND_PORT ? Number(process.env.RESEND_INBOUND_PORT) : 0;
1581
+ const inboundHost = process.env.RESEND_INBOUND_HOST ?? "0.0.0.0";
1582
+ const server = new AbracadabraResendServer({
1583
+ url,
1584
+ spaceId: process.env.ABRA_SPACE_ID,
1585
+ keyFile: process.env.ABRA_KEY_FILE,
1586
+ inviteCode: process.env.ABRA_INVITE_CODE,
1587
+ agentName: process.env.ABRA_AGENT_NAME
1588
+ });
1589
+ try {
1590
+ await server.connect();
1591
+ } catch (err) {
1592
+ console.error(`[abracadabra-resend] Failed to connect: ${err?.message ?? err}`);
1593
+ process.exit(1);
1594
+ }
1595
+ const boot = await bootstrap(server);
1596
+ console.error(`[abracadabra-resend] Bootstrapped Inbox=${boot.inboxId} Outbox=${boot.outboxId}`);
1597
+ const watcher = new OutboxWatcher({
1598
+ server,
1599
+ sender: new ResendClient(resendApiKey),
1600
+ bootstrap: boot,
1601
+ defaultFrom
1602
+ });
1603
+ watcher.start();
1604
+ let inbound = null;
1605
+ if (inboundEnabled) {
1606
+ inbound = new InboundServer({
1607
+ server,
1608
+ bootstrap: boot,
1609
+ secret: inboundSecret,
1610
+ port: Number.isFinite(inboundPort) ? inboundPort : 0,
1611
+ host: inboundHost
1612
+ });
1613
+ try {
1614
+ await inbound.start();
1615
+ } catch (err) {
1616
+ console.error(`[abracadabra-resend] Inbound server failed to start: ${err?.message ?? err}`);
1617
+ process.exit(1);
1618
+ }
1619
+ } else console.error("[abracadabra-resend] Inbound disabled (RESEND_INBOUND_ENABLED=false)");
1620
+ console.error("[abracadabra-resend] Ready.");
1621
+ const shutdown = async () => {
1622
+ console.error("[abracadabra-resend] Shutting down...");
1623
+ watcher.stop();
1624
+ if (inbound) await inbound.stop();
1625
+ await server.destroy();
1626
+ process.exit(0);
1627
+ };
1628
+ process.on("SIGINT", shutdown);
1629
+ process.on("SIGTERM", shutdown);
1630
+ }
1631
+ main().catch((err) => {
1632
+ console.error("[abracadabra-resend] Fatal error:", err);
1633
+ process.exit(1);
1634
+ });
1635
+
1636
+ //#endregion
1637
+ export { AbracadabraResendServer, InboundServer, OutboxWatcher, RenderError, ResendClient, bootstrap, renderEmail };
1638
+ //# sourceMappingURL=abracadabra-resend.esm.js.map