@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.
- package/README.md +37 -3
- package/dist/node/filesystemDurability.d.ts +11 -0
- package/dist/node/filesystemDurability.d.ts.map +1 -0
- package/dist/node/filesystemEventKitReplayCheckpointStore.d.ts +8 -0
- package/dist/node/filesystemEventKitReplayCheckpointStore.d.ts.map +1 -0
- package/dist/node/filesystemFinalizedStatementPublicationJournal.d.ts +14 -0
- package/dist/node/filesystemFinalizedStatementPublicationJournal.d.ts.map +1 -0
- package/dist/node/filesystemHandshakeMutationIntentStore.d.ts +12 -0
- package/dist/node/filesystemHandshakeMutationIntentStore.d.ts.map +1 -0
- package/dist/node/filesystemJson.d.ts +14 -0
- package/dist/node/filesystemJson.d.ts.map +1 -0
- package/dist/node/filesystemPublisherEncryptionKeyStore.d.ts +15 -0
- package/dist/node/filesystemPublisherEncryptionKeyStore.d.ts.map +1 -0
- package/dist/node/filesystemPublisherEndpointChallengeStore.d.ts +12 -0
- package/dist/node/filesystemPublisherEndpointChallengeStore.d.ts.map +1 -0
- package/dist/node/filesystemWakeQueue.d.ts +2 -8
- package/dist/node/filesystemWakeQueue.d.ts.map +1 -1
- package/dist/node/handshake.d.ts +16 -0
- package/dist/node/handshake.d.ts.map +1 -0
- package/dist/node/handshake.mjs +1977 -0
- package/dist/node/handshake.mjs.map +7 -0
- package/dist/node/handshakeClients.d.ts +287 -0
- package/dist/node/handshakeClients.d.ts.map +1 -0
- package/dist/node/handshakeMutationIntent.d.ts +606 -0
- package/dist/node/handshakeMutationIntent.d.ts.map +1 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.mjs +2015 -61
- package/dist/node/index.mjs.map +4 -4
- package/package.json +36 -18
package/dist/node/index.mjs
CHANGED
|
@@ -40,46 +40,92 @@ import {
|
|
|
40
40
|
rename,
|
|
41
41
|
unlink as unlink2
|
|
42
42
|
} from "node:fs/promises";
|
|
43
|
-
import
|
|
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
|
-
|
|
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 =
|
|
57
|
-
const pendingDir =
|
|
58
|
-
const inflightDir =
|
|
59
|
-
const tempDir =
|
|
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
|
|
63
|
-
await
|
|
64
|
-
await
|
|
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(
|
|
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(
|
|
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 =
|
|
124
|
+
const tempPath = PATH3.join(tempDir, name);
|
|
79
125
|
await writeDurableFile(tempPath, `${JSON.stringify(wake)}
|
|
80
126
|
`);
|
|
81
127
|
try {
|
|
82
|
-
await link(tempPath,
|
|
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 =
|
|
93
|
-
const pendingPath =
|
|
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
|
|
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
|
|
202
|
+
return errorCode2(error) === "ENOENT";
|
|
177
203
|
}
|
|
178
204
|
function isAlreadyExists(error) {
|
|
179
|
-
return
|
|
205
|
+
return errorCode2(error) === "EEXIST";
|
|
180
206
|
}
|
|
181
|
-
function
|
|
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 =
|
|
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(
|
|
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
|
|
251
|
+
open as open3,
|
|
226
252
|
readdir as readdir2,
|
|
227
253
|
unlink as unlink3
|
|
228
254
|
} from "node:fs/promises";
|
|
229
|
-
import
|
|
255
|
+
import PATH5 from "node:path";
|
|
230
256
|
function createFilesystemWakeInbox(options) {
|
|
231
|
-
const root =
|
|
232
|
-
const tokensDir =
|
|
233
|
-
const wakesDir =
|
|
234
|
-
const tempDir =
|
|
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
|
|
273
|
+
return PATH5.join(tokensDir, `${digest([attempt.audience, attempt.publisher, attempt.jwtId])}.json`);
|
|
248
274
|
}
|
|
249
275
|
function wakePath(attempt) {
|
|
250
|
-
return
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
389
|
+
return errorCode3(error) === "ENOENT";
|
|
364
390
|
}
|
|
365
391
|
function isAlreadyExists2(error) {
|
|
366
|
-
return
|
|
392
|
+
return errorCode3(error) === "EEXIST";
|
|
367
393
|
}
|
|
368
|
-
function
|
|
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
|
|
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 :
|
|
379
|
-
if (!inMemory) await mkdir5(
|
|
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
|
-
|
|
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
|