@xyo-network/event-kit-node 0.0.1 → 0.1.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.
Files changed (30) hide show
  1. package/README.md +37 -3
  2. package/dist/node/filesystemDurability.d.ts +11 -0
  3. package/dist/node/filesystemDurability.d.ts.map +1 -0
  4. package/dist/node/filesystemEventKitReplayCheckpointStore.d.ts +8 -0
  5. package/dist/node/filesystemEventKitReplayCheckpointStore.d.ts.map +1 -0
  6. package/dist/node/filesystemFinalizedStatementPublicationJournal.d.ts +14 -0
  7. package/dist/node/filesystemFinalizedStatementPublicationJournal.d.ts.map +1 -0
  8. package/dist/node/filesystemHandshakeMutationIntentStore.d.ts +12 -0
  9. package/dist/node/filesystemHandshakeMutationIntentStore.d.ts.map +1 -0
  10. package/dist/node/filesystemJson.d.ts +14 -0
  11. package/dist/node/filesystemJson.d.ts.map +1 -0
  12. package/dist/node/filesystemPublisherEncryptionKeyStore.d.ts +15 -0
  13. package/dist/node/filesystemPublisherEncryptionKeyStore.d.ts.map +1 -0
  14. package/dist/node/filesystemPublisherEndpointChallengeStore.d.ts +12 -0
  15. package/dist/node/filesystemPublisherEndpointChallengeStore.d.ts.map +1 -0
  16. package/dist/node/filesystemWakeQueue.d.ts +2 -8
  17. package/dist/node/filesystemWakeQueue.d.ts.map +1 -1
  18. package/dist/node/handshake.d.ts +16 -0
  19. package/dist/node/handshake.d.ts.map +1 -0
  20. package/dist/node/handshake.mjs +1977 -0
  21. package/dist/node/handshake.mjs.map +7 -0
  22. package/dist/node/handshakeClients.d.ts +287 -0
  23. package/dist/node/handshakeClients.d.ts.map +1 -0
  24. package/dist/node/handshakeMutationIntent.d.ts +606 -0
  25. package/dist/node/handshakeMutationIntent.d.ts.map +1 -0
  26. package/dist/node/index.d.ts +1 -0
  27. package/dist/node/index.d.ts.map +1 -1
  28. package/dist/node/index.mjs +2015 -61
  29. package/dist/node/index.mjs.map +4 -4
  30. package/package.json +36 -18
@@ -40,46 +40,92 @@ import {
40
40
  rename,
41
41
  unlink as unlink2
42
42
  } from "node:fs/promises";
43
- import PATH3 from "node:path";
43
+ import PATH4 from "node:path";
44
44
 
45
45
  // src/filesystemWakeQueue.ts
46
46
  import { randomBytes } from "node:crypto";
47
47
  import {
48
48
  link,
49
- mkdir as mkdir2,
50
- open,
49
+ open as open2,
51
50
  readdir,
52
51
  unlink
53
52
  } from "node:fs/promises";
53
+ import PATH3 from "node:path";
54
+
55
+ // src/filesystemDurability.ts
56
+ import { mkdir as mkdir2, open } from "node:fs/promises";
54
57
  import PATH2 from "node:path";
