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