@dispatch-foo/protocol 0.0.0 → 0.2.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,628 @@
1
+ import { createCipheriv, createDecipheriv, createHash, createHmac, createPrivateKey, createPublicKey, diffieHellman, generateKeyPairSync, randomBytes, sign, verify } from "node:crypto";
2
+
3
+ //#region src/federation/hlc.ts
4
+ const MAX_HLC_COUNTER = 999999999;
5
+ const HLC = /^(\d{13})\.(\d{4,9})\.(.+)$/;
6
+ function parseOpHlc(hlc) {
7
+ const m = HLC.exec(hlc);
8
+ if (m === null) return null;
9
+ return {
10
+ ms: Number(m[1]),
11
+ counter: Number(m[2]),
12
+ replica: m[3] ?? ""
13
+ };
14
+ }
15
+ function compareHlc(a, b) {
16
+ if (a.ms !== b.ms) return a.ms < b.ms ? -1 : 1;
17
+ if (a.counter !== b.counter) return a.counter < b.counter ? -1 : 1;
18
+ return 0;
19
+ }
20
+ const MAX_CLOCK_LEAD_MS = 300 * 1e3;
21
+ /** Whether a reading's wall time is past the bound for the wall time `nowMs`. */
22
+ function aheadOfClock(hlc, nowMs) {
23
+ const clock = readClamped(hlc);
24
+ return clock !== null && clock.ms > nowMs + MAX_CLOCK_LEAD_MS;
25
+ }
26
+ function hlcWallMs(hlc) {
27
+ return parseOpHlc(hlc)?.ms ?? null;
28
+ }
29
+ function readClamped(hlc) {
30
+ const m = /^(\d{13})\.(\d{4,})\./.exec(hlc);
31
+ if (m === null) return null;
32
+ return {
33
+ ms: Number(m[1]),
34
+ counter: Math.min(Number(m[2]), MAX_HLC_COUNTER)
35
+ };
36
+ }
37
+ var OpClock = class {
38
+ ms;
39
+ counter;
40
+ constructor(replica, last = null, now = Date.now) {
41
+ this.replica = replica;
42
+ this.now = now;
43
+ const start = last === null ? null : readClamped(last);
44
+ this.ms = start?.ms ?? 0;
45
+ this.counter = start?.counter ?? 0;
46
+ }
47
+ /** A reading for a change made now, after every reading seen so far. */
48
+ tick() {
49
+ const wall = this.now();
50
+ if (wall > this.ms) {
51
+ this.ms = wall;
52
+ this.counter = 0;
53
+ } else if (this.counter < MAX_HLC_COUNTER) this.counter += 1;
54
+ else {
55
+ this.ms += 1;
56
+ this.counter = 0;
57
+ }
58
+ return this.last;
59
+ }
60
+ /** Moves past a reading from elsewhere, so the next tick sorts after it,
61
+ * unless it is past the bound (FW-R21); false when it refused. */
62
+ observe(remote) {
63
+ const clock = readClamped(remote);
64
+ if (clock === null) return true;
65
+ if (clock.ms > this.now() + MAX_CLOCK_LEAD_MS) return false;
66
+ if (clock.ms > this.ms || clock.ms === this.ms && clock.counter > this.counter) {
67
+ this.ms = clock.ms;
68
+ this.counter = clock.counter;
69
+ }
70
+ return true;
71
+ }
72
+ /** The latest reading, for persisting across restarts. */
73
+ get last() {
74
+ return `${String(this.ms).padStart(13, "0")}.${String(this.counter).padStart(4, "0")}.${this.replica}`;
75
+ }
76
+ };
77
+
78
+ //#endregion
79
+ //#region src/federation/jcs.ts
80
+ var CanonicalizeError = class extends TypeError {
81
+ name = "CanonicalizeError";
82
+ };
83
+ const MAX_JSON_DEPTH = 256;
84
+ const LONE_SURROGATE = /\p{Cs}/u;
85
+ function iJsonString(s) {
86
+ if (LONE_SURROGATE.test(s)) throw new CanonicalizeError("JCS refuses a lone surrogate");
87
+ return JSON.stringify(s);
88
+ }
89
+ function write(value, str, depth) {
90
+ if (value === null) return "null";
91
+ switch (typeof value) {
92
+ case "boolean": return value ? "true" : "false";
93
+ case "number":
94
+ if (!Number.isFinite(value)) throw new CanonicalizeError("JCS refuses non-finite numbers");
95
+ return JSON.stringify(value);
96
+ case "string": return str(value);
97
+ case "object": {
98
+ if (depth >= MAX_JSON_DEPTH) throw new CanonicalizeError(`JCS refuses nesting past ${MAX_JSON_DEPTH}`);
99
+ const inner = depth + 1;
100
+ if (Array.isArray(value)) return `[${value.map((v) => write(v === void 0 ? null : v, str, inner)).join(",")}]`;
101
+ const obj = value;
102
+ return `{${Object.keys(obj).filter((k) => obj[k] !== void 0).sort().map((k) => `${str(k)}:${write(obj[k], str, inner)}`).join(",")}}`;
103
+ }
104
+ default: throw new CanonicalizeError(`JCS cannot serialize a ${typeof value}`);
105
+ }
106
+ }
107
+ function canonicalize(value) {
108
+ return write(value, iJsonString, 0);
109
+ }
110
+ function canonicalizeLenient(value) {
111
+ return write(value, JSON.stringify, 0);
112
+ }
113
+
114
+ //#endregion
115
+ //#region src/federation/encoding.ts
116
+ const B64U = /^[A-Za-z0-9_-]*$/;
117
+ const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
118
+ function b64u(bytes) {
119
+ return Buffer.from(bytes).toString("base64url");
120
+ }
121
+ function fromB64u(text) {
122
+ if (!B64U.test(text)) throw new TypeError("not base64url");
123
+ const bytes = Buffer.from(text, "base64url");
124
+ if (b64u(bytes) !== text) throw new TypeError("not canonical base64url");
125
+ return bytes;
126
+ }
127
+ function sha256Hex(data) {
128
+ return createHash("sha256").update(data).digest("hex");
129
+ }
130
+ function crockford32(bytes) {
131
+ let out = "";
132
+ let buffer = 0;
133
+ let bits = 0;
134
+ for (const byte of bytes) {
135
+ buffer = (buffer << 8 | byte) & 65535;
136
+ bits += 8;
137
+ while (bits >= 5) {
138
+ out += CROCKFORD.charAt(buffer >>> bits - 5 & 31);
139
+ bits -= 5;
140
+ }
141
+ }
142
+ if (bits > 0) out += CROCKFORD.charAt(buffer << 5 - bits & 31);
143
+ return out;
144
+ }
145
+
146
+ //#endregion
147
+ //#region src/federation/keys.ts
148
+ const ED25519_PKCS8 = Buffer.from("302e020100300506032b657004220420", "hex");
149
+ const FIELD_PRIME$1 = 2n ** 255n - 19n;
150
+ const ORDER_8_Y = 2707385501144840649318225287225658788936804267575313519463743609750303402022n;
151
+ const SMALL_ORDER_Y = new Set([
152
+ 0n,
153
+ 1n,
154
+ FIELD_PRIME$1 - 1n,
155
+ ORDER_8_Y,
156
+ FIELD_PRIME$1 - ORDER_8_Y
157
+ ]);
158
+ function hasSmallOrder(raw) {
159
+ let y = 0n;
160
+ for (let i = raw.length - 1; i >= 0; i--) y = y << 8n | BigInt(raw[i] ?? 0);
161
+ return SMALL_ORDER_Y.has((y & (1n << 255n) - 1n) % FIELD_PRIME$1);
162
+ }
163
+ function rawPublic(key) {
164
+ const jwk = key.export({ format: "jwk" });
165
+ if (typeof jwk.x !== "string") throw new Error("expected an OKP public key");
166
+ return jwk.x;
167
+ }
168
+ function generateReplicaKeys() {
169
+ const s = generateKeyPairSync("ed25519");
170
+ const x = generateKeyPairSync("x25519");
171
+ return {
172
+ signPriv: s.privateKey.export({
173
+ type: "pkcs8",
174
+ format: "der"
175
+ }).toString("base64url"),
176
+ signPub: rawPublic(s.publicKey),
177
+ sealPriv: x.privateKey.export({
178
+ type: "pkcs8",
179
+ format: "der"
180
+ }).toString("base64url"),
181
+ sealPub: rawPublic(x.publicKey)
182
+ };
183
+ }
184
+ function ed25519FromSeed(seed) {
185
+ const der = Buffer.concat([ED25519_PKCS8, Buffer.from(seed)]);
186
+ const key = createPrivateKey({
187
+ key: der,
188
+ format: "der",
189
+ type: "pkcs8"
190
+ });
191
+ return {
192
+ signPriv: der.toString("base64url"),
193
+ signPub: rawPublic(createPublicKey(key))
194
+ };
195
+ }
196
+ function privateKeyOf(der) {
197
+ return createPrivateKey({
198
+ key: fromB64u(der),
199
+ format: "der",
200
+ type: "pkcs8"
201
+ });
202
+ }
203
+ function publicKeyOf(raw, crv) {
204
+ return createPublicKey({
205
+ key: {
206
+ kty: "OKP",
207
+ crv,
208
+ x: raw
209
+ },
210
+ format: "jwk"
211
+ });
212
+ }
213
+ function publicOfPrivate(der) {
214
+ return rawPublic(createPublicKey(privateKeyOf(der)));
215
+ }
216
+ function signText(signPriv, text) {
217
+ return b64u(sign(null, Buffer.from(text, "utf8"), privateKeyOf(signPriv)));
218
+ }
219
+ function verifyText(signPub, text, sig) {
220
+ try {
221
+ if (hasSmallOrder(fromB64u(signPub))) return false;
222
+ return verify(null, Buffer.from(text, "utf8"), publicKeyOf(signPub, "Ed25519"), fromB64u(sig));
223
+ } catch {
224
+ return false;
225
+ }
226
+ }
227
+
228
+ //#endregion
229
+ //#region src/federation/ops.ts
230
+ const OP_TYPES = [
231
+ "key",
232
+ "roster",
233
+ "task",
234
+ "presence",
235
+ "agent",
236
+ "channel",
237
+ "mail",
238
+ "state",
239
+ "memory",
240
+ "doc",
241
+ "a2a"
242
+ ];
243
+ const SEALED_TYPES = new Set([
244
+ "mail",
245
+ "state",
246
+ "a2a"
247
+ ]);
248
+ const STUBBABLE_TYPES = new Set([
249
+ "mail",
250
+ "state",
251
+ "presence",
252
+ "a2a"
253
+ ]);
254
+ const MAX_OP_BYTES = 1048576;
255
+ const MAX_SEALED_RECIPIENTS = 256;
256
+ const MAX_STATE_ENTRIES = 500;
257
+ const REPLICA_ID = /^[A-Za-z0-9._-]{1,32}-[0-9a-f]{8}$/;
258
+ const ZERO_HASH = "0".repeat(64);
259
+ const TAG = {
260
+ op: "dispatch-op-v2",
261
+ sealed: "dispatch-sealed-v1",
262
+ ack: "dispatch-ack-v1",
263
+ relay: "dispatch-relay-v1",
264
+ invite: "dispatch-invite-v1",
265
+ recovery: "dispatch-recovery-v1",
266
+ fingerprint: "dispatch-fp-v1"
267
+ };
268
+ function isStub(e) {
269
+ return "pruned" in e && e.pruned === true;
270
+ }
271
+ function headerOf(e) {
272
+ const h = {
273
+ v: 2,
274
+ replica: e.replica,
275
+ seq: e.seq,
276
+ prev: e.prev,
277
+ hlc: e.hlc,
278
+ type: e.type,
279
+ bodyHash: e.bodyHash
280
+ };
281
+ if (e.to !== void 0) h.to = e.to;
282
+ return h;
283
+ }
284
+ function contentHash(content) {
285
+ return sha256Hex(canonicalize(content));
286
+ }
287
+ function signingInput(h) {
288
+ return `${TAG.op}\n${canonicalize(h)}`;
289
+ }
290
+ function opHash(e) {
291
+ return sha256Hex(canonicalize({
292
+ ...headerOf(e),
293
+ sig: e.sig
294
+ }));
295
+ }
296
+ function stubOf(op) {
297
+ return {
298
+ ...headerOf(op),
299
+ sig: op.sig,
300
+ pruned: true
301
+ };
302
+ }
303
+
304
+ //#endregion
305
+ //#region src/federation/chain.ts
306
+ const TYPE = /^[a-z][a-z0-9-]{0,31}$/;
307
+ const HEX64 = /^[0-9a-f]{64}$/;
308
+ function buildOp(fields, signPriv) {
309
+ const problem = headerProblem(fields) ?? contentProblem(fields);
310
+ if (problem !== null) throw new RangeError(`cannot sign: ${problem}`);
311
+ const content = {};
312
+ if (fields.body !== void 0) content.body = fields.body;
313
+ if (fields.sealed !== void 0) content.sealed = fields.sealed;
314
+ const header = {
315
+ v: 2,
316
+ replica: fields.replica,
317
+ seq: fields.seq,
318
+ prev: fields.prev,
319
+ hlc: fields.hlc,
320
+ type: fields.type,
321
+ bodyHash: contentHash(content)
322
+ };
323
+ if (fields.to !== void 0) header.to = fields.to;
324
+ return {
325
+ ...header,
326
+ ...content,
327
+ sig: signText(signPriv, signingInput(header))
328
+ };
329
+ }
330
+ const fail = (reason) => ({
331
+ ok: false,
332
+ reason
333
+ });
334
+ function sortedUnique(list, max) {
335
+ if (!Array.isArray(list) || list.length === 0 || list.length > max) return false;
336
+ let last = "";
337
+ for (const r of list) {
338
+ if (typeof r !== "string" || !REPLICA_ID.test(r) || r <= last) return false;
339
+ last = r;
340
+ }
341
+ return true;
342
+ }
343
+ function sameKeys(sorted, keys) {
344
+ const other = [...keys].sort();
345
+ return sorted.length === other.length && sorted.every((r, i) => r === other[i]);
346
+ }
347
+ const isHex64 = (v) => typeof v === "string" && HEX64.test(v);
348
+ function headerProblem(h) {
349
+ if (typeof h.replica !== "string" || !REPLICA_ID.test(h.replica)) return "replica id outside the grammar";
350
+ if (typeof h.type !== "string" || !TYPE.test(h.type)) return "not a v2 op";
351
+ if (!Number.isSafeInteger(h.seq) || h.seq < 1) return "seq must rise";
352
+ if (!isHex64(h.prev)) return "malformed op";
353
+ const clock = typeof h.hlc === "string" ? parseOpHlc(h.hlc) : null;
354
+ if (clock === null) return "hlc outside the grammar";
355
+ if (clock.replica !== h.replica) return "hlc must name its own replica";
356
+ if (h.to !== void 0 && !sortedUnique(h.to, MAX_SEALED_RECIPIENTS)) return "bad recipient list";
357
+ return null;
358
+ }
359
+ function contentProblem(e) {
360
+ const sealedType = SEALED_TYPES.has(e.type);
361
+ if (sealedType && e.sealed === void 0) return "sealed types carry sealed content";
362
+ if (e.type === "state" && e.body !== void 0) return "state ops carry only sealed content";
363
+ if (!sealedType && (e.sealed !== void 0 || e.to !== void 0)) return "only mail and state are sealed";
364
+ if (sealedType && (e.to === void 0 || !sameKeys(e.to, Object.keys(e.sealed?.keys ?? {})))) return "keys must equal to";
365
+ return null;
366
+ }
367
+ function wellFormed(e) {
368
+ if (typeof e !== "object" || e === null || Array.isArray(e)) return false;
369
+ const { prev, bodyHash, sig } = e;
370
+ return typeof sig === "string" && isHex64(prev) && isHex64(bodyHash);
371
+ }
372
+ function orNull(step) {
373
+ try {
374
+ return step();
375
+ } catch {
376
+ return null;
377
+ }
378
+ }
379
+ function verifyEntry(head, e, signPub) {
380
+ if (!wellFormed(e)) return fail("malformed op");
381
+ if (e.v !== 2) return fail("not a v2 op");
382
+ const problem = headerProblem(e);
383
+ if (problem !== null) return fail(problem);
384
+ const bytes = orNull(() => Buffer.byteLength(canonicalize(e)));
385
+ if (bytes === null) return fail("malformed op");
386
+ if (bytes > MAX_OP_BYTES) return fail("over MAX_OP_BYTES");
387
+ if (head === null) {
388
+ if (isStub(e) || e.type !== "key" || e.prev !== ZERO_HASH) return fail("a log must start with its key op");
389
+ } else {
390
+ if (e.type === "key") return fail("a second key op");
391
+ if (e.prev !== head.hash) return fail("prev does not match");
392
+ if (e.seq <= head.seq) return fail("seq must rise");
393
+ const clock = parseOpHlc(e.hlc);
394
+ const before = parseOpHlc(head.hlc);
395
+ if (clock !== null && before !== null && compareHlc(clock, before) <= 0) return fail("hlc must rise");
396
+ }
397
+ if (!verifyText(signPub, signingInput(headerOf(e)), e.sig)) return fail("bad signature");
398
+ if (isStub(e)) {
399
+ if (!STUBBABLE_TYPES.has(e.type)) return fail(`a ${e.type} op cannot be a stub`);
400
+ if ("body" in e || "sealed" in e) return fail("a stub carries no content");
401
+ } else {
402
+ const content = {};
403
+ if (e.body !== void 0) content.body = e.body;
404
+ if (e.sealed !== void 0) content.sealed = e.sealed;
405
+ const hash = orNull(() => contentHash(content));
406
+ if (hash === null) return fail("malformed op");
407
+ if (hash !== e.bodyHash) return fail("bodyHash mismatch");
408
+ const broken = contentProblem(e);
409
+ if (broken !== null) return fail(broken);
410
+ }
411
+ return {
412
+ ok: true,
413
+ head: {
414
+ seq: e.seq,
415
+ hash: opHash(e),
416
+ hlc: e.hlc
417
+ }
418
+ };
419
+ }
420
+
421
+ //#endregion
422
+ //#region src/federation/fingerprint.ts
423
+ function fingerprint(signPub, sealPub) {
424
+ return (crockford32(createHash("sha256").update(`${TAG.fingerprint}\n${signPub}\n${sealPub}`).digest().subarray(0, 15)).match(/.{4}/g) ?? []).join("-");
425
+ }
426
+
427
+ //#endregion
428
+ //#region src/federation/hpke.ts
429
+ const i2osp2 = (n) => Buffer.from([n >> 8 & 255, n & 255]);
430
+ const KEM_SUITE = Buffer.concat([Buffer.from("KEM"), i2osp2(32)]);
431
+ const HPKE_SUITE = Buffer.concat([
432
+ Buffer.from("HPKE"),
433
+ i2osp2(32),
434
+ i2osp2(1),
435
+ i2osp2(2)
436
+ ]);
437
+ const V1 = Buffer.from("HPKE-v1");
438
+ const NH = 32;
439
+ const NK = 32;
440
+ const NN = 12;
441
+ const TAG_BYTES = 16;
442
+ const EMPTY = Buffer.alloc(0);
443
+ function extract(salt, ikm) {
444
+ return createHmac("sha256", salt.length === 0 ? Buffer.alloc(NH) : salt).update(ikm).digest();
445
+ }
446
+ function expand(prk, info, length) {
447
+ const blocks = [];
448
+ let t = EMPTY;
449
+ for (let i = 1; blocks.length * NH < length; i++) {
450
+ t = createHmac("sha256", prk).update(Buffer.concat([
451
+ t,
452
+ info,
453
+ Buffer.from([i])
454
+ ])).digest();
455
+ blocks.push(t);
456
+ }
457
+ return Buffer.concat(blocks).subarray(0, length);
458
+ }
459
+ function labeledExtract(suite, salt, label, ikm) {
460
+ return extract(salt, Buffer.concat([
461
+ V1,
462
+ suite,
463
+ Buffer.from(label),
464
+ ikm
465
+ ]));
466
+ }
467
+ function labeledExpand(suite, prk, label, info, length) {
468
+ return expand(prk, Buffer.concat([
469
+ i2osp2(length),
470
+ V1,
471
+ suite,
472
+ Buffer.from(label),
473
+ info
474
+ ]), length);
475
+ }
476
+ function x25519Public(raw) {
477
+ return publicKeyOf(raw.toString("base64url"), "X25519");
478
+ }
479
+ function freshEphemeral() {
480
+ const pair = generateKeyPairSync("x25519");
481
+ const { x } = pair.publicKey.export({ format: "jwk" });
482
+ if (typeof x !== "string") throw new Error("expected an OKP public key");
483
+ return {
484
+ privateKey: pair.privateKey,
485
+ publicRaw: fromB64u(x)
486
+ };
487
+ }
488
+ function sharedSecretOf(dh, enc, pkR) {
489
+ return labeledExpand(KEM_SUITE, labeledExtract(KEM_SUITE, EMPTY, "eae_prk", dh), "shared_secret", Buffer.concat([enc, pkR]), NH);
490
+ }
491
+ function encap(pkR, ephemeral = freshEphemeral()) {
492
+ return {
493
+ sharedSecret: sharedSecretOf(diffieHellman({
494
+ privateKey: ephemeral.privateKey,
495
+ publicKey: x25519Public(pkR)
496
+ }), ephemeral.publicRaw, pkR),
497
+ enc: ephemeral.publicRaw
498
+ };
499
+ }
500
+ function decap(enc, skR, pkR) {
501
+ return sharedSecretOf(diffieHellman({
502
+ privateKey: skR,
503
+ publicKey: x25519Public(enc)
504
+ }), enc, pkR);
505
+ }
506
+ function keySchedule(sharedSecret, info) {
507
+ const pskIdHash = labeledExtract(HPKE_SUITE, EMPTY, "psk_id_hash", EMPTY);
508
+ const infoHash = labeledExtract(HPKE_SUITE, EMPTY, "info_hash", info);
509
+ const context = Buffer.concat([
510
+ Buffer.from([0]),
511
+ pskIdHash,
512
+ infoHash
513
+ ]);
514
+ const secret = labeledExtract(HPKE_SUITE, sharedSecret, "secret", EMPTY);
515
+ return {
516
+ key: labeledExpand(HPKE_SUITE, secret, "key", context, NK),
517
+ baseNonce: labeledExpand(HPKE_SUITE, secret, "base_nonce", context, NN)
518
+ };
519
+ }
520
+ function aeadSeal(key, nonce, aad, pt) {
521
+ const c = createCipheriv("aes-256-gcm", key, nonce);
522
+ c.setAAD(aad);
523
+ return Buffer.concat([
524
+ c.update(pt),
525
+ c.final(),
526
+ c.getAuthTag()
527
+ ]);
528
+ }
529
+ function aeadOpen(key, nonce, aad, ct) {
530
+ if (ct.length < TAG_BYTES) throw new RangeError("ciphertext too short");
531
+ const d = createDecipheriv("aes-256-gcm", key, nonce);
532
+ d.setAAD(aad);
533
+ d.setAuthTag(ct.subarray(ct.length - TAG_BYTES));
534
+ return Buffer.concat([d.update(ct.subarray(0, ct.length - TAG_BYTES)), d.final()]);
535
+ }
536
+ function sealBase(pkR, info, pt) {
537
+ const { sharedSecret, enc } = encap(pkR);
538
+ const { key, baseNonce } = keySchedule(sharedSecret, info);
539
+ return {
540
+ enc,
541
+ ct: aeadSeal(key, baseNonce, EMPTY, pt)
542
+ };
543
+ }
544
+ function openBase(enc, skR, pkR, info, ct) {
545
+ const { key, baseNonce } = keySchedule(decap(enc, skR, pkR), info);
546
+ return aeadOpen(key, baseNonce, EMPTY, ct);
547
+ }
548
+
549
+ //#endregion
550
+ //#region src/federation/seal.ts
551
+ const CONTENT_KEY_BYTES = 32;
552
+ const NONCE_BYTES = 12;
553
+ const X25519_PUBLIC_BYTES = 32;
554
+ const FIELD_PRIME = 2n ** 255n - 19n;
555
+ function sealedAad(replica, seq, type) {
556
+ return `${TAG.sealed}\n${replica}\n${seq}\n${type}`;
557
+ }
558
+ function sealPayload(input) {
559
+ const to = [...input.recipients.keys()].sort();
560
+ if (to.length === 0 || to.length > MAX_SEALED_RECIPIENTS) throw new RangeError(`a sealed op names 1-${MAX_SEALED_RECIPIENTS} recipients`);
561
+ for (const r of to) {
562
+ if (!REPLICA_ID.test(r)) throw new RangeError(`${r} is not a replica id`);
563
+ if (!canSealTo(input.recipients.get(r) ?? "")) throw new RangeError(`cannot seal to ${r}: its sealPub is unusable`);
564
+ }
565
+ const key = randomBytes(CONTENT_KEY_BYTES);
566
+ const nonce = randomBytes(NONCE_BYTES);
567
+ const aad = sealedAad(input.replica, input.seq, input.type);
568
+ const ct = aeadSeal(key, nonce, Buffer.from(aad), Buffer.from(canonicalizeLenient(input.payload)));
569
+ const keys = {};
570
+ for (const r of to) keys[r] = wrapContentKey(key, aad, r, input.recipients.get(r) ?? "");
571
+ return {
572
+ to,
573
+ sealed: {
574
+ nonce: b64u(nonce),
575
+ ct: b64u(ct),
576
+ keys
577
+ },
578
+ key
579
+ };
580
+ }
581
+ function canSealTo(sealPub) {
582
+ try {
583
+ const raw = fromB64u(sealPub);
584
+ if (raw.length !== X25519_PUBLIC_BYTES) return false;
585
+ if (BigInt(`0x${Buffer.from(raw).reverse().toString("hex")}`) >= FIELD_PRIME) return false;
586
+ encap(raw);
587
+ return true;
588
+ } catch {
589
+ return false;
590
+ }
591
+ }
592
+ function wrapContentKey(key, aad, recipient, sealPub) {
593
+ const { enc, ct } = sealBase(fromB64u(sealPub), Buffer.from(`${aad}\n${recipient}`), key);
594
+ return {
595
+ enc: b64u(enc),
596
+ ct: b64u(ct)
597
+ };
598
+ }
599
+ function unwrapContentKey(op, me, sealPriv) {
600
+ const wrap = op.sealed?.keys[me];
601
+ if (wrap === void 0) return null;
602
+ try {
603
+ const skR = privateKeyOf(sealPriv);
604
+ const pkR = fromB64u(publicOfPrivate(sealPriv));
605
+ const aad = sealedAad(op.replica, op.seq, op.type);
606
+ return openBase(fromB64u(wrap.enc), skR, pkR, Buffer.from(`${aad}\n${me}`), fromB64u(wrap.ct));
607
+ } catch {
608
+ return null;
609
+ }
610
+ }
611
+ function openWithKey(op, key) {
612
+ if (op.sealed === void 0) return null;
613
+ try {
614
+ const aad = sealedAad(op.replica, op.seq, op.type);
615
+ const pt = aeadOpen(key, fromB64u(op.sealed.nonce), Buffer.from(aad), fromB64u(op.sealed.ct));
616
+ return JSON.parse(pt.toString("utf8"));
617
+ } catch {
618
+ return null;
619
+ }
620
+ }
621
+ function openPayload(op, me, sealPriv) {
622
+ const key = unwrapContentKey(op, me, sealPriv);
623
+ return key === null ? null : openWithKey(op, key);
624
+ }
625
+
626
+ //#endregion
627
+ export { CanonicalizeError, MAX_CLOCK_LEAD_MS, MAX_HLC_COUNTER, MAX_JSON_DEPTH, MAX_OP_BYTES, MAX_SEALED_RECIPIENTS, MAX_STATE_ENTRIES, OP_TYPES, OpClock, REPLICA_ID, SEALED_TYPES, STUBBABLE_TYPES, TAG, ZERO_HASH, aheadOfClock, b64u, buildOp, canSealTo, canonicalize, compareHlc, contentHash, crockford32, ed25519FromSeed, fingerprint, fromB64u, generateReplicaKeys, headerOf, hlcWallMs, isStub, opHash, openPayload, openWithKey, parseOpHlc, publicOfPrivate, sealPayload, sealedAad, sha256Hex, signText, stubOf, unwrapContentKey, verifyEntry, verifyText, wrapContentKey };
628
+ //# sourceMappingURL=index.js.map