58
+ async function writeDurableFile(path, body, mode) {
59
+ const handle = await open(path, "w", mode);
60
+ try {
61
+ await handle.writeFile(body, "utf8");
62
+ await handle.sync();
63
+ } finally {
64
+ await handle.close();
65
+ }
66
+ }
67
+ async function ensureDurableDirectory(path, mode) {
68
+ const directory = PATH2.resolve(path);
69
+ const firstCreated = await mkdir2(directory, { mode, recursive: true });
70
+ if (firstCreated === void 0) return;
71
+ const existingParent = PATH2.dirname(PATH2.resolve(firstCreated));
72
+ let cursor = directory;
73
+ while (true) {
74
+ await syncDirectory(cursor);
75
+ if (cursor === existingParent) return;
76
+ const parent = PATH2.dirname(cursor);
77
+ if (parent === cursor) throw new Error("Created directory is not below its reported existing parent");
78
+ cursor = parent;
79
+ }
80
+ }
81
+ async function syncDirectory(path) {
82
+ let handle;
83
+ try {
84
+ handle = await open(path, "r");
85
+ await handle.sync();
86
+ } catch (error) {
87
+ if (!isUnsupportedDirectorySync(error)) throw error;
88
+ } finally {
89
+ await handle?.close();
90
+ }
91
+ }
92
+ function isUnsupportedDirectorySync(error) {
93
+ const code = errorCode(error);
94
+ return code === "EISDIR" || code === "EINVAL" || code === "ENOTSUP" || code === "EOPNOTSUPP";
95
+ }
96
+ function errorCode(error) {
97
+ return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
98
+ }
99
+
100
+ // src/filesystemWakeQueue.ts
55
101
  function createFilesystemWakeQueue(options) {
56
- const root = PATH2.resolve(options.path);
57
- const pendingDir = PATH2.join(root, "pending");
58
- const inflightDir = PATH2.join(root, "inflight");
59
- const tempDir = PATH2.join(root, "tmp");
102
+ const root = PATH3.resolve(options.path);
103
+ const pendingDir = PATH3.join(root, "pending");
104
+ const inflightDir = PATH3.join(root, "inflight");
105
+ const tempDir = PATH3.join(root, "tmp");
60
106
  let sequence = 0;
61
107
  async function ensureDirs() {
62
- await mkdir2(pendingDir, { recursive: true });
63
- await mkdir2(inflightDir, { recursive: true });
64
- await mkdir2(tempDir, { recursive: true });
108
+ await ensureDurableDirectory(pendingDir);
109
+ await ensureDurableDirectory(inflightDir);
110
+ await ensureDurableDirectory(tempDir);
65
111
  }
66
112
  async function returnToPending(name) {
67
113
  try {
68
- await link(PATH2.join(inflightDir, name), PATH2.join(pendingDir, name));
114
+ await link(PATH3.join(inflightDir, name), PATH3.join(pendingDir, name));
69
115
  } catch (error) {
70
116
  if (!isAlreadyExists(error) && !isNotFound2(error)) throw error;
71
117
  }
72
- await unlink(PATH2.join(inflightDir, name)).catch(ignoreNotFound);
118
+ await unlink(PATH3.join(inflightDir, name)).catch(ignoreNotFound);
73
119
  }
74
120
  return {
75
121
  async append(wake) {
76
122
  await ensureDirs();
77
123
  const name = entryName(wake, sequence += 1);
78
- const tempPath = PATH2.join(tempDir, name);
124
+ const tempPath = PATH3.join(tempDir, name);
79
125
  await writeDurableFile(tempPath, `${JSON.stringify(wake)}
80
126
  `);
81
127
  try {
82
- await link(tempPath, PATH2.join(pendingDir, name));
128
+ await link(tempPath, PATH3.join(pendingDir, name));
83
129
  await syncDirectory(pendingDir);
84
130
  } finally {
85
131
  await unlink(tempPath).catch(ignoreNotFound);
@@ -89,8 +135,8 @@ function createFilesystemWakeQueue(options) {
89
135
  await ensureDirs();
90
136
  const names = (await readdir(pendingDir)).sort();
91
137
  for (const name of names) {
92
- const inflightPath = PATH2.join(inflightDir, name);
93
- const pendingPath = PATH2.join(pendingDir, name);
138
+ const inflightPath = PATH3.join(inflightDir, name);
139
+ const pendingPath = PATH3.join(pendingDir, name);
94
140
  try {
95
141
  await link(pendingPath, inflightPath);
96
142
  } catch (error) {
@@ -131,29 +177,9 @@ function createFilesystemWakeQueue(options) {
131
177
  }
132
178
  };
133
179
  }
134
- async function writeDurableFile(path, body) {
135
- const handle = await open(path, "w");
136
- try {
137
- await handle.writeFile(body, "utf8");
138
- await handle.sync();
139
- } finally {
140
- await handle.close();
141
- }
142
- }
143
- async function syncDirectory(path) {
144
- let handle;
145
- try {
146
- handle = await open(path, "r");
147
- await handle.sync();
148
- } catch {
149
- return;
150
- } finally {
151
- await handle?.close();
152
- }
153
- }
154
180
  async function readJsonFile(path) {
155
181
  try {
156
- const handle = await open(path, "r");
182
+ const handle = await open2(path, "r");
157
183
  try {
158
184
  const raw = await handle.readFile("utf8");
159
185
  return JSON.parse(raw);
@@ -173,18 +199,18 @@ function ignoreNotFound(error) {
173
199
  if (!isNotFound2(error)) throw error;
174
200
  }
175
201
  function isNotFound2(error) {
176
- return errorCode(error) === "ENOENT";
202
+ return errorCode2(error) === "ENOENT";
177
203
  }
178
204
  function isAlreadyExists(error) {
179
- return errorCode(error) === "EEXIST";
205
+ return errorCode2(error) === "EEXIST";
180
206
  }
181
- function errorCode(error) {
207
+ function errorCode2(error) {
182
208
  return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
183
209
  }
184
210
 
185
211
  // src/filesystemPublisherOutbox.ts
186
212
  function createFilesystemPublisherOutbox(options) {
187
- const filePath = PATH3.resolve(options.path);
213
+ const filePath = PATH4.resolve(options.path);
188
214
  return {
189
215
  async clear() {
190
216
  try {
@@ -197,7 +223,7 @@ function createFilesystemPublisherOutbox(options) {
197
223
  return await readJsonFile(filePath);
198
224
  },
199
225
  async save(pending) {
200
- await mkdir3(PATH3.dirname(filePath), { recursive: true });
226
+ await mkdir3(PATH4.dirname(filePath), { recursive: true });
201
227
  const tempPath = `${filePath}.${process.pid}.${randomBytes2(6).toString("hex")}.tmp`;
202
228
  await writeDurableFile(tempPath, `${JSON.stringify(pending)}
203
229
  `);
@@ -222,16 +248,16 @@ import { createHash, randomBytes as randomBytes3 } from "node:crypto";
222
248
  import {
223
249
  link as link2,
224
250
  mkdir as mkdir4,
225
- open as open2,
251
+ open as open3,
226
252
  readdir as readdir2,
227
253
  unlink as unlink3
228
254
  } from "node:fs/promises";
229
- import PATH4 from "node:path";
255
+ import PATH5 from "node:path";
230
256
  function createFilesystemWakeInbox(options) {
231
- const root = PATH4.resolve(options.path);
232
- const tokensDir = PATH4.join(root, "tokens");
233
- const wakesDir = PATH4.join(root, "wakes");
234
- const tempDir = PATH4.join(root, "tmp");
257
+ const root = PATH5.resolve(options.path);
258
+ const tokensDir = PATH5.join(root, "tokens");
259
+ const wakesDir = PATH5.join(root, "wakes");
260
+ const tempDir = PATH5.join(root, "tmp");
235
261
  let tail = Promise.resolve();
236
262
  function exclusive(work) {
237
263
  const run = tail.then(work, work);
@@ -244,10 +270,10 @@ function createFilesystemWakeInbox(options) {
244
270
  await mkdir4(tempDir, { recursive: true });
245
271
  }
246
272
  function tokenPath(attempt) {
247
- return PATH4.join(tokensDir, `${digest([attempt.audience, attempt.publisher, attempt.jwtId])}.json`);
273
+ return PATH5.join(tokensDir, `${digest([attempt.audience, attempt.publisher, attempt.jwtId])}.json`);
248
274
  }
249
275
  function wakePath(attempt) {
250
- return PATH4.join(wakesDir, `${digest([attempt.audience, attempt.publisher, attempt.wakeId])}.json`);
276
+ return PATH5.join(wakesDir, `${digest([attempt.audience, attempt.publisher, attempt.wakeId])}.json`);
251
277
  }
252
278
  async function bindToken(attempt, wake) {
253
279
  const record = {
@@ -311,7 +337,7 @@ function createFilesystemWakeInbox(options) {
311
337
  await ensureDirs();
312
338
  let removed = 0;
313
339
  for (const name of await readdir2(tokensDir)) {
314
- const path = PATH4.join(tokensDir, name);
340
+ const path = PATH5.join(tokensDir, name);
315
341
  const token = await readRecord(path);
316
342
  if (token === void 0 || token.retainJwtUntil > nowSeconds) continue;
317
343
  await unlink3(path).catch(ignoreNotFound2);
@@ -322,12 +348,12 @@ function createFilesystemWakeInbox(options) {
322
348
  };
323
349
  }
324
350
  async function writeRecordExclusive(tempDir, path, record) {
325
- const tempPath = PATH4.join(tempDir, `${PATH4.basename(path)}.${process.pid}.${randomSuffix()}`);
351
+ const tempPath = PATH5.join(tempDir, `${PATH5.basename(path)}.${process.pid}.${randomSuffix()}`);
326
352
  await writeDurableFile(tempPath, `${JSON.stringify(record)}
327
353
  `);
328
354
  try {
329
355
  await link2(tempPath, path);
330
- await syncDirectory(PATH4.dirname(path));
356
+ await syncDirectory(PATH5.dirname(path));
331
357
  return true;
332
358
  } catch (error) {
333
359
  if (isAlreadyExists2(error)) return false;
@@ -338,7 +364,7 @@ async function writeRecordExclusive(tempDir, path, record) {
338
364
  }
339
365
  async function readRecord(path) {
340
366
  try {
341
- const handle = await open2(path, "r");
367
+ const handle = await open3(path, "r");
342
368
  try {
343
369
  const raw = await handle.readFile("utf8");
344
370
  return JSON.parse(raw);
@@ -360,23 +386,1932 @@ function ignoreNotFound2(error) {
360
386
  if (!isNotFound4(error)) throw error;
361
387
  }
362
388
  function isNotFound4(error) {
363
- return errorCode2(error) === "ENOENT";
389
+ return errorCode3(error) === "ENOENT";
364
390
  }
365
391
  function isAlreadyExists2(error) {
366
- return errorCode2(error) === "EEXIST";
392
+ return errorCode3(error) === "EEXIST";
367
393
  }
368
- function errorCode2(error) {
394
+ function errorCode3(error) {
395
+ return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
396
+ }
397
+
398
+ // src/filesystemEventKitReplayCheckpointStore.ts
399
+ import PATH7 from "node:path";
400
+
401
+ // src/filesystemJson.ts
402
+ import { randomBytes as randomBytes4 } from "node:crypto";
403
+ import {
404
+ link as link3,
405
+ open as open4,
406
+ rename as rename2,
407
+ unlink as unlink4
408
+ } from "node:fs/promises";
409
+ import PATH6 from "node:path";
410
+ async function readJsonFileStrict(path) {
411
+ try {
412
+ const handle = await open4(path, "r");
413
+ try {
414
+ return JSON.parse(await handle.readFile("utf8"), reviveJson);
415
+ } finally {
416
+ await handle.close();
417
+ }
418
+ } catch (error) {
419
+ if (errorCode4(error) === "ENOENT") return void 0;
420
+ throw error;
421
+ }
422
+ }
423
+ async function replaceJsonFile(path, value, options = {}) {
424
+ const directory = PATH6.dirname(path);
425
+ await ensureDurableDirectory(directory, options.directoryMode);
426
+ const tempPath = PATH6.join(directory, `.${PATH6.basename(path)}.${process.pid}.${suffix()}.tmp`);
427
+ await writeDurableFile(tempPath, `${stringifyJson(value)}
428
+ `, options.mode);
429
+ try {
430
+ await rename2(tempPath, path);
431
+ await syncDirectory(directory);
432
+ } catch (error) {
433
+ await unlink4(tempPath).catch(ignoreNotFound3);
434
+ throw error;
435
+ }
436
+ }
437
+ async function publishJsonFile(path, value, options = {}) {
438
+ const directory = PATH6.dirname(path);
439
+ await ensureDurableDirectory(directory, options.directoryMode);
440
+ const tempPath = PATH6.join(directory, `.${PATH6.basename(path)}.${process.pid}.${suffix()}.tmp`);
441
+ await writeDurableFile(tempPath, `${stringifyJson(value)}
442
+ `, options.mode);
443
+ try {
444
+ await link3(tempPath, path);
445
+ await syncDirectory(directory);
446
+ return true;
447
+ } catch (error) {
448
+ if (errorCode4(error) === "EEXIST") return false;
449
+ throw error;
450
+ } finally {
451
+ await unlink4(tempPath).catch(ignoreNotFound3);
452
+ }
453
+ }
454
+ function suffix() {
455
+ return randomBytes4(8).toString("hex");
456
+ }
457
+ function stringifyJson(value) {
458
+ return JSON.stringify(value, replaceJsonValue);
459
+ }
460
+ function replaceJsonValue(_key, candidate) {
461
+ return typeof candidate === "bigint" ? { $eventKitBigInt: candidate.toString() } : candidate;
462
+ }
463
+ function reviveJson(_key, candidate) {
464
+ if (typeof candidate === "object" && candidate !== null && Object.keys(candidate).length === 1 && "$eventKitBigInt" in candidate && typeof candidate.$eventKitBigInt === "string" && /^(?:0|[1-9][0-9]*)$/u.test(candidate.$eventKitBigInt)) {
465
+ return BigInt(candidate.$eventKitBigInt);
466
+ }
467
+ return candidate;
468
+ }
469
+ function ignoreNotFound3(error) {
470
+ if (errorCode4(error) !== "ENOENT") throw error;
471
+ }
472
+ function errorCode4(error) {
369
473
  return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
370
474
  }
371
475
 
476
+ // src/filesystemEventKitReplayCheckpointStore.ts
477
+ function createFilesystemEventKitReplayCheckpointStore(options) {
478
+ const path = PATH7.resolve(options.path);
479
+ let tail = Promise.resolve();
480
+ return {
481
+ async load() {
482
+ await tail;
483
+ return await readJsonFileStrict(path);
484
+ },
485
+ async save(checkpoint) {
486
+ const save = tail.then(async () => await replaceJsonFile(path, checkpoint));
487
+ tail = save.catch(() => false);
488
+ await save;
489
+ }
490
+ };
491
+ }
492
+
493
+ // src/filesystemFinalizedStatementPublicationJournal.ts
494
+ import { createHash as createHash2 } from "node:crypto";
495
+ import PATH8 from "node:path";
496
+ import { isDeepStrictEqual } from "node:util";
497
+ var PublicationPhases = [
498
+ "signed",
499
+ "content-written",
500
+ "content-verified",
501
+ "broadcast",
502
+ "included",
503
+ "finalized"
504
+ ];
505
+ var PublicationPhaseSet = new Set(PublicationPhases);
506
+ function sameStrings(left, right) {
507
+ return left.length === right.length && left.every((value, index) => value === right[index]);
508
+ }
509
+ function assertIntentShape(intent) {
510
+ if (typeof intent.operationId !== "string" || intent.operationId.length === 0 || intent.operationId.length > 256) {
511
+ throw new TypeError("publication operationId must be a nonempty string of at most 256 characters");
512
+ }
513
+ if (!/^[0-9a-f]{40}$/u.test(intent.source)) {
514
+ throw new TypeError("publication source must be a canonical 20-byte lowercase hexadecimal address");
515
+ }
516
+ if (!/^[0-9a-f]{40}$/u.test(intent.chainId)) {
517
+ throw new TypeError("publication chainId must be a canonical 20-byte lowercase hexadecimal chain id");
518
+ }
519
+ if (![...intent.eventHashes, ...intent.attachmentHashes].every((hash) => /^[0-9a-f]{64}$/u.test(hash))) {
520
+ throw new TypeError("publication intent hashes must be 32-byte lowercase hexadecimal");
521
+ }
522
+ }
523
+ function sameIntent(left, right) {
524
+ return left.operationId === right.operationId && left.source === right.source && left.chainId === right.chainId && sameStrings(left.eventHashes, right.eventHashes) && sameStrings(left.attachmentHashes, right.attachmentHashes);
525
+ }
526
+ function intentFromRecord(record) {
527
+ return {
528
+ attachmentHashes: record.attachmentHashes,
529
+ chainId: record.chainId,
530
+ eventHashes: record.eventHashes,
531
+ operationId: record.operationId,
532
+ source: record.source
533
+ };
534
+ }
535
+ function publicationDirectory(root, intent) {
536
+ const key = createHash2("sha256").update(stringifyJson({
537
+ chainId: intent.chainId,
538
+ operationId: intent.operationId,
539
+ source: intent.source
540
+ })).digest("hex");
541
+ return PATH8.join(root, key);
542
+ }
543
+ function asPublicationRecord(value, path) {
544
+ if (typeof value !== "object" || value === null) throw new Error(`malformed publication journal record at ${path}`);
545
+ const record = value;
546
+ if (!PublicationPhaseSet.has(record.phase)) throw new Error(`unknown publication phase at ${path}`);
547
+ return record;
548
+ }
549
+ function assertBoundRecord(record, intent, signed) {
550
+ if (!sameIntent(intentFromRecord(record), intent)) throw new Error("publication journal intent collision");
551
+ if (!isDeepStrictEqual(record.evidence, signed.evidence)) {
552
+ throw new Error("publication journal evidence collision");
553
+ }
554
+ }
555
+ function createFilesystemFinalizedStatementPublicationJournal(options) {
556
+ const root = PATH8.resolve(options.path);
557
+ return {
558
+ async load(intent) {
559
+ assertIntentShape(intent);
560
+ const directory = publicationDirectory(root, intent);
561
+ const intentPath = PATH8.join(directory, "intent.json");
562
+ const rawSigned = await readJsonFileStrict(intentPath);
563
+ if (rawSigned === void 0) return void 0;
564
+ const signed = asPublicationRecord(rawSigned, intentPath);
565
+ if (signed.phase !== "signed") throw new Error(`publication intent record is not signed at ${intentPath}`);
566
+ assertBoundRecord(signed, intent, signed);
567
+ let latest = signed;
568
+ for (const phase of PublicationPhases.slice(1)) {
569
+ const path = PATH8.join(directory, `${phase}.json`);
570
+ const rawRecord = await readJsonFileStrict(path);
571
+ if (rawRecord === void 0) continue;
572
+ const record = asPublicationRecord(rawRecord, path);
573
+ if (record.phase !== phase) throw new Error(`publication phase/path mismatch at ${path}`);
574
+ assertBoundRecord(record, intent, signed);
575
+ latest = record;
576
+ }
577
+ return latest;
578
+ },
579
+ async save(record) {
580
+ const intent = intentFromRecord(record);
581
+ assertIntentShape(intent);
582
+ if (!/^[0-9a-f]{64}$/u.test(record.evidence.transactionHash)) {
583
+ throw new TypeError("publication transaction hash must be 32-byte lowercase hex");
584
+ }
585
+ if (!PublicationPhaseSet.has(record.phase)) throw new TypeError("unknown publication phase");
586
+ const directory = publicationDirectory(root, intent);
587
+ const intentPath = PATH8.join(directory, "intent.json");
588
+ if (record.phase === "signed") {
589
+ if (await publishJsonFile(intentPath, record)) return;
590
+ const existing2 = asPublicationRecord(await readJsonFileStrict(intentPath), intentPath);
591
+ if (!isDeepStrictEqual(existing2, record)) {
592
+ if (!sameIntent(intentFromRecord(existing2), intent)) throw new Error("publication journal intent collision");
593
+ throw new Error("publication journal evidence collision");
594
+ }
595
+ return;
596
+ }
597
+ const rawSigned = await readJsonFileStrict(intentPath);
598
+ if (rawSigned === void 0) throw new Error("publication journal must bind signed evidence first");
599
+ const signed = asPublicationRecord(rawSigned, intentPath);
600
+ if (signed.phase !== "signed") throw new Error(`publication intent record is not signed at ${intentPath}`);
601
+ assertBoundRecord(record, intent, signed);
602
+ const path = PATH8.join(directory, `${record.phase}.json`);
603
+ if (await publishJsonFile(path, record)) return;
604
+ const existing = asPublicationRecord(await readJsonFileStrict(path), path);
605
+ if (!isDeepStrictEqual(existing, record)) {
606
+ throw new Error(`publication journal collision at ${record.operationId}/${record.phase}`);
607
+ }
608
+ }
609
+ };
610
+ }
611
+
612
+ // src/filesystemHandshakeMutationIntentStore.ts
613
+ import { stat } from "node:fs/promises";
614
+ import PATH9 from "node:path";
615
+
616
+ // src/handshakeMutationIntent.ts
617
+ import {
618
+ canonicalJsonBytes,
619
+ handshakeObjectHash
620
+ } from "@xyo-network/event-kit-protocol";
621
+ import {
622
+ Base64Url32BytesZod,
623
+ ChainIdZod,
624
+ EncryptedWakeSubscriptionZod,
625
+ EndpointChallengeResponseZod,
626
+ EndpointChallengeZod,
627
+ HandshakeIdZod,
628
+ HashZod,
629
+ NormalizedAddressZod,
630
+ PublisherEncryptionKeyRevocationZod,
631
+ PublisherEncryptionKeyZod,
632
+ PublisherServiceRevocationZod,
633
+ PublisherServiceZod,
634
+ WakeGrantObjectZod,
635
+ WakeGrantRevocationZod,
636
+ WakeSubscriptionAcceptanceZod,
637
+ WakeSubscriptionRevocationZod
638
+ } from "@xyo-network/event-kit-schemas";
639
+ import { z } from "zod";
640
+ var HandshakeMutationIntentKinds = [
641
+ "publisher-encryption-key-retire",
642
+ "publisher-service-publish",
643
+ "publisher-service-retire",
644
+ "publisher-service-rotate-key",
645
+ "publisher-subscription-accept",
646
+ "subscriber-grant-revoke",
647
+ "subscriber-subscription-revoke",
648
+ "subscriber-subscription-subscribe"
649
+ ];
650
+ var HandshakeMutationIntentKindZod = z.enum(HandshakeMutationIntentKinds);
651
+ var HandshakeMutationOperationIdZod = z.union([HandshakeIdZod, HashZod]);
652
+ var WakeSubscriptionPrivateKeyZod = z.strictObject({
653
+ encryptionKey: HashZod,
654
+ keyId: HandshakeIdZod,
655
+ privateKey: Base64Url32BytesZod
656
+ });
657
+ var IntentBaseFields = {
658
+ chainId: ChainIdZod,
659
+ operationId: HandshakeMutationOperationIdZod,
660
+ publisher: NormalizedAddressZod,
661
+ requestFingerprint: HashZod,
662
+ source: NormalizedAddressZod,
663
+ version: z.literal(1)
664
+ };
665
+ var PublisherServicePublishIntentZod = z.strictObject({
666
+ ...IntentBaseFields,
667
+ kind: z.literal("publisher-service-publish"),
668
+ operationId: HandshakeIdZod,
669
+ encryptionKey: PublisherEncryptionKeyZod,
670
+ privateKey: WakeSubscriptionPrivateKeyZod,
671
+ service: PublisherServiceZod
672
+ });
673
+ var PublisherServiceRotateKeyIntentZod = z.strictObject({
674
+ ...IntentBaseFields,
675
+ kind: z.literal("publisher-service-rotate-key"),
676
+ operationId: HandshakeIdZod,
677
+ encryptionKey: PublisherEncryptionKeyZod,
678
+ privateKey: WakeSubscriptionPrivateKeyZod,
679
+ selectedKey: HashZod,
680
+ selectedService: HashZod,
681
+ service: PublisherServiceZod
682
+ });
683
+ var SubscriberSubscriptionIntentZod = z.strictObject({
684
+ ...IntentBaseFields,
685
+ kind: z.literal("subscriber-subscription-subscribe"),
686
+ operationId: HandshakeIdZod,
687
+ encryptionKey: PublisherEncryptionKeyZod,
688
+ grant: WakeGrantObjectZod,
689
+ selectedKey: HashZod,
690
+ selectedService: HashZod,
691
+ service: PublisherServiceZod,
692
+ subscription: EncryptedWakeSubscriptionZod
693
+ });
694
+ var PublisherAcceptanceIntentZod = z.strictObject({
695
+ ...IntentBaseFields,
696
+ acceptance: WakeSubscriptionAcceptanceZod,
697
+ challenge: EndpointChallengeZod,
698
+ kind: z.literal("publisher-subscription-accept"),
699
+ operationId: HashZod,
700
+ response: EndpointChallengeResponseZod
701
+ });
702
+ var PublisherServiceRetireIntentZod = z.strictObject({
703
+ ...IntentBaseFields,
704
+ kind: z.literal("publisher-service-retire"),
705
+ operationId: HashZod,
706
+ revocation: PublisherServiceRevocationZod
707
+ });
708
+ var PublisherEncryptionKeyRetireIntentZod = z.strictObject({
709
+ ...IntentBaseFields,
710
+ kind: z.literal("publisher-encryption-key-retire"),
711
+ operationId: HashZod,
712
+ revocation: PublisherEncryptionKeyRevocationZod
713
+ });
714
+ var SubscriberGrantRevokeIntentZod = z.strictObject({
715
+ ...IntentBaseFields,
716
+ kind: z.literal("subscriber-grant-revoke"),
717
+ operationId: HashZod,
718
+ revocation: WakeGrantRevocationZod
719
+ });
720
+ var SubscriberSubscriptionRevokeIntentZod = z.strictObject({
721
+ ...IntentBaseFields,
722
+ kind: z.literal("subscriber-subscription-revoke"),
723
+ operationId: HashZod,
724
+ revocation: WakeSubscriptionRevocationZod
725
+ });
726
+ var HandshakeMutationIntentZod = z.discriminatedUnion("kind", [
727
+ PublisherEncryptionKeyRetireIntentZod,
728
+ PublisherServicePublishIntentZod,
729
+ PublisherServiceRetireIntentZod,
730
+ PublisherServiceRotateKeyIntentZod,
731
+ PublisherAcceptanceIntentZod,
732
+ SubscriberGrantRevokeIntentZod,
733
+ SubscriberSubscriptionRevokeIntentZod,
734
+ SubscriberSubscriptionIntentZod
735
+ ]);
736
+ var HandshakeMutationIntentConflictError = class extends Error {
737
+ constructor(message = "Handshake mutation operation ID is already bound to different intent") {
738
+ super(message);
739
+ this.name = "HandshakeMutationIntentConflictError";
740
+ }
741
+ };
742
+ async function handshakeMutationRequestFingerprint(value) {
743
+ const canonical = canonicalJsonBytes(value);
744
+ const digestInput = new Uint8Array(canonical.byteLength);
745
+ digestInput.set(canonical);
746
+ const digest2 = await globalThis.crypto.subtle.digest("SHA-256", digestInput);
747
+ const hex = Array.from(new Uint8Array(digest2), (byte) => byte.toString(16).padStart(2, "0")).join("");
748
+ return HashZod.parse(hex);
749
+ }
750
+ function parseHandshakeMutationIntentLookup(value) {
751
+ return Object.freeze({
752
+ chainId: ChainIdZod.parse(value.chainId),
753
+ kind: HandshakeMutationIntentKindZod.parse(value.kind),
754
+ operationId: HandshakeMutationOperationIdZod.parse(value.operationId),
755
+ publisher: NormalizedAddressZod.parse(value.publisher),
756
+ source: NormalizedAddressZod.parse(value.source)
757
+ });
758
+ }
759
+ async function parseHandshakeMutationIntent(value) {
760
+ const intent = HandshakeMutationIntentZod.parse(value);
761
+ await assertIntentIntegrity(intent);
762
+ return Object.freeze(intent);
763
+ }
764
+ function createMemoryHandshakeMutationIntentStore() {
765
+ const intents = /* @__PURE__ */ new Map();
766
+ return {
767
+ async get(rawLookup) {
768
+ const lookup = parseHandshakeMutationIntentLookup(rawLookup);
769
+ return intents.get(intentLookupKey(lookup));
770
+ },
771
+ async getOrCreate(rawIntent) {
772
+ const proposed = await parseHandshakeMutationIntent(rawIntent);
773
+ const key = intentLookupKey(proposed);
774
+ const existing = intents.get(key);
775
+ if (existing === void 0) {
776
+ intents.set(key, proposed);
777
+ return Object.freeze({ disposition: "created", intent: proposed });
778
+ }
779
+ assertSameIntentBinding(existing, proposed);
780
+ return Object.freeze({ disposition: "resumed", intent: existing });
781
+ }
782
+ };
783
+ }
784
+ function intentLookupKey(lookup) {
785
+ const parsed = parseHandshakeMutationIntentLookup(lookup);
786
+ return [parsed.chainId, parsed.publisher, parsed.source, parsed.kind, parsed.operationId].join("/");
787
+ }
788
+ function assertSameIntentBinding(left, right) {
789
+ const shared = left.version === right.version && left.chainId === right.chainId && left.kind === right.kind && left.operationId === right.operationId && left.publisher === right.publisher && left.source === right.source && left.requestFingerprint === right.requestFingerprint;
790
+ const bothRotate = left.kind === "publisher-service-rotate-key" && right.kind === "publisher-service-rotate-key";
791
+ const bothSubscribe = left.kind === "subscriber-subscription-subscribe" && right.kind === "subscriber-subscription-subscribe";
792
+ const selected = bothRotate || bothSubscribe ? left.selectedService === right.selectedService && left.selectedKey === right.selectedKey : !("selectedService" in left) && !("selectedService" in right);
793
+ if (!shared || !selected) throw new HandshakeMutationIntentConflictError();
794
+ }
795
+ async function assertIntentIntegrity(intent) {
796
+ if (intent.kind.startsWith("publisher-") && intent.source !== intent.publisher) {
797
+ throw new TypeError("Publisher mutation intent source must equal publisher");
798
+ }
799
+ if (intent.kind === "publisher-service-publish" || intent.kind === "publisher-service-rotate-key") {
800
+ await assertPublisherServiceIntent(intent);
801
+ return;
802
+ }
803
+ if (intent.kind === "subscriber-subscription-subscribe") {
804
+ await assertSubscriberSubscriptionIntent(intent);
805
+ return;
806
+ }
807
+ if (intent.kind === "publisher-subscription-accept") {
808
+ await assertPublisherAcceptanceIntent(intent);
809
+ return;
810
+ }
811
+ await assertRevocationIntent(intent);
812
+ }
813
+ async function assertPublisherServiceIntent(intent) {
814
+ const keyHash = await handshakeObjectHash(intent.encryptionKey);
815
+ if (intent.privateKey.encryptionKey !== keyHash || intent.privateKey.keyId !== intent.encryptionKey.keyId || intent.service.preferredEncryptionKey !== keyHash || intent.service.encryptionKeys.length !== 1 || intent.service.encryptionKeys[0] !== keyHash) {
816
+ throw new TypeError("Publisher service mutation intent has inconsistent encryption-key artifacts");
817
+ }
818
+ if (intent.kind === "publisher-service-publish") {
819
+ if (intent.service.revision !== 1 || intent.service.supersedes !== void 0) {
820
+ throw new TypeError("Publisher service root intent must contain a root service revision");
821
+ }
822
+ } else if (intent.service.supersedes !== intent.selectedService || intent.service.revision <= 1) {
823
+ throw new TypeError("Publisher service rotation intent does not extend its selected service");
824
+ }
825
+ }
826
+ async function assertSubscriberSubscriptionIntent(intent) {
827
+ const [keyHash, serviceHash, grantHash] = await Promise.all([
828
+ handshakeObjectHash(intent.encryptionKey),
829
+ handshakeObjectHash(intent.service),
830
+ handshakeObjectHash(intent.grant)
831
+ ]);
832
+ if (keyHash !== intent.selectedKey || serviceHash !== intent.selectedService || intent.service.preferredEncryptionKey !== keyHash || !intent.service.encryptionKeys.includes(keyHash) || intent.grant.publisher !== intent.publisher || intent.subscription.publisher !== intent.publisher || intent.subscription.service !== serviceHash || intent.subscription.encryptionKey !== keyHash || intent.subscription.keyId !== intent.encryptionKey.keyId || intent.subscription.grant !== grantHash) {
833
+ throw new TypeError("Subscriber subscription intent has inconsistent public artifacts");
834
+ }
835
+ }
836
+ async function assertPublisherAcceptanceIntent(intent) {
837
+ const [challengeHash, responseHash] = await Promise.all([
838
+ handshakeObjectHash(intent.challenge),
839
+ handshakeObjectHash(intent.response)
840
+ ]);
841
+ if (challengeHash !== intent.operationId || intent.challenge.publisher !== intent.publisher || intent.response.challenge !== challengeHash || intent.acceptance.challenge !== challengeHash || intent.acceptance.response !== responseHash || intent.acceptance.service !== intent.challenge.service || intent.acceptance.encryptionKey !== intent.challenge.encryptionKey || intent.acceptance.grant !== intent.challenge.grant || intent.acceptance.subscription !== intent.challenge.subscription) {
842
+ throw new TypeError("Publisher acceptance intent has inconsistent public artifacts");
843
+ }
844
+ }
845
+ async function assertRevocationIntent(intent) {
846
+ const target = intent.kind === "publisher-encryption-key-retire" ? intent.revocation.key : intent.kind === "publisher-service-retire" ? intent.revocation.service : intent.kind === "subscriber-grant-revoke" ? intent.revocation.grant : intent.revocation.subscription;
847
+ if (target !== intent.operationId) {
848
+ throw new TypeError("Handshake revocation intent has inconsistent target binding");
849
+ }
850
+ }
851
+
852
+ // src/filesystemHandshakeMutationIntentStore.ts
853
+ function createFilesystemHandshakeMutationIntentStore(options) {
854
+ const root = PATH9.resolve(options.path);
855
+ function pathFor(rawLookup) {
856
+ const lookup = parseHandshakeMutationIntentLookup(rawLookup);
857
+ return PATH9.join(
858
+ root,
859
+ lookup.chainId,
860
+ lookup.publisher,
861
+ lookup.source,
862
+ lookup.kind,
863
+ `${lookup.operationId}.json`
864
+ );
865
+ }
866
+ async function readIntent(lookup) {
867
+ const path = pathFor(lookup);
868
+ const raw = await readJsonFileStrict(path);
869
+ if (raw === void 0) return void 0;
870
+ const intent = await parseHandshakeMutationIntent(raw);
871
+ const parsedLookup = parseHandshakeMutationIntentLookup(lookup);
872
+ if (intent.chainId !== parsedLookup.chainId || intent.kind !== parsedLookup.kind || intent.operationId !== parsedLookup.operationId || intent.publisher !== parsedLookup.publisher || intent.source !== parsedLookup.source) {
873
+ throw new Error("handshake mutation intent does not match its immutable path");
874
+ }
875
+ const metadata = await stat(path);
876
+ if ((metadata.mode & 63) !== 0) throw new Error("handshake mutation intent permissions are too broad");
877
+ return intent;
878
+ }
879
+ return {
880
+ async get(lookup) {
881
+ return await readIntent(parseHandshakeMutationIntentLookup(lookup));
882
+ },
883
+ async getOrCreate(rawIntent) {
884
+ const proposed = await parseHandshakeMutationIntent(rawIntent);
885
+ const path = pathFor(proposed);
886
+ const created = await publishJsonFile(path, proposed, { directoryMode: 448, mode: 384 });
887
+ const winner = await readIntent(proposed);
888
+ if (winner === void 0) throw new Error("handshake mutation intent winner vanished");
889
+ assertSameIntentBinding(winner, proposed);
890
+ return Object.freeze({
891
+ disposition: created ? "created" : "resumed",
892
+ intent: winner
893
+ });
894
+ }
895
+ };
896
+ }
897
+
898
+ // src/filesystemPublisherEncryptionKeyStore.ts
899
+ import { stat as stat2 } from "node:fs/promises";
900
+ import PATH10 from "node:path";
901
+ import {
902
+ Base64Url32BytesZod as Base64Url32BytesZod2,
903
+ HandshakeIdZod as HandshakeIdZod2,
904
+ HashZod as HashZod2,
905
+ NormalizedAddressZod as NormalizedAddressZod2
906
+ } from "@xyo-network/event-kit-schemas";
907
+ function createFilesystemPublisherEncryptionKeyStore(options) {
908
+ const root = PATH10.resolve(options.path);
909
+ function parseLookup(lookup) {
910
+ return {
911
+ encryptionKey: HashZod2.parse(lookup.encryptionKey),
912
+ keyId: HandshakeIdZod2.parse(lookup.keyId),
913
+ publisher: NormalizedAddressZod2.parse(lookup.publisher)
914
+ };
915
+ }
916
+ function pathFor(lookup) {
917
+ return PATH10.join(root, lookup.publisher, `${lookup.encryptionKey}-${lookup.keyId}.json`);
918
+ }
919
+ function keyIdPath(lookup) {
920
+ return PATH10.join(root, lookup.publisher, "key-ids", `${lookup.keyId}.json`);
921
+ }
922
+ async function readRecord2(lookup) {
923
+ const path = pathFor(lookup);
924
+ const raw = await readJsonFileStrict(path);
925
+ if (raw === void 0) return void 0;
926
+ const parsed = parsePrivateKeyRecord(raw);
927
+ if (parsed.publisher !== lookup.publisher || parsed.encryptionKey !== lookup.encryptionKey || parsed.keyId !== lookup.keyId) throw new Error("publisher encryption-key record does not match its immutable path");
928
+ const keyIdRecord = parseKeyIdRecord(await readJsonFileStrict(keyIdPath(lookup)));
929
+ if (keyIdRecord.publisher !== lookup.publisher || keyIdRecord.encryptionKey !== lookup.encryptionKey || keyIdRecord.keyId !== lookup.keyId) throw new Error("publisher encryption-key keyId index does not match its immutable path");
930
+ const metadata = await stat2(path);
931
+ if ((metadata.mode & 63) !== 0) throw new Error("publisher encryption-key record permissions are too broad");
932
+ return parsed;
933
+ }
934
+ return {
935
+ async getPrivateKey(rawLookup) {
936
+ const lookup = parseLookup(rawLookup);
937
+ const record = await readRecord2(lookup);
938
+ if (record === void 0) return void 0;
939
+ return Object.freeze({
940
+ encryptionKey: record.encryptionKey,
941
+ keyId: record.keyId,
942
+ privateKey: record.privateKey
943
+ });
944
+ },
945
+ async putPrivateKey(input) {
946
+ const record = parsePrivateKeyRecord({ ...input.key, publisher: input.publisher });
947
+ const lookup = parseLookup(record);
948
+ const path = pathFor(lookup);
949
+ const keyIdRecord = {
950
+ encryptionKey: record.encryptionKey,
951
+ keyId: record.keyId,
952
+ publisher: record.publisher
953
+ };
954
+ if (!await publishJsonFile(
955
+ keyIdPath(lookup),
956
+ keyIdRecord,
957
+ { directoryMode: 448, mode: 384 }
958
+ )) {
959
+ const existingKeyId = parseKeyIdRecord(await readJsonFileStrict(keyIdPath(lookup)));
960
+ if (stringifyJson(existingKeyId) !== stringifyJson(keyIdRecord)) {
961
+ throw new Error(`publisher encryption-key keyId collision for ${record.keyId}`);
962
+ }
963
+ }
964
+ if (await publishJsonFile(path, record, { directoryMode: 448, mode: 384 })) return "stored";
965
+ const existing = await readRecord2(lookup);
966
+ if (stringifyJson(existing) !== stringifyJson(record)) {
967
+ throw new Error(`publisher encryption-key collision for ${record.encryptionKey}/${record.keyId}`);
968
+ }
969
+ return "duplicate";
970
+ }
971
+ };
972
+ }
973
+ function parseKeyIdRecord(value) {
974
+ if (typeof value !== "object" || value === null) throw new TypeError("invalid publisher encryption-key keyId record");
975
+ const record = value;
976
+ const keys = Object.keys(record).toSorted();
977
+ if (keys.join("|") !== "encryptionKey|keyId|publisher") {
978
+ throw new TypeError("invalid publisher encryption-key keyId record shape");
979
+ }
980
+ return {
981
+ encryptionKey: HashZod2.parse(record.encryptionKey),
982
+ keyId: HandshakeIdZod2.parse(record.keyId),
983
+ publisher: NormalizedAddressZod2.parse(record.publisher)
984
+ };
985
+ }
986
+ function parsePrivateKeyRecord(value) {
987
+ if (typeof value !== "object" || value === null) throw new TypeError("invalid publisher encryption-key record");
988
+ const record = value;
989
+ const keys = Object.keys(record).toSorted();
990
+ if (keys.join("|") !== "encryptionKey|keyId|privateKey|publisher") {
991
+ throw new TypeError("invalid publisher encryption-key record shape");
992
+ }
993
+ return {
994
+ encryptionKey: HashZod2.parse(record.encryptionKey),
995
+ keyId: HandshakeIdZod2.parse(record.keyId),
996
+ privateKey: Base64Url32BytesZod2.parse(record.privateKey),
997
+ publisher: NormalizedAddressZod2.parse(record.publisher)
998
+ };
999
+ }
1000
+
1001
+ // src/filesystemPublisherEndpointChallengeStore.ts
1002
+ import PATH11 from "node:path";
1003
+ import { endpointChallengeHash } from "@xyo-network/event-kit-protocol";
1004
+ import {
1005
+ ChallengeNonceZod,
1006
+ EndpointChallengeZod as EndpointChallengeZod2,
1007
+ HashZod as HashZod3
1008
+ } from "@xyo-network/event-kit-schemas";
1009
+ function createFilesystemPublisherEndpointChallengeStore(options) {
1010
+ const root = PATH11.resolve(options.path);
1011
+ function statePath(challengeHash, state) {
1012
+ return PATH11.join(root, challengeHash, `${state}.json`);
1013
+ }
1014
+ function noncePath(nonce) {
1015
+ return PATH11.join(root, "nonces", `${nonce}.json`);
1016
+ }
1017
+ async function readState(challengeHash, state) {
1018
+ const raw = await readJsonFileStrict(statePath(challengeHash, state));
1019
+ if (raw === void 0) return void 0;
1020
+ const record = await parseChallengeRecord(raw, state);
1021
+ if (record.challengeHash !== challengeHash) throw new Error("challenge record does not match its immutable path");
1022
+ return record;
1023
+ }
1024
+ async function current(challengeHash) {
1025
+ return await readState(challengeHash, "verified") ?? await readState(challengeHash, "attempted") ?? await readState(challengeHash, "issued");
1026
+ }
1027
+ async function publish(record) {
1028
+ return await publishJsonFile(
1029
+ statePath(record.challengeHash, record.state),
1030
+ record,
1031
+ { directoryMode: 448, mode: 384 }
1032
+ );
1033
+ }
1034
+ return {
1035
+ async beginAttempt(input) {
1036
+ const challengeHash = HashZod3.parse(input.challengeHash);
1037
+ requireTime(input.nowSeconds);
1038
+ const observed = await current(challengeHash);
1039
+ if (observed === void 0) return { kind: "missing" };
1040
+ if (observed.state === "verified") return { kind: "verified", record: observed };
1041
+ if (observed.state === "attempted") return { kind: "already-attempted", record: observed };
1042
+ if (!isLive(observed, input.nowSeconds)) return { kind: "expired", record: observed };
1043
+ const attempted = { ...observed, state: "attempted" };
1044
+ if (await publish(attempted)) return { kind: "started", record: attempted };
1045
+ const winner = await current(challengeHash);
1046
+ if (winner === void 0) throw new Error("challenge attempt winner vanished");
1047
+ return winner.state === "verified" ? { kind: "verified", record: winner } : { kind: "already-attempted", record: winner };
1048
+ },
1049
+ async completeVerified(input) {
1050
+ const challengeHash = HashZod3.parse(input.challengeHash);
1051
+ const responseHash = HashZod3.parse(input.responseHash);
1052
+ requireTime(input.nowSeconds);
1053
+ const observed = await current(challengeHash);
1054
+ if (observed === void 0) return { kind: "missing" };
1055
+ if (observed.state === "verified") {
1056
+ return observed.responseHash === responseHash ? { kind: "duplicate", record: observed } : { kind: "collision", record: observed };
1057
+ }
1058
+ if (observed.state !== "attempted") return { kind: "not-attempted", record: observed };
1059
+ if (!isLive(observed, input.nowSeconds)) return { kind: "expired", record: observed };
1060
+ const verified = {
1061
+ ...observed,
1062
+ responseHash,
1063
+ state: "verified"
1064
+ };
1065
+ if (await publish(verified)) return { kind: "verified", record: verified };
1066
+ const winner = await readState(challengeHash, "verified");
1067
+ if (winner === void 0) throw new Error("verified challenge winner vanished");
1068
+ return winner.responseHash === responseHash ? { kind: "duplicate", record: winner } : { kind: "collision", record: winner };
1069
+ },
1070
+ async get(rawHash) {
1071
+ return await current(HashZod3.parse(rawHash));
1072
+ },
1073
+ async issue(input) {
1074
+ const challenge = EndpointChallengeZod2.parse(input.challenge);
1075
+ const challengeHash = HashZod3.parse(input.challengeHash);
1076
+ if (await endpointChallengeHash(challenge) !== challengeHash) return { kind: "collision" };
1077
+ const nonceRecord = { challengeHash, nonce: challenge.nonce };
1078
+ if (!await publishJsonFile(
1079
+ noncePath(challenge.nonce),
1080
+ nonceRecord,
1081
+ { directoryMode: 448, mode: 384 }
1082
+ )) {
1083
+ const existingNonce = parseNonceRecord(await readJsonFileStrict(noncePath(challenge.nonce)));
1084
+ if (existingNonce.nonce !== challenge.nonce || existingNonce.challengeHash !== challengeHash) {
1085
+ return { kind: "collision" };
1086
+ }
1087
+ }
1088
+ const issued = {
1089
+ challenge,
1090
+ challengeHash,
1091
+ state: "issued"
1092
+ };
1093
+ if (await publish(issued)) return { kind: "issued", record: issued };
1094
+ const existing = await current(challengeHash);
1095
+ if (existing === void 0) throw new Error("issued challenge winner vanished");
1096
+ return sameChallenge(existing, issued) ? { kind: "duplicate", record: existing } : { kind: "collision" };
1097
+ }
1098
+ };
1099
+ }
1100
+ function parseNonceRecord(value) {
1101
+ if (typeof value !== "object" || value === null) throw new TypeError("invalid endpoint-challenge nonce record");
1102
+ const record = value;
1103
+ const keys = Object.keys(record).toSorted();
1104
+ if (keys.join("|") !== "challengeHash|nonce") throw new TypeError("invalid endpoint-challenge nonce record shape");
1105
+ return {
1106
+ challengeHash: HashZod3.parse(record.challengeHash),
1107
+ nonce: ChallengeNonceZod.parse(record.nonce)
1108
+ };
1109
+ }
1110
+ async function parseChallengeRecord(value, expectedState) {
1111
+ if (typeof value !== "object" || value === null) throw new TypeError("invalid endpoint-challenge record");
1112
+ const record = value;
1113
+ const state = record.state;
1114
+ if (state !== expectedState) throw new Error("endpoint-challenge record state does not match its path");
1115
+ const challenge = EndpointChallengeZod2.parse(record.challenge);
1116
+ const challengeHash = HashZod3.parse(record.challengeHash);
1117
+ if (await endpointChallengeHash(challenge) !== challengeHash) throw new Error("endpoint-challenge record hash mismatch");
1118
+ if (expectedState === "verified") {
1119
+ return {
1120
+ challenge,
1121
+ challengeHash,
1122
+ responseHash: HashZod3.parse(record.responseHash),
1123
+ state: expectedState
1124
+ };
1125
+ }
1126
+ if (record.responseHash !== void 0) throw new TypeError("unverified challenge record contains a response hash");
1127
+ return {
1128
+ challenge,
1129
+ challengeHash,
1130
+ state: expectedState
1131
+ };
1132
+ }
1133
+ function sameChallenge(left, right) {
1134
+ return left.challengeHash === right.challengeHash && stringifyJson(left.challenge) === stringifyJson(right.challenge);
1135
+ }
1136
+ function isLive(record, nowSeconds) {
1137
+ return record.challenge.issuedAt <= nowSeconds && nowSeconds < record.challenge.expiresAt;
1138
+ }
1139
+ function requireTime(value) {
1140
+ if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("nowSeconds must be a nonnegative safe integer");
1141
+ }
1142
+
1143
+ // src/handshakeClients.ts
1144
+ import {
1145
+ beginPublisherEndpointChallengeAttempt,
1146
+ completePublisherEndpointChallenge,
1147
+ createEncryptedWakeSubscription,
1148
+ createPublisherEncryptionKeyRevocation,
1149
+ createPublisherService,
1150
+ createPublisherServiceRevocation,
1151
+ createWakeGrant,
1152
+ createWakeGrantRevocation,
1153
+ createWakeSubscriptionPlaintext,
1154
+ createWakeSubscriptionRevocation,
1155
+ endpointChallengeMatchesPublisherContext,
1156
+ generatePublisherEncryptionKey,
1157
+ handshakeObjectHash as handshakeObjectHash2,
1158
+ issuePublisherEndpointChallenge,
1159
+ validateAndDecryptPublisherSubscription,
1160
+ withHandshakeObjectHash
1161
+ } from "@xyo-network/event-kit-protocol";
1162
+ import {
1163
+ ChainIdZod as ChainIdZod2,
1164
+ HandshakeIdZod as HandshakeIdZod3,
1165
+ HashZod as HashZod4,
1166
+ normalizeAddress,
1167
+ NormalizedAddressZod as NormalizedAddressZod3
1168
+ } from "@xyo-network/event-kit-schemas";
1169
+ import { createEventKitHandshakeReadModel } from "@xyo-network/event-kit-statement-graph";
1170
+ import {
1171
+ assertEventKitNetworkSession,
1172
+ publishPublisherEncryptionKeyRevocation,
1173
+ publishPublisherService,
1174
+ publishPublisherServiceRevocation,
1175
+ publishWakeGrantRevocation,
1176
+ publishWakeSubscription,
1177
+ publishWakeSubscriptionAcceptance,
1178
+ publishWakeSubscriptionRevocation
1179
+ } from "@xyo-network/event-kit-xl1";
1180
+ var EventKitHandshakeClientError = class extends Error {
1181
+ code;
1182
+ constructor(code, message) {
1183
+ super(message);
1184
+ this.name = "EventKitHandshakeClientError";
1185
+ this.code = code;
1186
+ }
1187
+ };
1188
+ function createSynchronizedEventKitHandshakeReader(options) {
1189
+ const publisher = NormalizedAddressZod3.parse(options.publisher);
1190
+ const chainId = ChainIdZod2.parse(options.session.chainId);
1191
+ function assertIdentity() {
1192
+ const checkpoint = options.replay.snapshot();
1193
+ if (checkpoint.chainId !== chainId || checkpoint.network !== options.session.network || checkpoint.publisher !== publisher) {
1194
+ throw new EventKitHandshakeClientError(
1195
+ "identity-mismatch",
1196
+ "Finalized replay identity does not match the pinned Event Kit network and publisher"
1197
+ );
1198
+ }
1199
+ }
1200
+ return Object.freeze({
1201
+ chainId,
1202
+ publisher,
1203
+ async sync() {
1204
+ await assertEventKitNetworkSession(options.session, options.chain);
1205
+ assertIdentity();
1206
+ const status = await options.replay.sync();
1207
+ await assertEventKitNetworkSession(options.session, options.chain);
1208
+ assertIdentity();
1209
+ return Object.freeze({
1210
+ ...status.blocked === void 0 ? {} : { blocked: status.blocked },
1211
+ finalizedHead: status.finalizedHead,
1212
+ indexedThrough: status.indexedThrough,
1213
+ readModel: createEventKitHandshakeReadModel({
1214
+ body: (hash) => options.replay.body(hash),
1215
+ viewer: options.replay.viewer()
1216
+ })
1217
+ });
1218
+ }
1219
+ });
1220
+ }
1221
+ function createEventKitHandshakePublicationPort(transport) {
1222
+ const chainId = ChainIdZod2.parse(transport.session.chainId);
1223
+ const port = {
1224
+ chainId,
1225
+ async publishPublisherEncryptionKeyRevocation(input) {
1226
+ return await publishPublisherEncryptionKeyRevocation({ ...transport, ...input });
1227
+ },
1228
+ async publishPublisherService(input) {
1229
+ return await publishPublisherService({ ...transport, ...input });
1230
+ },
1231
+ async publishPublisherServiceRevocation(input) {
1232
+ return await publishPublisherServiceRevocation({ ...transport, ...input });
1233
+ },
1234
+ async publishWakeGrantRevocation(input) {
1235
+ return await publishWakeGrantRevocation({ ...transport, ...input });
1236
+ },
1237
+ async publishWakeSubscription(input) {
1238
+ return await publishWakeSubscription({ ...transport, ...input });
1239
+ },
1240
+ async publishWakeSubscriptionAcceptance(input) {
1241
+ return await publishWakeSubscriptionAcceptance({ ...transport, ...input });
1242
+ },
1243
+ async publishWakeSubscriptionRevocation(input) {
1244
+ return await publishWakeSubscriptionRevocation({ ...transport, ...input });
1245
+ },
1246
+ async source() {
1247
+ const raw = String(await transport.signer.address());
1248
+ const normalized = NormalizedAddressZod3.parse(normalizeAddress(raw));
1249
+ if (raw !== normalized) {
1250
+ throw new EventKitHandshakeClientError(
1251
+ "identity-mismatch",
1252
+ "XL1 publication signer address is not canonical lowercase hexadecimal without 0x"
1253
+ );
1254
+ }
1255
+ return normalized;
1256
+ }
1257
+ };
1258
+ return Object.freeze(port);
1259
+ }
1260
+ function nextBlock(head) {
1261
+ if (!Number.isSafeInteger(head) || head < 0 || head === Number.MAX_SAFE_INTEGER) {
1262
+ throw new RangeError("finalized head cannot advance to a JSON-safe block number");
1263
+ }
1264
+ return head + 1;
1265
+ }
1266
+ function samplePublisherClock(nowSeconds) {
1267
+ const sampled = nowSeconds();
1268
+ if (!Number.isSafeInteger(sampled) || sampled < 0) {
1269
+ throw new EventKitHandshakeClientError(
1270
+ "challenge-rejected",
1271
+ "Publisher clock must return a nonnegative JSON-safe Unix second"
1272
+ );
1273
+ }
1274
+ return sampled;
1275
+ }
1276
+ function readySnapshot(snapshot) {
1277
+ if (snapshot.blocked !== void 0) {
1278
+ throw new EventKitHandshakeClientError("projection-blocked", "Finalized handshake replay is blocked");
1279
+ }
1280
+ if (snapshot.indexedThrough !== snapshot.finalizedHead) {
1281
+ throw new EventKitHandshakeClientError("projection-stale", "Finalized handshake replay is not caught up");
1282
+ }
1283
+ return snapshot;
1284
+ }
1285
+ function serviceQuery(reader, snapshot, serviceId) {
1286
+ return {
1287
+ chainId: reader.chainId,
1288
+ finalizedHead: snapshot.finalizedHead,
1289
+ publisher: reader.publisher,
1290
+ ...serviceId === void 0 ? {} : { serviceId }
1291
+ };
1292
+ }
1293
+ function requireService(discovery) {
1294
+ if (!discovery.ok) {
1295
+ throw new EventKitHandshakeClientError(
1296
+ "service-unavailable",
1297
+ `Publisher service is unavailable: ${discovery.reason}`
1298
+ );
1299
+ }
1300
+ return discovery;
1301
+ }
1302
+ function requireExpectedService(discovery, expected) {
1303
+ const expectedHash = HashZod4.parse(expected);
1304
+ if (discovery.service.objectHash !== expectedHash) {
1305
+ throw new EventKitHandshakeClientError(
1306
+ "service-mismatch",
1307
+ "Currently selected publisher service does not match the expected object hash"
1308
+ );
1309
+ }
1310
+ return expectedHash;
1311
+ }
1312
+ function assertPublicationIdentity(publication, reader) {
1313
+ if (publication.chainId !== reader.chainId) {
1314
+ throw new EventKitHandshakeClientError(
1315
+ "identity-mismatch",
1316
+ "Publication transport chain ID does not match the synchronized handshake reader"
1317
+ );
1318
+ }
1319
+ }
1320
+ async function assertPublicationSource(publication, expected) {
1321
+ if (await publication.source() !== expected) {
1322
+ throw new EventKitHandshakeClientError(
1323
+ "identity-mismatch",
1324
+ "Publication signer does not match the required handshake Claim source"
1325
+ );
1326
+ }
1327
+ }
1328
+ function intentLookup(reader, kind, operationId, source) {
1329
+ return {
1330
+ chainId: reader.chainId,
1331
+ kind,
1332
+ operationId,
1333
+ publisher: reader.publisher,
1334
+ source
1335
+ };
1336
+ }
1337
+ function mutationIntentError(error) {
1338
+ return error instanceof HandshakeMutationIntentConflictError ? new EventKitHandshakeClientError("mutation-intent-conflict", error.message) : new EventKitHandshakeClientError(
1339
+ "mutation-intent-unavailable",
1340
+ "Handshake mutation intent storage is unavailable or invalid"
1341
+ );
1342
+ }
1343
+ async function loadMutationIntent(store, reader, kind, operationId, source, requestFingerprint) {
1344
+ try {
1345
+ const existing = await store.get(intentLookup(reader, kind, operationId, source));
1346
+ if (existing === void 0) return void 0;
1347
+ if (existing.kind !== kind || existing.requestFingerprint !== requestFingerprint) {
1348
+ throw new HandshakeMutationIntentConflictError();
1349
+ }
1350
+ return existing;
1351
+ } catch (error) {
1352
+ throw mutationIntentError(error);
1353
+ }
1354
+ }
1355
+ async function persistMutationIntent(store, intent) {
1356
+ try {
1357
+ return await store.getOrCreate(intent);
1358
+ } catch (error) {
1359
+ throw mutationIntentError(error);
1360
+ }
1361
+ }
1362
+ async function publisherServiceRequestFingerprint(input) {
1363
+ return await handshakeMutationRequestFingerprint({
1364
+ expiresAtBlock: input.expiresAtBlock ?? null,
1365
+ maxBlocksPerWake: input.maxBlocksPerWake,
1366
+ maxSubscriptionLifetimeBlocks: input.maxSubscriptionLifetimeBlocks,
1367
+ validFromBlock: input.validFromBlock ?? null
1368
+ });
1369
+ }
1370
+ async function publisherRotationRequestFingerprint(input) {
1371
+ return await handshakeMutationRequestFingerprint({
1372
+ expiresAtBlock: input.expiresAtBlock ?? null,
1373
+ keyExpiresAtBlock: input.keyExpiresAtBlock ?? null,
1374
+ maxBlocksPerWake: input.maxBlocksPerWake ?? null,
1375
+ maxSubscriptionLifetimeBlocks: input.maxSubscriptionLifetimeBlocks ?? null,
1376
+ service: HashZod4.parse(input.service),
1377
+ validFromBlock: input.validFromBlock ?? null
1378
+ });
1379
+ }
1380
+ async function subscriberSubscriptionRequestFingerprint(input) {
1381
+ return await handshakeMutationRequestFingerprint({
1382
+ audience: input.audience,
1383
+ challengeSecret: input.challengeSecret,
1384
+ deploymentId: input.deploymentId,
1385
+ endpoint: input.endpoint,
1386
+ expiresAtBlock: input.expiresAtBlock,
1387
+ grantId: input.grantId ?? null,
1388
+ maxBlocksPerWake: input.maxBlocksPerWake ?? null,
1389
+ queueId: input.queueId ?? null,
1390
+ revision: input.revision ?? null,
1391
+ subscriptionId: input.subscriptionId ?? null,
1392
+ supersedes: input.supersedes ?? null
1393
+ });
1394
+ }
1395
+ async function exactMutationRequestFingerprint(value) {
1396
+ return await handshakeMutationRequestFingerprint(value);
1397
+ }
1398
+ function protocolCandidate(reader, discovery, candidate, keys) {
1399
+ return {
1400
+ chainId: reader.chainId,
1401
+ finalizedHead: discovery.finalizedHead,
1402
+ grant: {
1403
+ body: candidate.tuple.grant.body,
1404
+ objectHash: HashZod4.parse(candidate.tuple.grant.objectHash)
1405
+ },
1406
+ key: {
1407
+ body: candidate.key.body,
1408
+ objectHash: HashZod4.parse(candidate.key.objectHash)
1409
+ },
1410
+ keys,
1411
+ publisher: reader.publisher,
1412
+ service: {
1413
+ body: candidate.tuple.service.body,
1414
+ objectHash: HashZod4.parse(candidate.tuple.service.objectHash)
1415
+ },
1416
+ subscriber: NormalizedAddressZod3.parse(candidate.subscriber),
1417
+ subscription: {
1418
+ body: candidate.tuple.subscription.body,
1419
+ objectHash: HashZod4.parse(candidate.tuple.subscription.objectHash)
1420
+ },
1421
+ tupleBlock: candidate.tuple.tupleEvent.block,
1422
+ tupleObjectHashes: [
1423
+ HashZod4.parse(candidate.tuple.service.objectHash),
1424
+ HashZod4.parse(candidate.tuple.grant.objectHash),
1425
+ HashZod4.parse(candidate.tuple.subscription.objectHash)
1426
+ ],
1427
+ tupleSubject: reader.publisher
1428
+ };
1429
+ }
1430
+ function validationError(reason) {
1431
+ return new EventKitHandshakeClientError(
1432
+ "subscription-invalid",
1433
+ `Publisher subscription candidate failed validation: ${reason}`
1434
+ );
1435
+ }
1436
+ function challengeError(result) {
1437
+ const reason = result.ok ? "unknown" : result.reason;
1438
+ return new EventKitHandshakeClientError("challenge-rejected", `Endpoint challenge was rejected: ${reason}`);
1439
+ }
1440
+ function createPublisherHandshakeClient(options) {
1441
+ assertPublicationIdentity(options.publication, options.reader);
1442
+ const serviceId = options.serviceId === void 0 ? void 0 : HandshakeIdZod3.parse(options.serviceId);
1443
+ async function snapshot() {
1444
+ return readySnapshot(await options.reader.sync());
1445
+ }
1446
+ async function discovery(current) {
1447
+ return await current.readModel.discoverPublisherService(serviceQuery(options.reader, current, serviceId));
1448
+ }
1449
+ async function candidateDiscovery(selector = {}) {
1450
+ const current = await snapshot();
1451
+ return await current.readModel.acceptanceCandidates({
1452
+ ...serviceQuery(options.reader, current, serviceId),
1453
+ ...selector.subscriber === void 0 ? {} : { subscriber: NormalizedAddressZod3.parse(selector.subscriber) },
1454
+ ...selector.subscriptionId === void 0 ? {} : { subscriptionId: HandshakeIdZod3.parse(selector.subscriptionId) }
1455
+ });
1456
+ }
1457
+ async function resolveCandidate(selector) {
1458
+ const found = await candidateDiscovery(selector);
1459
+ if (!found.ok) throw new EventKitHandshakeClientError("service-unavailable", `Publisher service is unavailable: ${found.reason}`);
1460
+ const subscriber = NormalizedAddressZod3.parse(selector.subscriber);
1461
+ const subscription = selector.subscription === void 0 ? void 0 : HashZod4.parse(selector.subscription);
1462
+ const candidates = found.candidates.filter((candidate2) => candidate2.subscriber === subscriber && (subscription === void 0 || candidate2.tuple.subscription.objectHash === subscription));
1463
+ if (candidates.length === 0) {
1464
+ throw new EventKitHandshakeClientError("candidate-not-found", "No exact finalized acceptance candidate was found");
1465
+ }
1466
+ if (candidates.length !== 1) {
1467
+ throw new EventKitHandshakeClientError("candidate-ambiguous", "More than one finalized acceptance candidate matched");
1468
+ }
1469
+ const candidate = candidates[0];
1470
+ const validated = await validateAndDecryptPublisherSubscription(
1471
+ protocolCandidate(options.reader, found, candidate, options.keys)
1472
+ );
1473
+ if (!validated.ok) throw validationError(validated.reason);
1474
+ return { candidate, validated };
1475
+ }
1476
+ async function candidateForChallenge(challenge) {
1477
+ const resolved = await resolveCandidate({
1478
+ subscriber: challenge.subscriber,
1479
+ subscription: challenge.subscription
1480
+ });
1481
+ if (!endpointChallengeMatchesPublisherContext(challenge, resolved.validated.context)) {
1482
+ throw new EventKitHandshakeClientError(
1483
+ "challenge-context-invalid",
1484
+ "Endpoint challenge does not match the current finalized subscription candidate"
1485
+ );
1486
+ }
1487
+ return resolved;
1488
+ }
1489
+ async function reconcileAcceptance(acceptance, publication, subscriber, subscription) {
1490
+ let current;
1491
+ try {
1492
+ current = await options.reader.sync();
1493
+ } catch {
1494
+ return Object.freeze({ reason: "synchronization-failed", status: "unavailable" });
1495
+ }
1496
+ if (current.blocked !== void 0) {
1497
+ return Object.freeze({
1498
+ finalizedHead: current.finalizedHead,
1499
+ indexedThrough: current.indexedThrough,
1500
+ reason: "projection-blocked",
1501
+ status: "unavailable"
1502
+ });
1503
+ }
1504
+ if (current.indexedThrough !== current.finalizedHead) {
1505
+ return Object.freeze({
1506
+ finalizedHead: current.finalizedHead,
1507
+ indexedThrough: current.indexedThrough,
1508
+ reason: "projection-stale",
1509
+ status: "unavailable"
1510
+ });
1511
+ }
1512
+ if (current.finalizedHead < publication.finalizedBlock) {
1513
+ return Object.freeze({
1514
+ finalizedHead: current.finalizedHead,
1515
+ indexedThrough: current.indexedThrough,
1516
+ reason: "publication-not-indexed",
1517
+ status: "unavailable"
1518
+ });
1519
+ }
1520
+ let view;
1521
+ try {
1522
+ view = await current.readModel.publisher(serviceQuery(options.reader, current, serviceId));
1523
+ } catch {
1524
+ return Object.freeze({
1525
+ finalizedHead: current.finalizedHead,
1526
+ indexedThrough: current.indexedThrough,
1527
+ reason: "synchronization-failed",
1528
+ status: "unavailable"
1529
+ });
1530
+ }
1531
+ if (!view.ok) {
1532
+ return Object.freeze({
1533
+ finalizedHead: view.finalizedHead,
1534
+ indexedThrough: view.indexedThrough,
1535
+ reason: view.reason,
1536
+ status: "unavailable"
1537
+ });
1538
+ }
1539
+ const matches = view.subscriptions.filter((item) => item.subscriber === subscriber && item.tuples.some((tuple) => tuple.subscription.objectHash === subscription));
1540
+ if (matches.length === 0) {
1541
+ return Object.freeze({
1542
+ finalizedHead: view.finalizedHead,
1543
+ indexedThrough: view.indexedThrough,
1544
+ status: "not-found"
1545
+ });
1546
+ }
1547
+ if (matches.length !== 1) {
1548
+ return Object.freeze({
1549
+ finalizedHead: view.finalizedHead,
1550
+ indexedThrough: view.indexedThrough,
1551
+ reason: "ambiguous-lifecycle",
1552
+ status: "unavailable"
1553
+ });
1554
+ }
1555
+ const lifecycle = matches[0];
1556
+ const active = lifecycle.state === "active" && lifecycle.selectedSubscription === subscription && lifecycle.acceptance?.objectHash === acceptance;
1557
+ return Object.freeze({
1558
+ finalizedHead: view.finalizedHead,
1559
+ indexedThrough: view.indexedThrough,
1560
+ lifecycle,
1561
+ status: active ? "active" : "not-active"
1562
+ });
1563
+ }
1564
+ async function publishAcceptanceIntent(intent, intentDisposition) {
1565
+ const [acceptanceHash, responseHash] = await Promise.all([
1566
+ handshakeObjectHash2(intent.acceptance),
1567
+ handshakeObjectHash2(intent.response)
1568
+ ]);
1569
+ const publication = await options.publication.publishWakeSubscriptionAcceptance({
1570
+ acceptance: intent.acceptance,
1571
+ challenge: intent.challenge,
1572
+ operationId: intent.operationId,
1573
+ response: intent.response
1574
+ });
1575
+ const reconciliation = await reconcileAcceptance(
1576
+ acceptanceHash,
1577
+ publication,
1578
+ intent.challenge.subscriber,
1579
+ intent.challenge.subscription
1580
+ );
1581
+ return Object.freeze({
1582
+ acceptance: Object.freeze({ body: intent.acceptance, objectHash: acceptanceHash }),
1583
+ challenge: intent.operationId,
1584
+ intentDisposition,
1585
+ operationId: intent.operationId,
1586
+ publication,
1587
+ reconciliation,
1588
+ response: responseHash,
1589
+ subscriber: intent.challenge.subscriber,
1590
+ subscription: intent.challenge.subscription
1591
+ });
1592
+ }
1593
+ async function publishPublisherServiceIntent(intent, intentDisposition) {
1594
+ let keyCustody;
1595
+ try {
1596
+ keyCustody = await options.keys.putPrivateKey({
1597
+ key: intent.privateKey,
1598
+ publisher: options.reader.publisher
1599
+ });
1600
+ } catch {
1601
+ throw new EventKitHandshakeClientError(
1602
+ "key-custody-failed",
1603
+ "Publisher encryption private key was not durably retained; publication was not attempted"
1604
+ );
1605
+ }
1606
+ const [keyHash, serviceHash] = await Promise.all([
1607
+ handshakeObjectHash2(intent.encryptionKey),
1608
+ handshakeObjectHash2(intent.service)
1609
+ ]);
1610
+ const publication = await options.publication.publishPublisherService({
1611
+ encryptionKeys: [intent.encryptionKey],
1612
+ operationId: intent.operationId,
1613
+ service: intent.service
1614
+ });
1615
+ return Object.freeze({
1616
+ encryptionKey: Object.freeze({ body: intent.encryptionKey, objectHash: keyHash }),
1617
+ intentDisposition,
1618
+ keyCustody,
1619
+ operationId: intent.operationId,
1620
+ publication,
1621
+ service: Object.freeze({ body: intent.service, objectHash: serviceHash })
1622
+ });
1623
+ }
1624
+ const client = {
1625
+ async acceptanceCandidates(selector = {}) {
1626
+ const found = await candidateDiscovery(selector);
1627
+ if (!found.ok || selector.subscription === void 0) return found;
1628
+ const subscription = HashZod4.parse(selector.subscription);
1629
+ return Object.freeze({
1630
+ ...found,
1631
+ candidates: found.candidates.filter(
1632
+ (candidate) => candidate.tuple.subscription.objectHash === subscription
1633
+ )
1634
+ });
1635
+ },
1636
+ async beginChallenge(input) {
1637
+ const challengeHash = HashZod4.parse(input.challengeHash);
1638
+ const record = await options.challenges.get(challengeHash);
1639
+ if (record === void 0) return { kind: "missing" };
1640
+ const resolved = await candidateForChallenge(record.challenge);
1641
+ const begun = await beginPublisherEndpointChallengeAttempt({
1642
+ challengeHash,
1643
+ nowSeconds: samplePublisherClock(input.nowSeconds),
1644
+ store: options.challenges
1645
+ });
1646
+ if (begun.kind !== "started") {
1647
+ if (begun.kind === "store-invalid" || begun.kind === "store-unavailable") {
1648
+ throw new EventKitHandshakeClientError("challenge-rejected", `Endpoint challenge store failed: ${begun.kind}`);
1649
+ }
1650
+ return { kind: begun.kind };
1651
+ }
1652
+ return Object.freeze({
1653
+ challenge: begun.record.challenge,
1654
+ challengeHash,
1655
+ kind: "started",
1656
+ subscriber: resolved.validated.context.subscriber,
1657
+ subscription: resolved.validated.context.subscription,
1658
+ target: Object.freeze({
1659
+ audience: resolved.validated.plaintext.audience,
1660
+ endpoint: resolved.validated.plaintext.endpoint
1661
+ })
1662
+ });
1663
+ },
1664
+ async completeChallengeAndAccept(input) {
1665
+ await assertPublicationSource(options.publication, options.reader.publisher);
1666
+ const challengeHash = HashZod4.parse(input.challengeHash);
1667
+ const subscriber = NormalizedAddressZod3.parse(input.subscriber);
1668
+ const subscription = HashZod4.parse(input.subscription);
1669
+ const record = await options.challenges.get(challengeHash);
1670
+ if (record === void 0) {
1671
+ throw new EventKitHandshakeClientError("challenge-not-found", "Endpoint challenge was not found");
1672
+ }
1673
+ if (record.challenge.subscriber !== subscriber || record.challenge.subscription !== subscription) {
1674
+ throw new EventKitHandshakeClientError(
1675
+ "challenge-context-invalid",
1676
+ "Stored endpoint challenge does not match the caller expected subscriber and subscription"
1677
+ );
1678
+ }
1679
+ const responseHash = await handshakeObjectHash2(input.response);
1680
+ const requestFingerprint = await exactMutationRequestFingerprint({
1681
+ challengeHash,
1682
+ responseHash,
1683
+ subscriber,
1684
+ subscription
1685
+ });
1686
+ const existing = await loadMutationIntent(
1687
+ options.intents,
1688
+ options.reader,
1689
+ "publisher-subscription-accept",
1690
+ challengeHash,
1691
+ options.reader.publisher,
1692
+ requestFingerprint
1693
+ );
1694
+ if (existing !== void 0) {
1695
+ if (record.state !== "verified" || record.responseHash !== responseHash) {
1696
+ throw new EventKitHandshakeClientError(
1697
+ "mutation-intent-conflict",
1698
+ "Acceptance intent does not match the durable verified challenge decision"
1699
+ );
1700
+ }
1701
+ return await publishAcceptanceIntent(existing, "resumed");
1702
+ }
1703
+ const resolved = await candidateForChallenge(record.challenge);
1704
+ const completed = await completePublisherEndpointChallenge({
1705
+ challenge: record.challenge,
1706
+ nowSeconds: samplePublisherClock(input.nowSeconds),
1707
+ response: input.response,
1708
+ store: options.challenges,
1709
+ subscription: resolved.validated
1710
+ });
1711
+ if (!completed.ok) throw challengeError(completed);
1712
+ const concurrentlyPersisted = await loadMutationIntent(
1713
+ options.intents,
1714
+ options.reader,
1715
+ "publisher-subscription-accept",
1716
+ challengeHash,
1717
+ options.reader.publisher,
1718
+ requestFingerprint
1719
+ );
1720
+ if (concurrentlyPersisted !== void 0) return await publishAcceptanceIntent(concurrentlyPersisted, "resumed");
1721
+ let current;
1722
+ try {
1723
+ current = await candidateForChallenge(record.challenge);
1724
+ } catch (error) {
1725
+ const recovered = await loadMutationIntent(
1726
+ options.intents,
1727
+ options.reader,
1728
+ "publisher-subscription-accept",
1729
+ challengeHash,
1730
+ options.reader.publisher,
1731
+ requestFingerprint
1732
+ );
1733
+ if (recovered !== void 0) return await publishAcceptanceIntent(recovered, "resumed");
1734
+ throw error;
1735
+ }
1736
+ if (!endpointChallengeMatchesPublisherContext(record.challenge, current.validated.context)) {
1737
+ throw new EventKitHandshakeClientError(
1738
+ "challenge-context-invalid",
1739
+ "Endpoint challenge candidate changed before acceptance publication"
1740
+ );
1741
+ }
1742
+ const resolvedIntent = await persistMutationIntent(options.intents, {
1743
+ acceptance: completed.acceptance,
1744
+ chainId: options.reader.chainId,
1745
+ challenge: record.challenge,
1746
+ kind: "publisher-subscription-accept",
1747
+ operationId: challengeHash,
1748
+ publisher: options.reader.publisher,
1749
+ requestFingerprint,
1750
+ response: input.response,
1751
+ source: options.reader.publisher,
1752
+ version: 1
1753
+ });
1754
+ return await publishAcceptanceIntent(resolvedIntent.intent, resolvedIntent.disposition);
1755
+ },
1756
+ async issueChallenge(input) {
1757
+ const resolved = await resolveCandidate(input);
1758
+ const issued = await issuePublisherEndpointChallenge({
1759
+ issuedAt: input.issuedAt,
1760
+ ...input.lifetimeSeconds === void 0 ? {} : { lifetimeSeconds: input.lifetimeSeconds },
1761
+ store: options.challenges,
1762
+ subscription: resolved.validated
1763
+ });
1764
+ if (!issued.ok) {
1765
+ throw new EventKitHandshakeClientError("challenge-rejected", `Endpoint challenge issue failed: ${issued.reason}`);
1766
+ }
1767
+ return Object.freeze({
1768
+ challenge: issued.challenge,
1769
+ challengeHash: issued.challengeHash,
1770
+ disposition: issued.disposition,
1771
+ subscriber: resolved.validated.context.subscriber,
1772
+ subscription: resolved.validated.context.subscription
1773
+ });
1774
+ },
1775
+ async key() {
1776
+ const current = await options.reader.sync();
1777
+ return await current.readModel.discoverPublisherService(serviceQuery(options.reader, current, serviceId));
1778
+ },
1779
+ async publishService(input) {
1780
+ await assertPublicationSource(options.publication, options.reader.publisher);
1781
+ if (serviceId !== void 0) {
1782
+ throw new EventKitHandshakeClientError(
1783
+ "service-root-requires-fresh-id",
1784
+ "Root service publication requires an unconfigured client so a fresh serviceId is generated"
1785
+ );
1786
+ }
1787
+ const operationId = HandshakeIdZod3.parse(input.operationId);
1788
+ const requestFingerprint = await publisherServiceRequestFingerprint(input);
1789
+ const priorIntent = await loadMutationIntent(
1790
+ options.intents,
1791
+ options.reader,
1792
+ "publisher-service-publish",
1793
+ operationId,
1794
+ options.reader.publisher,
1795
+ requestFingerprint
1796
+ );
1797
+ if (priorIntent !== void 0) return await publishPublisherServiceIntent(priorIntent, "resumed");
1798
+ const current = await snapshot();
1799
+ const existing = await discovery(current);
1800
+ if (existing.ok) {
1801
+ throw new EventKitHandshakeClientError(
1802
+ "service-already-exists",
1803
+ "An active publisher service already exists; rotate its key to publish a successor"
1804
+ );
1805
+ }
1806
+ if (existing.reason !== "service-not-found") {
1807
+ throw new EventKitHandshakeClientError("service-unavailable", `Cannot declare service: ${existing.reason}`);
1808
+ }
1809
+ const validFromBlock = input.validFromBlock ?? nextBlock(current.finalizedHead);
1810
+ const generated = await generatePublisherEncryptionKey({ validFromBlock });
1811
+ const service = createPublisherService({
1812
+ encryptionKeys: [generated.descriptorHash],
1813
+ ...input.expiresAtBlock === void 0 ? {} : { expiresAtBlock: input.expiresAtBlock },
1814
+ maxBlocksPerWake: input.maxBlocksPerWake,
1815
+ maxSubscriptionLifetimeBlocks: input.maxSubscriptionLifetimeBlocks,
1816
+ preferredEncryptionKey: generated.descriptorHash,
1817
+ validFromBlock
1818
+ });
1819
+ const resolvedIntent = await persistMutationIntent(options.intents, {
1820
+ chainId: options.reader.chainId,
1821
+ encryptionKey: generated.descriptor,
1822
+ kind: "publisher-service-publish",
1823
+ operationId,
1824
+ privateKey: generated.privateKey,
1825
+ publisher: options.reader.publisher,
1826
+ requestFingerprint,
1827
+ service,
1828
+ source: options.reader.publisher,
1829
+ version: 1
1830
+ });
1831
+ return await publishPublisherServiceIntent(resolvedIntent.intent, resolvedIntent.disposition);
1832
+ },
1833
+ async retireEncryptionKey(key) {
1834
+ await assertPublicationSource(options.publication, options.reader.publisher);
1835
+ const keyHash = HashZod4.parse(key);
1836
+ const requestFingerprint = await exactMutationRequestFingerprint({ key: keyHash });
1837
+ let resolvedIntent = await loadMutationIntent(
1838
+ options.intents,
1839
+ options.reader,
1840
+ "publisher-encryption-key-retire",
1841
+ keyHash,
1842
+ options.reader.publisher,
1843
+ requestFingerprint
1844
+ );
1845
+ let intentDisposition = "resumed";
1846
+ if (resolvedIntent === void 0) {
1847
+ const current = await snapshot();
1848
+ const selected = requireService(await discovery(current));
1849
+ if (selected.key.objectHash !== keyHash || !selected.service.body.encryptionKeys.includes(keyHash) || selected.service.body.preferredEncryptionKey !== keyHash) {
1850
+ throw new EventKitHandshakeClientError(
1851
+ "key-mismatch",
1852
+ "Encryption key does not match the exact current preferred descriptor evidence"
1853
+ );
1854
+ }
1855
+ const persisted = await persistMutationIntent(options.intents, {
1856
+ chainId: options.reader.chainId,
1857
+ kind: "publisher-encryption-key-retire",
1858
+ operationId: keyHash,
1859
+ publisher: options.reader.publisher,
1860
+ requestFingerprint,
1861
+ revocation: createPublisherEncryptionKeyRevocation(keyHash),
1862
+ source: options.reader.publisher,
1863
+ version: 1
1864
+ });
1865
+ resolvedIntent = persisted.intent;
1866
+ intentDisposition = persisted.disposition;
1867
+ }
1868
+ const revocation = await withHandshakeObjectHash(resolvedIntent.revocation);
1869
+ const publication = await options.publication.publishPublisherEncryptionKeyRevocation({
1870
+ operationId: keyHash,
1871
+ revocation: resolvedIntent.revocation
1872
+ });
1873
+ return Object.freeze({
1874
+ intentDisposition,
1875
+ key: keyHash,
1876
+ operationId: keyHash,
1877
+ publication,
1878
+ revocation
1879
+ });
1880
+ },
1881
+ async retireService(expectedService) {
1882
+ await assertPublicationSource(options.publication, options.reader.publisher);
1883
+ const service = HashZod4.parse(expectedService);
1884
+ const requestFingerprint = await exactMutationRequestFingerprint({ service });
1885
+ let resolvedIntent = await loadMutationIntent(
1886
+ options.intents,
1887
+ options.reader,
1888
+ "publisher-service-retire",
1889
+ service,
1890
+ options.reader.publisher,
1891
+ requestFingerprint
1892
+ );
1893
+ let intentDisposition = "resumed";
1894
+ if (resolvedIntent === void 0) {
1895
+ const current = await snapshot();
1896
+ const selected = requireService(await discovery(current));
1897
+ requireExpectedService(selected, service);
1898
+ const persisted = await persistMutationIntent(options.intents, {
1899
+ chainId: options.reader.chainId,
1900
+ kind: "publisher-service-retire",
1901
+ operationId: service,
1902
+ publisher: options.reader.publisher,
1903
+ requestFingerprint,
1904
+ revocation: createPublisherServiceRevocation(service),
1905
+ source: options.reader.publisher,
1906
+ version: 1
1907
+ });
1908
+ resolvedIntent = persisted.intent;
1909
+ intentDisposition = persisted.disposition;
1910
+ }
1911
+ const revocation = await withHandshakeObjectHash(resolvedIntent.revocation);
1912
+ const publication = await options.publication.publishPublisherServiceRevocation({
1913
+ operationId: service,
1914
+ revocation: resolvedIntent.revocation
1915
+ });
1916
+ return Object.freeze({
1917
+ intentDisposition,
1918
+ operationId: service,
1919
+ publication,
1920
+ revocation,
1921
+ service
1922
+ });
1923
+ },
1924
+ async rotateEncryptionKey(input) {
1925
+ await assertPublicationSource(options.publication, options.reader.publisher);
1926
+ const operationId = HandshakeIdZod3.parse(input.operationId);
1927
+ const requestFingerprint = await publisherRotationRequestFingerprint(input);
1928
+ const priorIntent = await loadMutationIntent(
1929
+ options.intents,
1930
+ options.reader,
1931
+ "publisher-service-rotate-key",
1932
+ operationId,
1933
+ options.reader.publisher,
1934
+ requestFingerprint
1935
+ );
1936
+ if (priorIntent !== void 0) return await publishPublisherServiceIntent(priorIntent, "resumed");
1937
+ const current = await snapshot();
1938
+ const selected = requireService(await discovery(current));
1939
+ requireExpectedService(selected, input.service);
1940
+ const validFromBlock = input.validFromBlock ?? nextBlock(current.finalizedHead);
1941
+ const generated = await generatePublisherEncryptionKey({
1942
+ ...input.keyExpiresAtBlock === void 0 ? {} : { expiresAtBlock: input.keyExpiresAtBlock },
1943
+ validFromBlock
1944
+ });
1945
+ const service = createPublisherService({
1946
+ encryptionKeys: [generated.descriptorHash],
1947
+ ...input.expiresAtBlock === void 0 ? selected.service.body.expiresAtBlock === void 0 ? {} : { expiresAtBlock: selected.service.body.expiresAtBlock } : { expiresAtBlock: input.expiresAtBlock },
1948
+ maxBlocksPerWake: input.maxBlocksPerWake ?? selected.service.body.limits.maxBlocksPerWake,
1949
+ maxSubscriptionLifetimeBlocks: input.maxSubscriptionLifetimeBlocks ?? selected.service.body.limits.maxSubscriptionLifetimeBlocks,
1950
+ preferredEncryptionKey: generated.descriptorHash,
1951
+ revision: selected.service.body.revision + 1,
1952
+ serviceId: selected.service.body.serviceId,
1953
+ supersedes: HashZod4.parse(selected.service.objectHash),
1954
+ validFromBlock
1955
+ });
1956
+ const resolvedIntent = await persistMutationIntent(options.intents, {
1957
+ chainId: options.reader.chainId,
1958
+ encryptionKey: generated.descriptor,
1959
+ kind: "publisher-service-rotate-key",
1960
+ operationId,
1961
+ privateKey: generated.privateKey,
1962
+ publisher: options.reader.publisher,
1963
+ requestFingerprint,
1964
+ selectedKey: HashZod4.parse(selected.key.objectHash),
1965
+ selectedService: HashZod4.parse(selected.service.objectHash),
1966
+ service,
1967
+ source: options.reader.publisher,
1968
+ version: 1
1969
+ });
1970
+ return await publishPublisherServiceIntent(resolvedIntent.intent, resolvedIntent.disposition);
1971
+ },
1972
+ async service() {
1973
+ const current = await options.reader.sync();
1974
+ return await current.readModel.discoverPublisherService(serviceQuery(options.reader, current, serviceId));
1975
+ },
1976
+ async subscriptions() {
1977
+ const current = await options.reader.sync();
1978
+ return await current.readModel.publisher(serviceQuery(options.reader, current, serviceId));
1979
+ }
1980
+ };
1981
+ return Object.freeze(client);
1982
+ }
1983
+ function tupleHashes(view) {
1984
+ const tuples = view.subscriptions.flatMap((subscription) => subscription.tuples);
1985
+ return {
1986
+ grants: new Set(tuples.map((tuple) => tuple.grant.objectHash)),
1987
+ subscriptions: new Set(tuples.map((tuple) => tuple.subscription.objectHash))
1988
+ };
1989
+ }
1990
+ function createSubscriberHandshakeClient(options) {
1991
+ assertPublicationIdentity(options.publication, options.reader);
1992
+ const subscriber = NormalizedAddressZod3.parse(options.subscriber);
1993
+ const serviceId = options.serviceId === void 0 ? void 0 : HandshakeIdZod3.parse(options.serviceId);
1994
+ async function snapshot() {
1995
+ return readySnapshot(await options.reader.sync());
1996
+ }
1997
+ async function serviceAt(current) {
1998
+ return requireService(
1999
+ await current.readModel.discoverPublisherService(serviceQuery(options.reader, current, serviceId))
2000
+ );
2001
+ }
2002
+ async function lookup(subscriptionId) {
2003
+ const current = await options.reader.sync();
2004
+ return await lookupAt(current, subscriptionId);
2005
+ }
2006
+ async function lookupAt(current, subscriptionId) {
2007
+ return await current.readModel.subscriber({
2008
+ ...serviceQuery(options.reader, current, serviceId),
2009
+ subscriber,
2010
+ ...subscriptionId === void 0 ? {} : { subscriptionId: HandshakeIdZod3.parse(subscriptionId) }
2011
+ });
2012
+ }
2013
+ async function validateRevision(current, input) {
2014
+ const revision = input.revision ?? 1;
2015
+ if (revision === 1) {
2016
+ if (input.supersedes !== void 0) {
2017
+ throw new EventKitHandshakeClientError(
2018
+ "subscription-invalid",
2019
+ "Subscription revision 1 must not declare supersedes"
2020
+ );
2021
+ }
2022
+ if (input.subscriptionId === void 0) return;
2023
+ const view2 = await lookupAt(current, input.subscriptionId);
2024
+ if (!view2.ok) {
2025
+ throw new EventKitHandshakeClientError("service-unavailable", `Cannot validate subscription identity: ${view2.reason}`);
2026
+ }
2027
+ if (view2.subscriptions.some((item) => item.subscriptionId === input.subscriptionId)) {
2028
+ throw new EventKitHandshakeClientError(
2029
+ "subscription-invalid",
2030
+ "Subscription revision 1 would duplicate an existing finalized stable identity"
2031
+ );
2032
+ }
2033
+ return;
2034
+ }
2035
+ if (input.subscriptionId === void 0 || input.supersedes === void 0) {
2036
+ throw new EventKitHandshakeClientError(
2037
+ "subscription-invalid",
2038
+ "A replacement subscription requires subscriptionId and supersedes"
2039
+ );
2040
+ }
2041
+ const view = await lookupAt(current, input.subscriptionId);
2042
+ if (!view.ok) {
2043
+ throw new EventKitHandshakeClientError("service-unavailable", `Cannot validate subscription revision: ${view.reason}`);
2044
+ }
2045
+ const lifecycles = view.subscriptions.filter((item) => item.subscriptionId === input.subscriptionId);
2046
+ if (lifecycles.length !== 1) {
2047
+ throw new EventKitHandshakeClientError(
2048
+ "subscription-invalid",
2049
+ "Replacement subscription does not resolve one finalized stable identity"
2050
+ );
2051
+ }
2052
+ if (lifecycles[0].state === "rejected") {
2053
+ throw new EventKitHandshakeClientError(
2054
+ "subscription-invalid",
2055
+ "Replacement subscription cannot extend a rejected finalized revision chain"
2056
+ );
2057
+ }
2058
+ const highestRevision = Math.max(...lifecycles[0].tuples.map((tuple) => tuple.subscription.body.revision));
2059
+ const latest = lifecycles[0].tuples.filter((tuple) => tuple.subscription.body.revision === highestRevision);
2060
+ if (latest.length !== 1 || revision !== highestRevision + 1 || latest[0].subscription.objectHash !== input.supersedes) {
2061
+ throw new EventKitHandshakeClientError(
2062
+ "subscription-invalid",
2063
+ "Replacement subscription revision and supersedes must extend the exact finalized chain tip"
2064
+ );
2065
+ }
2066
+ }
2067
+ async function publishSubscriberSubscriptionIntent(intent, intentDisposition) {
2068
+ const [grantHash, subscriptionHash] = await Promise.all([
2069
+ handshakeObjectHash2(intent.grant),
2070
+ handshakeObjectHash2(intent.subscription)
2071
+ ]);
2072
+ const publication = await options.publication.publishWakeSubscription({
2073
+ encryptionKey: intent.encryptionKey,
2074
+ grant: intent.grant,
2075
+ operationId: intent.operationId,
2076
+ service: intent.service,
2077
+ subscription: intent.subscription
2078
+ });
2079
+ return Object.freeze({
2080
+ grant: Object.freeze({ body: intent.grant, objectHash: grantHash }),
2081
+ intentDisposition,
2082
+ operationId: intent.operationId,
2083
+ publication,
2084
+ service: intent.selectedService,
2085
+ subscription: Object.freeze({ body: intent.subscription, objectHash: subscriptionHash })
2086
+ });
2087
+ }
2088
+ const client = {
2089
+ async revokeGrant(grant) {
2090
+ await assertPublicationSource(options.publication, subscriber);
2091
+ const grantHash = HashZod4.parse(grant);
2092
+ const requestFingerprint = await exactMutationRequestFingerprint({ grant: grantHash });
2093
+ let resolvedIntent = await loadMutationIntent(
2094
+ options.intents,
2095
+ options.reader,
2096
+ "subscriber-grant-revoke",
2097
+ grantHash,
2098
+ subscriber,
2099
+ requestFingerprint
2100
+ );
2101
+ let intentDisposition = "resumed";
2102
+ if (resolvedIntent === void 0) {
2103
+ const current = await snapshot();
2104
+ const view = await current.readModel.subscriber({ ...serviceQuery(options.reader, current, serviceId), subscriber });
2105
+ if (!view.ok || !tupleHashes(view).grants.has(grantHash)) {
2106
+ throw new EventKitHandshakeClientError(
2107
+ "subscription-not-found",
2108
+ "Grant is not present in this subscriber finalization view"
2109
+ );
2110
+ }
2111
+ const persisted = await persistMutationIntent(options.intents, {
2112
+ chainId: options.reader.chainId,
2113
+ kind: "subscriber-grant-revoke",
2114
+ operationId: grantHash,
2115
+ publisher: options.reader.publisher,
2116
+ requestFingerprint,
2117
+ revocation: createWakeGrantRevocation(grantHash),
2118
+ source: subscriber,
2119
+ version: 1
2120
+ });
2121
+ resolvedIntent = persisted.intent;
2122
+ intentDisposition = persisted.disposition;
2123
+ }
2124
+ const revocation = await withHandshakeObjectHash(resolvedIntent.revocation);
2125
+ const publication = await options.publication.publishWakeGrantRevocation({
2126
+ operationId: grantHash,
2127
+ publisher: options.reader.publisher,
2128
+ revocation: resolvedIntent.revocation
2129
+ });
2130
+ return Object.freeze({
2131
+ grant: grantHash,
2132
+ intentDisposition,
2133
+ operationId: grantHash,
2134
+ publication,
2135
+ revocation
2136
+ });
2137
+ },
2138
+ async revokeSubscription(subscription) {
2139
+ await assertPublicationSource(options.publication, subscriber);
2140
+ const subscriptionHash = HashZod4.parse(subscription);
2141
+ const requestFingerprint = await exactMutationRequestFingerprint({ subscription: subscriptionHash });
2142
+ let resolvedIntent = await loadMutationIntent(
2143
+ options.intents,
2144
+ options.reader,
2145
+ "subscriber-subscription-revoke",
2146
+ subscriptionHash,
2147
+ subscriber,
2148
+ requestFingerprint
2149
+ );
2150
+ let intentDisposition = "resumed";
2151
+ if (resolvedIntent === void 0) {
2152
+ const current = await snapshot();
2153
+ const view = await current.readModel.subscriber({ ...serviceQuery(options.reader, current, serviceId), subscriber });
2154
+ if (!view.ok || !tupleHashes(view).subscriptions.has(subscriptionHash)) {
2155
+ throw new EventKitHandshakeClientError(
2156
+ "subscription-not-found",
2157
+ "Subscription is not present in this subscriber finalization view"
2158
+ );
2159
+ }
2160
+ const persisted = await persistMutationIntent(options.intents, {
2161
+ chainId: options.reader.chainId,
2162
+ kind: "subscriber-subscription-revoke",
2163
+ operationId: subscriptionHash,
2164
+ publisher: options.reader.publisher,
2165
+ requestFingerprint,
2166
+ revocation: createWakeSubscriptionRevocation(subscriptionHash),
2167
+ source: subscriber,
2168
+ version: 1
2169
+ });
2170
+ resolvedIntent = persisted.intent;
2171
+ intentDisposition = persisted.disposition;
2172
+ }
2173
+ const revocation = await withHandshakeObjectHash(resolvedIntent.revocation);
2174
+ const publication = await options.publication.publishWakeSubscriptionRevocation({
2175
+ operationId: subscriptionHash,
2176
+ publisher: options.reader.publisher,
2177
+ revocation: resolvedIntent.revocation
2178
+ });
2179
+ return Object.freeze({
2180
+ intentDisposition,
2181
+ operationId: subscriptionHash,
2182
+ publication,
2183
+ revocation,
2184
+ subscription: subscriptionHash
2185
+ });
2186
+ },
2187
+ async service() {
2188
+ const current = await options.reader.sync();
2189
+ return await current.readModel.discoverPublisherService(serviceQuery(options.reader, current, serviceId));
2190
+ },
2191
+ async subscription(subscription) {
2192
+ const subscriptionHash = HashZod4.parse(subscription);
2193
+ const current = await options.reader.sync();
2194
+ const view = await lookupAt(current);
2195
+ if (!view.ok) return view;
2196
+ const matches = view.subscriptions.flatMap((lifecycle) => lifecycle.tuples.filter((tuple) => tuple.subscription.objectHash === subscriptionHash).map((tuple) => ({ lifecycle, tuple })));
2197
+ if (matches.length > 1) {
2198
+ throw new EventKitHandshakeClientError(
2199
+ "subscription-invalid",
2200
+ "Exact subscription hash appears more than once in the subscriber view"
2201
+ );
2202
+ }
2203
+ if (matches.length === 0) return Object.freeze({
2204
+ found: false,
2205
+ lookup: view,
2206
+ ok: true
2207
+ });
2208
+ return Object.freeze({
2209
+ found: true,
2210
+ lifecycle: matches[0].lifecycle,
2211
+ lookup: view,
2212
+ ok: true,
2213
+ tuple: matches[0].tuple
2214
+ });
2215
+ },
2216
+ async subscribe(input) {
2217
+ await assertPublicationSource(options.publication, subscriber);
2218
+ const operationId = HandshakeIdZod3.parse(input.operationId);
2219
+ const requestFingerprint = await subscriberSubscriptionRequestFingerprint(input);
2220
+ const priorIntent = await loadMutationIntent(
2221
+ options.intents,
2222
+ options.reader,
2223
+ "subscriber-subscription-subscribe",
2224
+ operationId,
2225
+ subscriber,
2226
+ requestFingerprint
2227
+ );
2228
+ if (priorIntent !== void 0) return await publishSubscriberSubscriptionIntent(priorIntent, "resumed");
2229
+ const current = await snapshot();
2230
+ const selected = await serviceAt(current);
2231
+ if (input.expiresAtBlock - current.finalizedHead <= 3) {
2232
+ throw new EventKitHandshakeClientError(
2233
+ "subscription-invalid",
2234
+ "Requested subscription expiry must leave finalized blocks for tuple publication, acceptance, and activation"
2235
+ );
2236
+ }
2237
+ await validateRevision(current, input);
2238
+ const maxBlocksPerWake = input.maxBlocksPerWake ?? selected.service.body.limits.maxBlocksPerWake;
2239
+ if (maxBlocksPerWake > selected.service.body.limits.maxBlocksPerWake) {
2240
+ throw new EventKitHandshakeClientError(
2241
+ "subscription-invalid",
2242
+ "Requested maxBlocksPerWake exceeds the selected publisher service limit"
2243
+ );
2244
+ }
2245
+ if (input.expiresAtBlock > current.finalizedHead + selected.service.body.limits.maxSubscriptionLifetimeBlocks) {
2246
+ throw new EventKitHandshakeClientError(
2247
+ "subscription-invalid",
2248
+ "Requested subscription expiry exceeds the selected publisher service lifetime limit"
2249
+ );
2250
+ }
2251
+ const grant = createWakeGrant({
2252
+ deploymentId: input.deploymentId,
2253
+ ...input.grantId === void 0 ? {} : { grantId: input.grantId },
2254
+ publisher: options.reader.publisher,
2255
+ ...input.queueId === void 0 || input.queueId === null ? {} : { queueId: input.queueId }
2256
+ });
2257
+ const grantHash = await handshakeObjectHash2(grant);
2258
+ const plaintext = createWakeSubscriptionPlaintext({
2259
+ audience: input.audience,
2260
+ challengeSecret: input.challengeSecret,
2261
+ deploymentId: input.deploymentId,
2262
+ endpoint: input.endpoint,
2263
+ maxBlocksPerWake,
2264
+ queueId: input.queueId
2265
+ });
2266
+ const subscription = await createEncryptedWakeSubscription({
2267
+ chainId: options.reader.chainId,
2268
+ expiresAtBlock: input.expiresAtBlock,
2269
+ grant: grantHash,
2270
+ plaintext,
2271
+ publisher: options.reader.publisher,
2272
+ publisherKey: {
2273
+ encryptionKey: HashZod4.parse(selected.key.objectHash),
2274
+ keyId: selected.key.body.keyId,
2275
+ publicKey: selected.key.body.publicKey
2276
+ },
2277
+ ...input.revision === void 0 ? {} : { revision: input.revision },
2278
+ service: HashZod4.parse(selected.service.objectHash),
2279
+ subscriber,
2280
+ ...input.subscriptionId === void 0 ? {} : { subscriptionId: input.subscriptionId },
2281
+ ...input.supersedes === void 0 ? {} : { supersedes: input.supersedes }
2282
+ });
2283
+ const resolvedIntent = await persistMutationIntent(options.intents, {
2284
+ chainId: options.reader.chainId,
2285
+ encryptionKey: selected.key.body,
2286
+ grant,
2287
+ kind: "subscriber-subscription-subscribe",
2288
+ operationId,
2289
+ publisher: options.reader.publisher,
2290
+ requestFingerprint,
2291
+ selectedKey: HashZod4.parse(selected.key.objectHash),
2292
+ selectedService: HashZod4.parse(selected.service.objectHash),
2293
+ service: selected.service.body,
2294
+ source: subscriber,
2295
+ subscription,
2296
+ version: 1
2297
+ });
2298
+ return await publishSubscriberSubscriptionIntent(resolvedIntent.intent, resolvedIntent.disposition);
2299
+ },
2300
+ async subscriptions(subscriptionId) {
2301
+ return await lookup(subscriptionId);
2302
+ }
2303
+ };
2304
+ return Object.freeze(client);
2305
+ }
2306
+
372
2307
  // src/sqliteWakeQueue.ts
373
2308
  import { mkdir as mkdir5 } from "node:fs/promises";
374
- import PATH5 from "node:path";
2309
+ import PATH12 from "node:path";
375
2310
  import { DatabaseSync } from "node:sqlite";
376
2311
  async function createSqliteWakeQueue(options) {
377
2312
  const inMemory = options.path === ":memory:";
378
- const file = inMemory ? options.path : PATH5.resolve(options.path);
379
- if (!inMemory) await mkdir5(PATH5.dirname(file), { recursive: true });
2313
+ const file = inMemory ? options.path : PATH12.resolve(options.path);
2314
+ if (!inMemory) await mkdir5(PATH12.dirname(file), { recursive: true });
380
2315
  const database = new DatabaseSync(file);
381
2316
  database.exec(`
382
2317
  PRAGMA journal_mode = WAL;
@@ -437,10 +2372,29 @@ async function createSqliteWakeQueue(options) {
437
2372
  };
438
2373
  }
439
2374
  export {
2375
+ EventKitHandshakeClientError,
2376
+ HandshakeMutationIntentConflictError,
2377
+ HandshakeMutationIntentKinds,
2378
+ HandshakeMutationIntentZod,
2379
+ assertSameIntentBinding,
2380
+ createEventKitHandshakePublicationPort,
440
2381
  createFilesystemCursorStore,
2382
+ createFilesystemEventKitReplayCheckpointStore,
2383
+ createFilesystemFinalizedStatementPublicationJournal,
2384
+ createFilesystemHandshakeMutationIntentStore,
2385
+ createFilesystemPublisherEncryptionKeyStore,
2386
+ createFilesystemPublisherEndpointChallengeStore,
441
2387
  createFilesystemPublisherOutbox,
442
2388
  createFilesystemWakeInbox,
443
2389
  createFilesystemWakeQueue,
444
- createSqliteWakeQueue
2390
+ createMemoryHandshakeMutationIntentStore,
2391
+ createPublisherHandshakeClient,
2392
+ createSqliteWakeQueue,
2393
+ createSubscriberHandshakeClient,
2394
+ createSynchronizedEventKitHandshakeReader,
2395
+ handshakeMutationRequestFingerprint,
2396
+ intentLookupKey,
2397
+ parseHandshakeMutationIntent,
2398
+ parseHandshakeMutationIntentLookup
445
2399
  };
446
2400
  //# sourceMappingURL=index.mjs.map