@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.
- package/README.md +79 -0
- package/dist/abracadabra-resend.cjs +1674 -0
- package/dist/abracadabra-resend.cjs.map +1 -0
- package/dist/abracadabra-resend.esm.js +1638 -0
- package/dist/abracadabra-resend.esm.js.map +1 -0
- package/dist/index.d.ts +140 -0
- package/package.json +42 -0
- package/src/bootstrap.ts +182 -0
- package/src/crypto.ts +72 -0
- package/src/inbound-server.ts +413 -0
- package/src/index.ts +128 -0
- package/src/outbox-watcher.ts +283 -0
- package/src/render.ts +112 -0
- package/src/resend-client.ts +50 -0
- package/src/server.ts +322 -0
- package/src/utils.ts +32 -0
|
@@ -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
|