@xyo-network/event-kit-node 1.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -34
- package/dist/node/bestEffortSnapshotWorker.d.ts +44 -0
- package/dist/node/bestEffortSnapshotWorker.d.ts.map +1 -0
- package/dist/node/bestEffortTypes.d.ts +55 -0
- package/dist/node/bestEffortTypes.d.ts.map +1 -0
- package/dist/node/filesystemCursorStore.d.ts.map +1 -1
- package/dist/node/filesystemJson.d.ts.map +1 -1
- package/dist/node/filesystemPublisherOutbox.d.ts.map +1 -1
- package/dist/node/filesystemWakeInbox.d.ts +3 -20
- package/dist/node/filesystemWakeInbox.d.ts.map +1 -1
- package/dist/node/filesystemWakeQueue.d.ts +10 -19
- package/dist/node/filesystemWakeQueue.d.ts.map +1 -1
- package/dist/node/handshake.mjs +24 -11
- package/dist/node/handshake.mjs.map +2 -2
- package/dist/node/handshakeClients.d.ts +5 -1
- package/dist/node/handshakeClients.d.ts.map +1 -1
- package/dist/node/handshakeMutationIntent.d.ts +128 -66
- package/dist/node/handshakeMutationIntent.d.ts.map +1 -1
- package/dist/node/index.d.ts +8 -1
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.mjs +2190 -706
- package/dist/node/index.mjs.map +4 -4
- package/dist/node/privateSqlite.d.ts +19 -0
- package/dist/node/privateSqlite.d.ts.map +1 -0
- package/dist/node/receiverAuthority.d.ts +102 -0
- package/dist/node/receiverAuthority.d.ts.map +1 -0
- package/dist/node/receiverMigration.d.ts +16 -0
- package/dist/node/receiverMigration.d.ts.map +1 -0
- package/dist/node/receiverMigrationInventory.d.ts +6 -0
- package/dist/node/receiverMigrationInventory.d.ts.map +1 -0
- package/dist/node/receiverMigrationSnapshot.d.ts +6 -0
- package/dist/node/receiverMigrationSnapshot.d.ts.map +1 -0
- package/dist/node/receiverMigrationTypes.d.ts +113 -0
- package/dist/node/receiverMigrationTypes.d.ts.map +1 -0
- package/dist/node/runtime/handshakeDiscovery.d.ts +4 -0
- package/dist/node/runtime/handshakeDiscovery.d.ts.map +1 -1
- package/dist/node/runtime/publisherDeliveryState.d.ts +107 -0
- package/dist/node/runtime/publisherDeliveryState.d.ts.map +1 -0
- package/dist/node/runtime/publisherRuntime.d.ts +17 -1
- package/dist/node/runtime/publisherRuntime.d.ts.map +1 -1
- package/dist/node/runtime/sourceSigner.d.ts +1 -0
- package/dist/node/runtime/sourceSigner.d.ts.map +1 -1
- package/dist/node/runtime/sqliteHandshakeStores.d.ts +3 -1
- package/dist/node/runtime/sqliteHandshakeStores.d.ts.map +1 -1
- package/dist/node/runtime.mjs +265 -93
- package/dist/node/runtime.mjs.map +4 -4
- package/dist/node/sqliteBestEffortInbox.d.ts +20 -0
- package/dist/node/sqliteBestEffortInbox.d.ts.map +1 -0
- package/dist/node/sqliteWakeQueue.d.ts +83 -17
- package/dist/node/sqliteWakeQueue.d.ts.map +1 -1
- package/package.json +13 -13
package/dist/node/index.mjs
CHANGED
|
@@ -9,60 +9,93 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
9
9
|
return result;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// src/
|
|
12
|
+
// src/bestEffortSnapshotWorker.ts
|
|
13
|
+
import { canonicalJson, wakeSourcesEqual } from "@xyo-network/event-kit-protocol";
|
|
13
14
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from "
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
HashZod,
|
|
16
|
+
NonnegativeJsonSafeIntegerZod,
|
|
17
|
+
WakeSourceZod
|
|
18
|
+
} from "@xyo-network/event-kit-schemas";
|
|
19
|
+
function createBestEffortSnapshotWorker(options) {
|
|
20
|
+
if (!Number.isSafeInteger(options.maxSnapshotBytes) || options.maxSnapshotBytes < 1)
|
|
21
|
+
throw new TypeError("Invalid bounded snapshot byte limit");
|
|
22
|
+
const nowSeconds = options.nowSeconds ?? (() => Math.floor(Date.now() / 1e3));
|
|
23
|
+
const leaseSeconds = options.leaseSeconds ?? 60;
|
|
24
|
+
if (!Number.isSafeInteger(leaseSeconds) || leaseSeconds < 1)
|
|
25
|
+
throw new TypeError("Invalid Best Effort worker lease lifetime");
|
|
26
|
+
async function processSnapshot(lease, signal) {
|
|
27
|
+
const source = lease.binding.source;
|
|
28
|
+
const observed = await options.source.readCurrent({
|
|
29
|
+
source,
|
|
30
|
+
signal,
|
|
31
|
+
maxBytes: options.maxSnapshotBytes
|
|
32
|
+
});
|
|
33
|
+
signal.throwIfAborted();
|
|
34
|
+
const snapshot = await options.source.verifySnapshot({
|
|
35
|
+
source,
|
|
36
|
+
signal,
|
|
37
|
+
snapshot: observed
|
|
38
|
+
});
|
|
39
|
+
signal.throwIfAborted();
|
|
40
|
+
WakeSourceZod.parse(snapshot.source);
|
|
41
|
+
NonnegativeJsonSafeIntegerZod.parse(snapshot.position);
|
|
42
|
+
if (snapshot.hash !== void 0) HashZod.parse(snapshot.hash);
|
|
43
|
+
if (!wakeSourcesEqual(source, snapshot.source) || source.kind !== "utc-interval" && snapshot.hash === void 0)
|
|
44
|
+
throw new TypeError("Independent snapshot does not match its pinned source");
|
|
45
|
+
if (Buffer.byteLength(canonicalJson(snapshot.value), "utf8") > options.maxSnapshotBytes)
|
|
46
|
+
throw new RangeError("Independent snapshot exceeds its configured byte bound");
|
|
47
|
+
const previous = lease.previousSnapshot;
|
|
48
|
+
if (previous !== void 0 && (snapshot.position < previous.position || snapshot.position === previous.position && snapshot.hash !== previous.hash))
|
|
49
|
+
throw new TypeError("Independent snapshot regressed or changed at the committed position");
|
|
50
|
+
if (!await lease.isCurrent()) return "superseded";
|
|
51
|
+
await options.application.commitSnapshot({ lease, snapshot });
|
|
52
|
+
signal.throwIfAborted();
|
|
53
|
+
return await lease.complete(snapshot) ? "completed" : "superseded";
|
|
54
|
+
}
|
|
21
55
|
return {
|
|
22
|
-
async
|
|
56
|
+
async runOnce(inputSignal) {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const signal = inputSignal ?? controller.signal;
|
|
59
|
+
signal.throwIfAborted();
|
|
60
|
+
const now = nowSeconds();
|
|
61
|
+
await options.inbox.recoverExpired(now);
|
|
62
|
+
const lease = await options.inbox.claim({ nowSeconds: now, leaseSeconds });
|
|
63
|
+
if (lease === void 0) return "idle";
|
|
64
|
+
let failed = false;
|
|
65
|
+
let failure;
|
|
66
|
+
let releaseFailed = false;
|
|
67
|
+
let releaseFailure;
|
|
68
|
+
let result = "superseded";
|
|
23
69
|
try {
|
|
24
|
-
|
|
25
|
-
const parsed = JSON.parse(raw);
|
|
26
|
-
return typeof parsed.lastProcessedBlock !== "number" || !Number.isFinite(parsed.lastProcessedBlock) ? void 0 : parsed.lastProcessedBlock;
|
|
70
|
+
result = await processSnapshot(lease, signal);
|
|
27
71
|
} catch (error) {
|
|
28
|
-
|
|
29
|
-
|
|
72
|
+
failed = true;
|
|
73
|
+
failure = error;
|
|
30
74
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
75
|
+
try {
|
|
76
|
+
await lease.release();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
releaseFailed = true;
|
|
79
|
+
releaseFailure = error;
|
|
80
|
+
}
|
|
81
|
+
if (failed && releaseFailed)
|
|
82
|
+
throw new AggregateError([failure, releaseFailure], "Best Effort snapshot work failed and its lease release also failed", { cause: failure });
|
|
83
|
+
if (failed) throw failure;
|
|
84
|
+
if (releaseFailed) throw releaseFailure;
|
|
85
|
+
return result;
|
|
37
86
|
}
|
|
38
87
|
};
|
|
39
88
|
}
|
|
40
|
-
function isNotFound(error) {
|
|
41
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
42
|
-
}
|
|
43
89
|
|
|
44
|
-
// src/
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
mkdir as mkdir3,
|
|
48
|
-
rename,
|
|
49
|
-
unlink as unlink2
|
|
50
|
-
} from "node:fs/promises";
|
|
90
|
+
// src/filesystemCursorStore.ts
|
|
91
|
+
import { lstat } from "node:fs/promises";
|
|
51
92
|
import PATH4 from "node:path";
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
import { randomBytes } from "node:crypto";
|
|
55
|
-
import {
|
|
56
|
-
link,
|
|
57
|
-
open as open2,
|
|
58
|
-
readdir,
|
|
59
|
-
unlink
|
|
60
|
-
} from "node:fs/promises";
|
|
61
|
-
import PATH3 from "node:path";
|
|
93
|
+
import { NonnegativeJsonSafeIntegerZod as NonnegativeJsonSafeIntegerZod2 } from "@xyo-network/event-kit-schemas";
|
|
94
|
+
import { z } from "zod";
|
|
62
95
|
|
|
63
96
|
// src/filesystemDurability.ts
|
|
64
|
-
import { mkdir
|
|
65
|
-
import
|
|
97
|
+
import { mkdir, open } from "node:fs/promises";
|
|
98
|
+
import PATH from "node:path";
|
|
66
99
|
async function writeDurableFile(path, body, mode) {
|
|
67
100
|
const handle = await open(path, "w", mode);
|
|
68
101
|
try {
|
|
@@ -73,15 +106,15 @@ async function writeDurableFile(path, body, mode) {
|
|
|
73
106
|
}
|
|
74
107
|
}
|
|
75
108
|
async function ensureDurableDirectory(path, mode) {
|
|
76
|
-
const directory =
|
|
77
|
-
const firstCreated = await
|
|
109
|
+
const directory = PATH.resolve(path);
|
|
110
|
+
const firstCreated = await mkdir(directory, { mode, recursive: true });
|
|
78
111
|
if (firstCreated === void 0) return;
|
|
79
|
-
const existingParent =
|
|
112
|
+
const existingParent = PATH.dirname(PATH.resolve(firstCreated));
|
|
80
113
|
let cursor = directory;
|
|
81
114
|
while (true) {
|
|
82
115
|
await syncDirectory(cursor);
|
|
83
116
|
if (cursor === existingParent) return;
|
|
84
|
-
const parent =
|
|
117
|
+
const parent = PATH.dirname(cursor);
|
|
85
118
|
if (parent === cursor) throw new Error("Created directory is not below its reported existing parent");
|
|
86
119
|
cursor = parent;
|
|
87
120
|
}
|
|
@@ -105,140 +138,299 @@ function errorCode(error) {
|
|
|
105
138
|
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
106
139
|
}
|
|
107
140
|
|
|
108
|
-
// src/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
141
|
+
// src/filesystemJson.ts
|
|
142
|
+
import { randomBytes } from "node:crypto";
|
|
143
|
+
import {
|
|
144
|
+
link,
|
|
145
|
+
open as open2,
|
|
146
|
+
rename,
|
|
147
|
+
unlink
|
|
148
|
+
} from "node:fs/promises";
|
|
149
|
+
import PATH2 from "node:path";
|
|
150
|
+
async function readJsonFileStrict(path) {
|
|
151
|
+
let handle;
|
|
152
|
+
try {
|
|
153
|
+
handle = await open2(path, "r");
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (errorCode2(error) === "ENOENT") return void 0;
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(await handle.readFile("utf8"), reviveJson);
|
|
160
|
+
} finally {
|
|
161
|
+
await handle.close();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function replaceJsonFile(path, value, options = {}) {
|
|
165
|
+
const directory = PATH2.dirname(path);
|
|
166
|
+
await ensureDurableDirectory(directory, options.directoryMode);
|
|
167
|
+
const tempPath = PATH2.join(directory, `.${PATH2.basename(path)}.${process.pid}.${suffix()}.tmp`);
|
|
168
|
+
try {
|
|
169
|
+
await writeDurableFile(tempPath, `${stringifyJson(value)}
|
|
170
|
+
`, options.mode);
|
|
171
|
+
await rename(tempPath, path);
|
|
172
|
+
await syncDirectory(directory);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
await unlinkIfMissing(tempPath);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function publishJsonFile(path, value, options = {}) {
|
|
179
|
+
const directory = PATH2.dirname(path);
|
|
180
|
+
await ensureDurableDirectory(directory, options.directoryMode);
|
|
181
|
+
const tempPath = PATH2.join(directory, `.${PATH2.basename(path)}.${process.pid}.${suffix()}.tmp`);
|
|
182
|
+
try {
|
|
183
|
+
await writeDurableFile(tempPath, `${stringifyJson(value)}
|
|
184
|
+
`, options.mode);
|
|
185
|
+
await link(tempPath, path);
|
|
186
|
+
await syncDirectory(directory);
|
|
187
|
+
return true;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (errorCode2(error) === "EEXIST") return false;
|
|
190
|
+
throw error;
|
|
191
|
+
} finally {
|
|
192
|
+
await unlinkIfMissing(tempPath);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function suffix() {
|
|
196
|
+
return randomBytes(8).toString("hex");
|
|
197
|
+
}
|
|
198
|
+
function stringifyJson(value) {
|
|
199
|
+
return JSON.stringify(value, replaceJsonValue);
|
|
200
|
+
}
|
|
201
|
+
function replaceJsonValue(_key, candidate) {
|
|
202
|
+
return typeof candidate === "bigint" ? { $eventKitBigInt: candidate.toString() } : candidate;
|
|
203
|
+
}
|
|
204
|
+
function reviveJson(_key, candidate) {
|
|
205
|
+
return 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) ? BigInt(candidate.$eventKitBigInt) : candidate;
|
|
206
|
+
}
|
|
207
|
+
async function unlinkIfMissing(path) {
|
|
208
|
+
try {
|
|
209
|
+
await unlink(path);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (errorCode2(error) !== "ENOENT") throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function errorCode2(error) {
|
|
215
|
+
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/privateSqlite.ts
|
|
219
|
+
import {
|
|
220
|
+
closeSync,
|
|
221
|
+
constants,
|
|
222
|
+
fstatSync,
|
|
223
|
+
fsyncSync,
|
|
224
|
+
lstatSync,
|
|
225
|
+
mkdirSync,
|
|
226
|
+
openSync
|
|
227
|
+
} from "node:fs";
|
|
228
|
+
import PATH3 from "node:path";
|
|
229
|
+
import { DatabaseSync } from "node:sqlite";
|
|
230
|
+
var LegacyReceiverStoreError = class extends Error {
|
|
231
|
+
name = "LegacyReceiverStoreError";
|
|
232
|
+
constructor(path) {
|
|
233
|
+
super(`Receiver store requires explicit offline migration: ${path}`);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
function ensurePrivateDirectory(path) {
|
|
237
|
+
const created = mkdirSync(path, { recursive: true, mode: 448 });
|
|
238
|
+
const info = lstatSync(path);
|
|
239
|
+
if (!info.isDirectory() || info.isSymbolicLink() || (info.mode & 63) !== 0 || process.getuid !== void 0 && info.uid !== process.getuid()) {
|
|
240
|
+
throw new Error(`Receiver directory must be owned and private (0700): ${path}`);
|
|
241
|
+
}
|
|
242
|
+
if (created === void 0) return;
|
|
243
|
+
const existingParent = PATH3.dirname(PATH3.resolve(created));
|
|
244
|
+
let cursor = PATH3.resolve(path);
|
|
245
|
+
for (; ; ) {
|
|
246
|
+
syncPrivateDirectory(cursor);
|
|
247
|
+
if (cursor === existingParent) break;
|
|
248
|
+
cursor = PATH3.dirname(cursor);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function syncPrivateDirectory(path) {
|
|
252
|
+
let descriptor;
|
|
253
|
+
try {
|
|
254
|
+
descriptor = openSync(path, "r");
|
|
255
|
+
fsyncSync(descriptor);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
258
|
+
if (!["EISDIR", "EINVAL", "ENOTSUP", "EOPNOTSUPP"].includes(String(code))) throw error;
|
|
259
|
+
} finally {
|
|
260
|
+
if (descriptor !== void 0) closeSync(descriptor);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function checkPrivateStoreFile(path, create = false) {
|
|
264
|
+
let descriptor;
|
|
265
|
+
try {
|
|
266
|
+
descriptor = openSync(path, (create ? constants.O_RDWR : constants.O_RDONLY) | constants.O_NOFOLLOW | (create ? constants.O_CREAT : 0), 384);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (!create && typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const info = fstatSync(descriptor);
|
|
273
|
+
if (!info.isFile() || (info.mode & 63) !== 0 || process.getuid !== void 0 && info.uid !== process.getuid()) {
|
|
274
|
+
throw new Error(`Receiver file must be owned and private (0600): ${path}`);
|
|
125
275
|
}
|
|
126
|
-
|
|
276
|
+
} finally {
|
|
277
|
+
closeSync(descriptor);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function initializeDatabase(database, file, format) {
|
|
281
|
+
database.exec("PRAGMA busy_timeout=100");
|
|
282
|
+
const tables = database.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
|
|
283
|
+
if (tables.length > 0 && tables.every((row) => row.name !== "eventkit_store")) throw new LegacyReceiverStoreError(file);
|
|
284
|
+
if (tables.some((row) => row.name === "eventkit_store")) {
|
|
285
|
+
const columns = database.prepare("PRAGMA table_info(eventkit_store)").all();
|
|
286
|
+
if (columns.every((row) => row.name !== "initialized")) throw new LegacyReceiverStoreError(file);
|
|
287
|
+
const priorFormat = database.prepare("SELECT format FROM eventkit_store WHERE id=1").get();
|
|
288
|
+
if (priorFormat?.format !== format) throw new Error("Receiver store format/namespace mismatch");
|
|
289
|
+
}
|
|
290
|
+
database.exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON;");
|
|
291
|
+
database.exec("BEGIN IMMEDIATE");
|
|
292
|
+
try {
|
|
293
|
+
database.exec("CREATE TABLE IF NOT EXISTS eventkit_store (id INTEGER PRIMARY KEY CHECK(id=1), format TEXT NOT NULL, initialized INTEGER NOT NULL DEFAULT 0)");
|
|
294
|
+
const prior = database.prepare("SELECT format FROM eventkit_store WHERE id=1").get();
|
|
295
|
+
if (prior !== void 0 && prior.format !== format) throw new Error("Receiver store format/namespace mismatch");
|
|
296
|
+
database.prepare("INSERT OR IGNORE INTO eventkit_store(id,format) VALUES(1,?)").run(format);
|
|
297
|
+
database.exec("COMMIT");
|
|
298
|
+
} catch (error) {
|
|
299
|
+
database.exec("ROLLBACK");
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function openPrivateSqlite(options) {
|
|
304
|
+
const file = options.path === ":memory:" ? options.path : PATH3.resolve(options.path);
|
|
305
|
+
if (file !== ":memory:") {
|
|
306
|
+
ensurePrivateDirectory(PATH3.dirname(file));
|
|
307
|
+
checkPrivateStoreFile(file, true);
|
|
308
|
+
checkPrivateStoreFile(`${file}-wal`);
|
|
309
|
+
checkPrivateStoreFile(`${file}-shm`);
|
|
310
|
+
checkPrivateStoreFile(`${file}-journal`);
|
|
311
|
+
}
|
|
312
|
+
const database = new DatabaseSync(file);
|
|
313
|
+
let closed = false;
|
|
314
|
+
try {
|
|
315
|
+
initializeDatabase(database, file, options.format);
|
|
316
|
+
if (file !== ":memory:") {
|
|
317
|
+
checkPrivateStoreFile(file);
|
|
318
|
+
checkPrivateStoreFile(`${file}-wal`);
|
|
319
|
+
checkPrivateStoreFile(`${file}-shm`);
|
|
320
|
+
syncPrivateDirectory(PATH3.dirname(file));
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
database.close();
|
|
324
|
+
throw error;
|
|
127
325
|
}
|
|
128
326
|
return {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
await writeDurableFile(tempPath, `${JSON.stringify(wake)}
|
|
134
|
-
`);
|
|
327
|
+
database,
|
|
328
|
+
transaction(work) {
|
|
329
|
+
if (closed) throw new Error("Receiver store is closed");
|
|
330
|
+
database.exec("BEGIN IMMEDIATE");
|
|
135
331
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
await unlinkIfMissing(tempPath);
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
async claim() {
|
|
143
|
-
await ensureDirs();
|
|
144
|
-
const names = (await readdir(pendingDir)).toSorted();
|
|
145
|
-
for (const name of names) {
|
|
146
|
-
const inflightPath = PATH3.join(inflightDir, name);
|
|
147
|
-
const pendingPath = PATH3.join(pendingDir, name);
|
|
148
|
-
try {
|
|
149
|
-
await link(pendingPath, inflightPath);
|
|
150
|
-
} catch (error) {
|
|
151
|
-
if (isAlreadyExists(error) || isNotFound2(error)) continue;
|
|
152
|
-
throw error;
|
|
332
|
+
const result = work();
|
|
333
|
+
if (result !== null && (typeof result === "object" || typeof result === "function") && "then" in result) {
|
|
334
|
+
throw new TypeError("Receiver transactions must be synchronous");
|
|
153
335
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
wake,
|
|
162
|
-
async ack() {
|
|
163
|
-
await unlinkIfMissing(inflightPath);
|
|
164
|
-
},
|
|
165
|
-
async release() {
|
|
166
|
-
await returnToPending(name);
|
|
167
|
-
}
|
|
168
|
-
};
|
|
336
|
+
database.exec("COMMIT");
|
|
337
|
+
return result;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
database.exec("ROLLBACK");
|
|
340
|
+
throw error;
|
|
169
341
|
}
|
|
170
|
-
return void 0;
|
|
171
342
|
},
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
async recoverInflight() {
|
|
177
|
-
await ensureDirs();
|
|
178
|
-
const names = await readdir(inflightDir);
|
|
179
|
-
let recovered = 0;
|
|
180
|
-
for (const name of names) {
|
|
181
|
-
await returnToPending(name);
|
|
182
|
-
recovered += 1;
|
|
183
|
-
}
|
|
184
|
-
return recovered;
|
|
343
|
+
close() {
|
|
344
|
+
if (closed) return;
|
|
345
|
+
database.close();
|
|
346
|
+
closed = true;
|
|
185
347
|
}
|
|
186
348
|
};
|
|
187
349
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
350
|
+
|
|
351
|
+
// src/filesystemCursorStore.ts
|
|
352
|
+
var CursorFileZod = z.strictObject({ lastProcessedBlock: NonnegativeJsonSafeIntegerZod2 });
|
|
353
|
+
function createFilesystemCursorStore(options) {
|
|
354
|
+
const filePath = PATH4.resolve(options.path);
|
|
355
|
+
const directory = PATH4.dirname(filePath);
|
|
356
|
+
return {
|
|
357
|
+
async load() {
|
|
358
|
+
if (!await existingPrivateDirectory(directory)) return void 0;
|
|
359
|
+
checkPrivateStoreFile(filePath);
|
|
360
|
+
const raw = await readJsonFileStrict(filePath);
|
|
361
|
+
if (raw === void 0) return void 0;
|
|
362
|
+
const cursor = CursorFileZod.parse(raw);
|
|
363
|
+
await syncDirectory(directory);
|
|
364
|
+
return cursor.lastProcessedBlock;
|
|
365
|
+
},
|
|
366
|
+
async save(lastProcessedBlock) {
|
|
367
|
+
const body = CursorFileZod.parse({ lastProcessedBlock });
|
|
368
|
+
await ensureDurableDirectory(directory, 448);
|
|
369
|
+
ensurePrivateDirectory(directory);
|
|
370
|
+
checkPrivateStoreFile(filePath);
|
|
371
|
+
await replaceJsonFile(filePath, body, { mode: 384, directoryMode: 448 });
|
|
196
372
|
}
|
|
197
|
-
}
|
|
198
|
-
return void 0;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
function entryName(wake, sequence) {
|
|
202
|
-
const stamp = String(Date.now()).padStart(15, "0");
|
|
203
|
-
const ordinal = String(sequence).padStart(6, "0");
|
|
204
|
-
return `${stamp}-${ordinal}-${randomBytes(6).toString("hex")}-${wake.wakeId}.json`;
|
|
373
|
+
};
|
|
205
374
|
}
|
|
206
|
-
async function
|
|
375
|
+
async function existingPrivateDirectory(directory) {
|
|
207
376
|
try {
|
|
208
|
-
await
|
|
377
|
+
await lstat(directory);
|
|
209
378
|
} catch (error) {
|
|
210
|
-
if (
|
|
379
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return false;
|
|
380
|
+
throw error;
|
|
211
381
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return errorCode2(error) === "ENOENT";
|
|
215
|
-
}
|
|
216
|
-
function isAlreadyExists(error) {
|
|
217
|
-
return errorCode2(error) === "EEXIST";
|
|
218
|
-
}
|
|
219
|
-
function errorCode2(error) {
|
|
220
|
-
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
382
|
+
ensurePrivateDirectory(directory);
|
|
383
|
+
return true;
|
|
221
384
|
}
|
|
222
385
|
|
|
223
386
|
// src/filesystemPublisherOutbox.ts
|
|
387
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
388
|
+
import {
|
|
389
|
+
lstat as lstat2,
|
|
390
|
+
rename as rename2,
|
|
391
|
+
unlink as unlink2
|
|
392
|
+
} from "node:fs/promises";
|
|
393
|
+
import PATH5 from "node:path";
|
|
394
|
+
import { WakeEnvelopeSchema, WakeEnvelopeZod } from "@xyo-network/event-kit-schemas";
|
|
224
395
|
function createFilesystemPublisherOutbox(options) {
|
|
225
|
-
const filePath =
|
|
396
|
+
const filePath = PATH5.resolve(options.path);
|
|
397
|
+
const directory = PATH5.dirname(filePath);
|
|
226
398
|
return {
|
|
227
399
|
async clear() {
|
|
400
|
+
if (!await existingPrivateDirectory2(directory)) return;
|
|
401
|
+
checkPrivateStoreFile(filePath);
|
|
402
|
+
let removed = false;
|
|
228
403
|
try {
|
|
229
404
|
await unlink2(filePath);
|
|
405
|
+
removed = true;
|
|
230
406
|
} catch (error) {
|
|
231
|
-
if (!
|
|
407
|
+
if (!isNotFound(error)) throw error;
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
await syncDirectory(directory);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
if (removed || !isNotFound(error)) throw error;
|
|
232
413
|
}
|
|
233
414
|
},
|
|
234
|
-
|
|
415
|
+
async load() {
|
|
416
|
+
if (!await existingPrivateDirectory2(directory)) return void 0;
|
|
417
|
+
checkPrivateStoreFile(filePath);
|
|
418
|
+
const raw = await readJsonFileStrict(filePath);
|
|
419
|
+
if (raw === void 0) return void 0;
|
|
420
|
+
const pending = validatePendingWake(raw);
|
|
421
|
+
await syncDirectory(directory);
|
|
422
|
+
return pending;
|
|
423
|
+
},
|
|
235
424
|
async save(pending) {
|
|
236
|
-
|
|
425
|
+
validatePendingWake(pending);
|
|
426
|
+
await ensureDurableDirectory(directory, 448);
|
|
427
|
+
ensurePrivateDirectory(directory);
|
|
428
|
+
checkPrivateStoreFile(filePath);
|
|
237
429
|
const tempPath = `${filePath}.${process.pid}.${randomBytes2(6).toString("hex")}.tmp`;
|
|
238
|
-
await writeDurableFile(tempPath, `${JSON.stringify(pending)}
|
|
239
|
-
`);
|
|
240
430
|
try {
|
|
241
|
-
await
|
|
431
|
+
await writeDurableFile(tempPath, `${JSON.stringify(pending)}
|
|
432
|
+
`, 384);
|
|
433
|
+
await rename2(tempPath, filePath);
|
|
242
434
|
} catch (error) {
|
|
243
435
|
try {
|
|
244
436
|
await unlink2(tempPath);
|
|
@@ -246,254 +438,526 @@ function createFilesystemPublisherOutbox(options) {
|
|
|
246
438
|
}
|
|
247
439
|
throw error;
|
|
248
440
|
}
|
|
441
|
+
await syncDirectory(directory);
|
|
249
442
|
}
|
|
250
443
|
};
|
|
251
444
|
}
|
|
252
|
-
function
|
|
445
|
+
function validatePendingWake(raw) {
|
|
446
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw) || "schema" in raw || "deliveryMode" in raw) {
|
|
447
|
+
throw new Error("Invalid pending wake record");
|
|
448
|
+
}
|
|
449
|
+
WakeEnvelopeZod.parse({
|
|
450
|
+
...raw,
|
|
451
|
+
schema: WakeEnvelopeSchema,
|
|
452
|
+
deliveryMode: "durable"
|
|
453
|
+
});
|
|
454
|
+
return raw;
|
|
455
|
+
}
|
|
456
|
+
function isNotFound(error) {
|
|
253
457
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
254
458
|
}
|
|
459
|
+
async function existingPrivateDirectory2(directory) {
|
|
460
|
+
try {
|
|
461
|
+
await lstat2(directory);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
if (isNotFound(error)) return false;
|
|
464
|
+
throw error;
|
|
465
|
+
}
|
|
466
|
+
ensurePrivateDirectory(directory);
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
255
469
|
|
|
256
470
|
// src/filesystemWakeInbox.ts
|
|
257
|
-
import {
|
|
471
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2 } from "node:fs";
|
|
472
|
+
import PATH7 from "node:path";
|
|
473
|
+
|
|
474
|
+
// src/filesystemWakeQueue.ts
|
|
475
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
476
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
258
477
|
import {
|
|
259
478
|
link as link2,
|
|
260
|
-
mkdir as mkdir4,
|
|
261
479
|
open as open3,
|
|
262
|
-
readdir
|
|
480
|
+
readdir,
|
|
263
481
|
unlink as unlink3
|
|
264
482
|
} from "node:fs/promises";
|
|
265
|
-
import
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
483
|
+
import PATH6 from "node:path";
|
|
484
|
+
import { parseAdmittedWake as parseAdmittedWake2, StaleWakeLeaseError as StaleWakeLeaseError2 } from "@xyo-network/event-kit-protocol";
|
|
485
|
+
|
|
486
|
+
// src/receiverAuthority.ts
|
|
487
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
488
|
+
import { createHash, randomBytes as randomBytes3 } from "node:crypto";
|
|
489
|
+
import { parseAdmittedWake, StaleWakeLeaseError } from "@xyo-network/event-kit-protocol";
|
|
490
|
+
import {
|
|
491
|
+
HashZod as HashZod2,
|
|
492
|
+
NonnegativeJsonSafeIntegerZod as NonnegativeJsonSafeIntegerZod3,
|
|
493
|
+
NormalizedAddressZod,
|
|
494
|
+
WakeIdZod,
|
|
495
|
+
WakeJwtAudienceZod,
|
|
496
|
+
WakeJwtIdZod
|
|
497
|
+
} from "@xyo-network/event-kit-schemas";
|
|
498
|
+
function receiverIdentity(parts) {
|
|
499
|
+
return createHash("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
500
|
+
}
|
|
501
|
+
function validateAttempt(attempt) {
|
|
502
|
+
WakeJwtAudienceZod.parse(attempt.audience);
|
|
503
|
+
NormalizedAddressZod.parse(attempt.publisher);
|
|
504
|
+
WakeIdZod.parse(attempt.wakeId);
|
|
505
|
+
WakeJwtIdZod.parse(attempt.jwtId);
|
|
506
|
+
HashZod2.parse(attempt.eventHash);
|
|
507
|
+
NonnegativeJsonSafeIntegerZod3.parse(attempt.retainJwtUntil);
|
|
508
|
+
}
|
|
509
|
+
function boundedBody(wake) {
|
|
510
|
+
const body = JSON.stringify(wake);
|
|
511
|
+
if (Buffer2.byteLength(body, "utf8") > 65536) throw new RangeError("Receiver work exceeds its 64 KiB bound");
|
|
512
|
+
return body;
|
|
513
|
+
}
|
|
514
|
+
function sameValues(pairs) {
|
|
515
|
+
return pairs.every(([actual, expected]) => actual === expected);
|
|
516
|
+
}
|
|
517
|
+
function validateQueueState(row) {
|
|
518
|
+
if (!Number.isSafeInteger(row.id) || row.id < 1) throw new Error("Invalid persisted queue identity");
|
|
519
|
+
if (row.state === "completed") {
|
|
520
|
+
if (row.body !== null || row.lease_token !== null || row.terminal_operation !== "ack" || row.terminal_token === null || !/^[0-9a-f]{32}$/u.test(row.terminal_token)) throw new Error("Invalid persisted completion state");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (typeof row.body !== "string") throw new Error("Active receiver work has no bounded body");
|
|
524
|
+
if (row.state === "pending" && row.lease_token === null) return;
|
|
525
|
+
if (row.state === "inflight" && row.lease_token !== null && /^[0-9a-f]{32}$/u.test(row.lease_token)) return;
|
|
526
|
+
throw new Error("Invalid persisted lease state");
|
|
527
|
+
}
|
|
528
|
+
function openReceiverAuthority(path) {
|
|
529
|
+
const owner = openPrivateSqlite({ path, format: "event-kit-receiver" });
|
|
530
|
+
const database = owner.database;
|
|
531
|
+
const transaction = (work) => owner.transaction(work);
|
|
532
|
+
const requiredTables = ["receiver_queue", "receiver_wakes", "receiver_tokens"];
|
|
533
|
+
try {
|
|
534
|
+
transaction(() => {
|
|
535
|
+
const existingTables = new Set(database.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((row) => row.name));
|
|
536
|
+
const initialized = database.prepare("SELECT initialized FROM eventkit_store WHERE id=1").get()?.initialized;
|
|
537
|
+
if (initialized !== 0 && initialized !== 1 || initialized === 1 && requiredTables.some((name) => !existingTables.has(name)) || initialized === 0 && requiredTables.some((name) => existingTables.has(name))) {
|
|
538
|
+
throw new Error("Incomplete persisted receiver schema; refusing to recreate admission/completion history");
|
|
278
539
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
540
|
+
if (initialized === 1) return;
|
|
541
|
+
database.exec(`
|
|
542
|
+
CREATE TABLE IF NOT EXISTS receiver_queue (
|
|
543
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
544
|
+
body TEXT,
|
|
545
|
+
state TEXT NOT NULL CHECK(state IN ('pending','inflight','completed')),
|
|
546
|
+
lease_token TEXT,
|
|
547
|
+
terminal_token TEXT,
|
|
548
|
+
terminal_operation TEXT,
|
|
549
|
+
CHECK ((state='inflight' AND lease_token IS NOT NULL) OR (state!='inflight' AND lease_token IS NULL))
|
|
550
|
+
);
|
|
551
|
+
CREATE INDEX IF NOT EXISTS receiver_pending ON receiver_queue(state,id);
|
|
552
|
+
CREATE TABLE IF NOT EXISTS receiver_wakes (
|
|
553
|
+
identity TEXT PRIMARY KEY,
|
|
554
|
+
audience TEXT NOT NULL, publisher TEXT NOT NULL, wake_id TEXT NOT NULL,
|
|
555
|
+
event_hash TEXT NOT NULL, receipt TEXT NOT NULL, authorization TEXT NOT NULL,
|
|
556
|
+
queue_id INTEGER NOT NULL UNIQUE REFERENCES receiver_queue(id)
|
|
557
|
+
);
|
|
558
|
+
CREATE TABLE IF NOT EXISTS receiver_tokens (
|
|
559
|
+
identity TEXT PRIMARY KEY, wake_identity TEXT NOT NULL REFERENCES receiver_wakes(identity),
|
|
560
|
+
event_hash TEXT NOT NULL, wake_id TEXT NOT NULL, retain_until INTEGER NOT NULL
|
|
561
|
+
);
|
|
562
|
+
CREATE INDEX IF NOT EXISTS receiver_token_expiry ON receiver_tokens(retain_until);
|
|
563
|
+
`);
|
|
564
|
+
database.prepare("UPDATE eventkit_store SET initialized=1 WHERE id=1").run();
|
|
565
|
+
});
|
|
566
|
+
} catch (error) {
|
|
567
|
+
owner.close();
|
|
568
|
+
throw error;
|
|
283
569
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
570
|
+
const boundedRow = `id,state,lease_token,terminal_token,terminal_operation,
|
|
571
|
+
CASE WHEN length(CAST(body AS BLOB)) <= 65536 THEN body ELSE NULL END AS body`;
|
|
572
|
+
const pending = database.prepare(`SELECT ${boundedRow} FROM receiver_queue WHERE state='pending' ORDER BY id LIMIT 1`);
|
|
573
|
+
const getRow = database.prepare(`SELECT ${boundedRow} FROM receiver_queue WHERE id=?`);
|
|
574
|
+
const insert = database.prepare("INSERT INTO receiver_queue(body,state) VALUES(?,'pending')");
|
|
575
|
+
const count = database.prepare("SELECT COUNT(*) AS total FROM receiver_queue WHERE state='pending'");
|
|
576
|
+
const wakeKey = (attempt) => receiverIdentity([attempt.audience, attempt.publisher, attempt.wakeId]);
|
|
577
|
+
const tokenKey = (attempt) => receiverIdentity([attempt.audience, attempt.publisher, attempt.jwtId]);
|
|
578
|
+
function validateAssociation(row, wake) {
|
|
579
|
+
validateQueueState(row);
|
|
580
|
+
const record = database.prepare("SELECT * FROM receiver_wakes WHERE queue_id=?").get(row.id);
|
|
581
|
+
if (record === void 0) return;
|
|
582
|
+
const receipt = JSON.parse(String(record.receipt));
|
|
583
|
+
const receiptMatches = sameValues([
|
|
584
|
+
[record.identity, receiverIdentity([String(record.audience), String(record.publisher), String(record.wake_id)])],
|
|
585
|
+
[receipt.id, record.identity],
|
|
586
|
+
[receipt.audience, record.audience],
|
|
587
|
+
[receipt.publisher, record.publisher],
|
|
588
|
+
[receipt.wakeId, record.wake_id],
|
|
589
|
+
[receipt.eventHash, record.event_hash]
|
|
590
|
+
]);
|
|
591
|
+
const wakeMatches = wake === void 0 || sameValues([
|
|
592
|
+
[wake.audience, record.audience],
|
|
593
|
+
[wake.publisher, record.publisher],
|
|
594
|
+
[wake.wakeId, record.wake_id],
|
|
595
|
+
[wake.eventHash, record.event_hash]
|
|
596
|
+
]);
|
|
597
|
+
if (!receiptMatches || !wakeMatches) throw new Error("Queued body does not match its admitted identity/receipt");
|
|
288
598
|
}
|
|
289
|
-
function
|
|
290
|
-
|
|
599
|
+
async function preparedWork(attempt) {
|
|
600
|
+
const record = database.prepare("SELECT queue_id FROM receiver_wakes WHERE identity=?").get(wakeKey(attempt));
|
|
601
|
+
if (record === void 0) return void 0;
|
|
602
|
+
const row = currentRow(Number(record.queue_id));
|
|
603
|
+
if (row === void 0) throw new Error("Receiver receipt has no work/completion record");
|
|
604
|
+
const wake = row.body === null ? void 0 : await parseAdmittedWake(JSON.parse(row.body));
|
|
605
|
+
validateAssociation(row, wake);
|
|
606
|
+
return { id: row.id, body: row.body };
|
|
291
607
|
}
|
|
292
|
-
|
|
293
|
-
return PATH5.join(wakesDir, `${digest([attempt.audience, attempt.publisher, attempt.wakeId])}.json`);
|
|
608
|
+
class WorkChanged extends Error {
|
|
294
609
|
}
|
|
295
|
-
async function
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
610
|
+
async function validatedTransaction(attempt, work) {
|
|
611
|
+
for (let tries = 0; tries < 4; tries++) {
|
|
612
|
+
const prepared = await preparedWork(attempt);
|
|
613
|
+
try {
|
|
614
|
+
return transaction(() => work(prepared));
|
|
615
|
+
} catch (error) {
|
|
616
|
+
if (!(error instanceof WorkChanged)) throw error;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
throw new Error("Receiver work changed during validation; retry admission");
|
|
302
620
|
}
|
|
303
|
-
|
|
304
|
-
const token =
|
|
305
|
-
if (token !== void 0) {
|
|
306
|
-
|
|
621
|
+
function resolve(attempt, prepared) {
|
|
622
|
+
const token = database.prepare("SELECT * FROM receiver_tokens WHERE identity=?").get(tokenKey(attempt));
|
|
623
|
+
if (token !== void 0 && (token.event_hash !== attempt.eventHash || token.wake_id !== attempt.wakeId)) return { kind: "jwt-id-collision" };
|
|
624
|
+
const record = database.prepare("SELECT * FROM receiver_wakes WHERE identity=?").get(wakeKey(attempt));
|
|
625
|
+
if (record === void 0) {
|
|
626
|
+
if (token !== void 0) throw new Error("Receiver token points to missing admitted work");
|
|
627
|
+
return { kind: "new" };
|
|
628
|
+
}
|
|
629
|
+
if (record.event_hash !== attempt.eventHash) return { kind: "wake-id-collision" };
|
|
630
|
+
const receipt = JSON.parse(String(record.receipt));
|
|
631
|
+
if (!sameValues([
|
|
632
|
+
[receipt.id, wakeKey(attempt)],
|
|
633
|
+
[receipt.eventHash, attempt.eventHash],
|
|
634
|
+
[receipt.publisher, attempt.publisher],
|
|
635
|
+
[receipt.audience, attempt.audience],
|
|
636
|
+
[receipt.wakeId, attempt.wakeId]
|
|
637
|
+
])) {
|
|
638
|
+
throw new Error("Receiver receipt does not match its identity");
|
|
639
|
+
}
|
|
640
|
+
const row = currentRow(Number(record.queue_id));
|
|
641
|
+
if (row === void 0) throw new Error("Receiver receipt has no pending/completed work record");
|
|
642
|
+
validateAssociation(row);
|
|
643
|
+
if (row.state !== "completed" && (prepared?.id !== row.id || prepared.body !== row.body)) throw new WorkChanged();
|
|
644
|
+
if (token !== void 0 && (token.wake_identity !== record.identity || !Number.isSafeInteger(token.retain_until) || Number(token.retain_until) < 0)) {
|
|
645
|
+
throw new Error("Persisted token does not match its admitted work");
|
|
307
646
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
await bindToken(attempt, wake);
|
|
312
|
-
return { kind: "duplicate", receipt: wake.receipt };
|
|
647
|
+
database.prepare(`INSERT INTO receiver_tokens(identity,wake_identity,event_hash,wake_id,retain_until) VALUES(?,?,?,?,?)
|
|
648
|
+
ON CONFLICT(identity) DO UPDATE SET retain_until=MAX(retain_until,excluded.retain_until)`).run(tokenKey(attempt), wakeKey(attempt), attempt.eventHash, attempt.wakeId, attempt.retainJwtUntil);
|
|
649
|
+
return { kind: "duplicate", receipt };
|
|
313
650
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return await
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
651
|
+
const inbox = {
|
|
652
|
+
async resolveAttempt(attempt) {
|
|
653
|
+
validateAttempt(attempt);
|
|
654
|
+
return await validatedTransaction(attempt, (prepared) => resolve(attempt, prepared));
|
|
655
|
+
},
|
|
656
|
+
async commitAuthorized(input) {
|
|
657
|
+
validateAttempt(input.attempt);
|
|
658
|
+
const wake = await parseAdmittedWake(input.wake);
|
|
659
|
+
const { attempt, grant } = input;
|
|
660
|
+
if (attempt.audience !== wake.audience || attempt.publisher !== wake.publisher || attempt.wakeId !== wake.wakeId || attempt.eventHash !== wake.eventHash || attempt.jwtId !== wake.jti || grant.grant.authorizationKind !== "activated-subscription" || grant.grant.target !== wake.publisher || grant.grant.deploymentId !== wake.deploymentId || grant.grant.queueId !== wake.queueId || grant.grant.filterHash !== wake.event.filterHash || grant.grant.authorizationChainId !== wake.event.authorizationChainId || wake.event.deliveryMode !== "durable") {
|
|
661
|
+
throw new TypeError("Authorized admission does not match its exact wake/identity binding");
|
|
662
|
+
}
|
|
663
|
+
return await validatedTransaction(attempt, (prepared) => {
|
|
664
|
+
const previous = resolve(attempt, prepared);
|
|
665
|
+
if (previous.kind !== "new") return previous;
|
|
666
|
+
const receipt = Object.freeze({
|
|
667
|
+
audience: attempt.audience,
|
|
668
|
+
eventHash: attempt.eventHash,
|
|
669
|
+
id: wakeKey(attempt),
|
|
670
|
+
publisher: attempt.publisher,
|
|
671
|
+
wakeId: attempt.wakeId
|
|
672
|
+
});
|
|
673
|
+
const row = insert.run(boundedBody(wake));
|
|
674
|
+
database.prepare("INSERT INTO receiver_wakes(identity,audience,publisher,wake_id,event_hash,receipt,authorization,queue_id) VALUES(?,?,?,?,?,?,?,?)").run(
|
|
675
|
+
wakeKey(attempt),
|
|
676
|
+
attempt.audience,
|
|
677
|
+
attempt.publisher,
|
|
678
|
+
attempt.wakeId,
|
|
679
|
+
attempt.eventHash,
|
|
680
|
+
JSON.stringify(receipt),
|
|
681
|
+
JSON.stringify(grant),
|
|
682
|
+
row.lastInsertRowid
|
|
683
|
+
);
|
|
684
|
+
database.prepare("INSERT INTO receiver_tokens(identity,wake_identity,event_hash,wake_id,retain_until) VALUES(?,?,?,?,?)").run(tokenKey(attempt), wakeKey(attempt), attempt.eventHash, attempt.wakeId, attempt.retainJwtUntil);
|
|
685
|
+
return { kind: "enqueued", receipt };
|
|
329
686
|
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
function currentRow(id) {
|
|
690
|
+
return getRow.get(id);
|
|
691
|
+
}
|
|
692
|
+
function rows() {
|
|
693
|
+
return database.prepare(`SELECT ${boundedRow} FROM receiver_queue WHERE state!='completed' ORDER BY id`).all();
|
|
694
|
+
}
|
|
695
|
+
function finish(id, token, operation) {
|
|
696
|
+
transaction(() => {
|
|
697
|
+
const row = currentRow(id);
|
|
698
|
+
if (row?.terminal_token === token && row.terminal_operation === operation) return;
|
|
699
|
+
if (row?.state !== "inflight" || row.lease_token !== token) throw new StaleWakeLeaseError();
|
|
700
|
+
if (operation === "ack") database.prepare(`UPDATE receiver_queue
|
|
701
|
+
SET state='completed',body=NULL,lease_token=NULL,terminal_token=?,terminal_operation='ack'
|
|
702
|
+
WHERE id=? AND lease_token=?`).run(token, id, token);
|
|
703
|
+
else database.prepare(`UPDATE receiver_queue
|
|
704
|
+
SET state='pending',lease_token=NULL,terminal_token=?,terminal_operation='release'
|
|
705
|
+
WHERE id=? AND lease_token=?`).run(token, id, token);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
async function claim(validate) {
|
|
709
|
+
for (; ; ) {
|
|
710
|
+
const row = pending.get();
|
|
711
|
+
if (row === void 0) return void 0;
|
|
712
|
+
if (typeof row.body !== "string") throw new Error("Pending receiver work has no body");
|
|
713
|
+
const wake = await parseAdmittedWake(JSON.parse(row.body));
|
|
714
|
+
validateAssociation(row, wake);
|
|
715
|
+
await validate?.(row);
|
|
716
|
+
const token = randomBytes3(16).toString("hex");
|
|
717
|
+
const won = transaction(() => database.prepare(`UPDATE receiver_queue SET state='inflight',lease_token=?
|
|
718
|
+
WHERE id=? AND state='pending' AND body=?`).run(token, row.id, row.body).changes === 1);
|
|
719
|
+
if (!won) continue;
|
|
720
|
+
let completed;
|
|
721
|
+
const complete = (operation) => {
|
|
722
|
+
if (completed !== void 0) {
|
|
723
|
+
if (completed !== operation) throw new StaleWakeLeaseError();
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
finish(row.id, token, operation);
|
|
727
|
+
completed = operation;
|
|
336
728
|
};
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
async pruneExpiredTokens(nowSeconds) {
|
|
350
|
-
await ensureDirs();
|
|
351
|
-
let removed = 0;
|
|
352
|
-
for (const name of await readdir2(tokensDir)) {
|
|
353
|
-
const path = PATH5.join(tokensDir, name);
|
|
354
|
-
const token = await readRecord(path);
|
|
355
|
-
if (token === void 0 || token.retainJwtUntil > nowSeconds) continue;
|
|
356
|
-
await unlinkIfMissing2(path);
|
|
357
|
-
removed += 1;
|
|
358
|
-
}
|
|
359
|
-
return removed;
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
async function writeRecordExclusive(tempDir, path, record) {
|
|
364
|
-
const tempPath = PATH5.join(tempDir, `${PATH5.basename(path)}.${process.pid}.${randomSuffix()}`);
|
|
365
|
-
await writeDurableFile(tempPath, `${JSON.stringify(record)}
|
|
366
|
-
`);
|
|
367
|
-
try {
|
|
368
|
-
await link2(tempPath, path);
|
|
369
|
-
await syncDirectory(PATH5.dirname(path));
|
|
370
|
-
return true;
|
|
371
|
-
} catch (error) {
|
|
372
|
-
if (isAlreadyExists2(error)) return false;
|
|
373
|
-
throw error;
|
|
374
|
-
} finally {
|
|
375
|
-
await unlinkIfMissing2(tempPath);
|
|
729
|
+
return {
|
|
730
|
+
id: row.id,
|
|
731
|
+
token,
|
|
732
|
+
wake,
|
|
733
|
+
async ack() {
|
|
734
|
+
complete("ack");
|
|
735
|
+
},
|
|
736
|
+
async release() {
|
|
737
|
+
complete("release");
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
}
|
|
376
741
|
}
|
|
742
|
+
const queue = {
|
|
743
|
+
async append(wake) {
|
|
744
|
+
const parsed = await parseAdmittedWake(wake);
|
|
745
|
+
transaction(() => insert.run(boundedBody(parsed)));
|
|
746
|
+
},
|
|
747
|
+
claim: () => claim(),
|
|
748
|
+
pendingCount: async () => Number(count.get()?.total),
|
|
749
|
+
async recoverInflight(options) {
|
|
750
|
+
if (options?.consumersStopped !== true) throw new TypeError("Recovery requires all consumers to be stopped");
|
|
751
|
+
return transaction(() => Number(database.prepare("UPDATE receiver_queue SET state='pending',lease_token=NULL WHERE state='inflight'").run().changes));
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
return {
|
|
755
|
+
queue,
|
|
756
|
+
inbox,
|
|
757
|
+
database,
|
|
758
|
+
claim,
|
|
759
|
+
currentRow,
|
|
760
|
+
rows,
|
|
761
|
+
async append(wake) {
|
|
762
|
+
const parsed = await parseAdmittedWake(wake);
|
|
763
|
+
return transaction(() => Number(insert.run(boundedBody(parsed)).lastInsertRowid));
|
|
764
|
+
},
|
|
765
|
+
receiptQueueId(receipt) {
|
|
766
|
+
const row = database.prepare("SELECT queue_id FROM receiver_wakes WHERE identity=?").get(receipt.id);
|
|
767
|
+
if (row === void 0) throw new Error("Committed receipt has no work/completion record");
|
|
768
|
+
return Number(row.queue_id);
|
|
769
|
+
},
|
|
770
|
+
close: () => owner.close(),
|
|
771
|
+
pruneExpiredTokens(nowSeconds) {
|
|
772
|
+
NonnegativeJsonSafeIntegerZod3.parse(nowSeconds);
|
|
773
|
+
return transaction(() => Number(database.prepare("DELETE FROM receiver_tokens WHERE retain_until<=?").run(nowSeconds).changes));
|
|
774
|
+
}
|
|
775
|
+
};
|
|
377
776
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
777
|
+
|
|
778
|
+
// src/filesystemWakeQueue.ts
|
|
779
|
+
var authorities = /* @__PURE__ */ new WeakMap();
|
|
780
|
+
function filesystemReceiverAuthority(queue) {
|
|
781
|
+
const authority = authorities.get(queue);
|
|
782
|
+
if (authority === void 0) throw new TypeError("Filesystem inbox requires its coordinated filesystem receiver queue");
|
|
783
|
+
return authority;
|
|
784
|
+
}
|
|
785
|
+
function createFilesystemWakeQueue(options) {
|
|
786
|
+
const root = PATH6.resolve(options.path);
|
|
787
|
+
const file = PATH6.join(root, "receiver.sqlite");
|
|
788
|
+
if (!existsSync(file) && ["pending", "inflight", "tmp"].some((name) => {
|
|
789
|
+
const directory = PATH6.join(root, name);
|
|
790
|
+
return existsSync(directory) && readdirSync(directory).length > 0;
|
|
791
|
+
})) throw new LegacyReceiverStoreError(root);
|
|
792
|
+
ensurePrivateDirectory(root);
|
|
793
|
+
const pendingDir = PATH6.join(root, "pending");
|
|
794
|
+
const inflightDir = PATH6.join(root, "inflight");
|
|
795
|
+
const tempDir = PATH6.join(root, "tmp");
|
|
796
|
+
for (const directory of [pendingDir, inflightDir, tempDir]) ensurePrivateDirectory(directory);
|
|
797
|
+
const authority = openReceiverAuthority(file);
|
|
798
|
+
async function publishProjection(path, body) {
|
|
799
|
+
if (existsSync(path)) {
|
|
800
|
+
checkPrivateStoreFile(path);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const temporary = PATH6.join(tempDir, `${randomBytes4(16).toString("hex")}.json`);
|
|
804
|
+
await writeDurableFile(temporary, body, 384);
|
|
381
805
|
try {
|
|
382
|
-
|
|
383
|
-
|
|
806
|
+
try {
|
|
807
|
+
await link2(temporary, path);
|
|
808
|
+
} catch (error) {
|
|
809
|
+
if (!hasCode(error, "EEXIST")) throw error;
|
|
810
|
+
}
|
|
811
|
+
await syncDirectory(PATH6.dirname(path));
|
|
384
812
|
} finally {
|
|
385
|
-
await
|
|
813
|
+
await unlinkIfMissing2(temporary);
|
|
386
814
|
}
|
|
387
|
-
} catch (error) {
|
|
388
|
-
if (isNotFound4(error)) return void 0;
|
|
389
|
-
throw error;
|
|
390
815
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
816
|
+
async function validateProjection(row) {
|
|
817
|
+
if (row.body === null) throw new Error("Receiver work has no committed body");
|
|
818
|
+
const path = PATH6.join(pendingDir, `${row.id}.json`);
|
|
819
|
+
checkPrivateStoreFile(path);
|
|
820
|
+
let raw = await readJsonFile(path);
|
|
821
|
+
if (raw === void 0) {
|
|
822
|
+
await publishProjection(path, row.body);
|
|
823
|
+
checkPrivateStoreFile(path);
|
|
824
|
+
raw = await readJsonFile(path);
|
|
825
|
+
}
|
|
826
|
+
checkPrivateStoreFile(path);
|
|
827
|
+
const projected = await parseAdmittedWake2(raw);
|
|
828
|
+
if (JSON.stringify(projected) !== row.body) throw new Error("Filesystem projection differs from committed receiver work");
|
|
403
829
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// src/filesystemEventKitReplayCheckpointStore.ts
|
|
416
|
-
import PATH7 from "node:path";
|
|
417
|
-
|
|
418
|
-
// src/filesystemJson.ts
|
|
419
|
-
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
420
|
-
import {
|
|
421
|
-
link as link3,
|
|
422
|
-
open as open4,
|
|
423
|
-
rename as rename2,
|
|
424
|
-
unlink as unlink4
|
|
425
|
-
} from "node:fs/promises";
|
|
426
|
-
import PATH6 from "node:path";
|
|
427
|
-
async function readJsonFileStrict(path) {
|
|
428
|
-
try {
|
|
429
|
-
const handle = await open4(path, "r");
|
|
430
|
-
try {
|
|
431
|
-
return JSON.parse(await handle.readFile("utf8"), reviveJson);
|
|
432
|
-
} finally {
|
|
433
|
-
await handle.close();
|
|
830
|
+
async function repairRow(id) {
|
|
831
|
+
const row = authority.currentRow(id);
|
|
832
|
+
if (row === void 0) throw new Error("Receiver work record vanished");
|
|
833
|
+
if (row.state === "completed") {
|
|
834
|
+
await unlinkIfMissing2(PATH6.join(pendingDir, `${id}.json`));
|
|
835
|
+
} else {
|
|
836
|
+
if (row.body === null) throw new Error("Active receiver work is corrupt");
|
|
837
|
+
await publishProjection(PATH6.join(pendingDir, `${id}.json`), row.body);
|
|
838
|
+
if (row.state === "inflight" && row.lease_token !== null) {
|
|
839
|
+
await publishProjection(PATH6.join(inflightDir, `${id}-${row.lease_token}.json`), row.body);
|
|
840
|
+
}
|
|
434
841
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
842
|
+
await syncDirectory(pendingDir);
|
|
843
|
+
}
|
|
844
|
+
async function repairProjections() {
|
|
845
|
+
for (const row of authority.rows()) {
|
|
846
|
+
if (row.body === null) throw new Error("Active receiver work is corrupt");
|
|
847
|
+
await publishProjection(PATH6.join(pendingDir, `${row.id}.json`), row.body);
|
|
848
|
+
if (row.state === "inflight" && row.lease_token !== null) {
|
|
849
|
+
await publishProjection(PATH6.join(inflightDir, `${row.id}-${row.lease_token}.json`), row.body);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
for (const name of await readdir(pendingDir)) {
|
|
853
|
+
const match = /^(\d+)\.json$/u.exec(name);
|
|
854
|
+
if (match !== null && authority.currentRow(Number(match[1]))?.state === "completed") await unlinkIfMissing2(PATH6.join(pendingDir, name));
|
|
855
|
+
}
|
|
856
|
+
for (const name of await readdir(inflightDir)) {
|
|
857
|
+
const match = /^(\d+)-([0-9a-f]{32})\.json$/u.exec(name);
|
|
858
|
+
if (match !== null && authority.currentRow(Number(match[1]))?.lease_token !== match[2]) await unlinkIfMissing2(PATH6.join(inflightDir, name));
|
|
859
|
+
}
|
|
860
|
+
await syncDirectory(pendingDir);
|
|
861
|
+
await syncDirectory(inflightDir);
|
|
438
862
|
}
|
|
863
|
+
const queue = {
|
|
864
|
+
async append(wake) {
|
|
865
|
+
const id = await authority.append(wake);
|
|
866
|
+
await repairRow(id);
|
|
867
|
+
},
|
|
868
|
+
async claim() {
|
|
869
|
+
const leased = await authority.claim(validateProjection);
|
|
870
|
+
if (leased === void 0) return;
|
|
871
|
+
try {
|
|
872
|
+
await repairRow(leased.id);
|
|
873
|
+
if (authority.currentRow(leased.id)?.lease_token !== leased.token) throw new StaleWakeLeaseError2();
|
|
874
|
+
} catch (error) {
|
|
875
|
+
try {
|
|
876
|
+
await leased.release();
|
|
877
|
+
} catch (releaseError) {
|
|
878
|
+
throw new AggregateError([error, releaseError], "Claim projection failed and its lease could not be released", { cause: releaseError });
|
|
879
|
+
}
|
|
880
|
+
throw error;
|
|
881
|
+
}
|
|
882
|
+
return {
|
|
883
|
+
wake: leased.wake,
|
|
884
|
+
async ack() {
|
|
885
|
+
await leased.ack();
|
|
886
|
+
await unlinkIfMissing2(PATH6.join(inflightDir, `${leased.id}-${leased.token}.json`));
|
|
887
|
+
await repairRow(leased.id);
|
|
888
|
+
await syncDirectory(inflightDir);
|
|
889
|
+
},
|
|
890
|
+
async release() {
|
|
891
|
+
await leased.release();
|
|
892
|
+
await unlinkIfMissing2(PATH6.join(inflightDir, `${leased.id}-${leased.token}.json`));
|
|
893
|
+
await repairRow(leased.id);
|
|
894
|
+
await syncDirectory(inflightDir);
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
},
|
|
898
|
+
pendingCount: authority.queue.pendingCount,
|
|
899
|
+
async recoverInflight(options2) {
|
|
900
|
+
const moved = await authority.queue.recoverInflight(options2);
|
|
901
|
+
await repairProjections();
|
|
902
|
+
return moved;
|
|
903
|
+
},
|
|
904
|
+
close: authority.close,
|
|
905
|
+
repairProjections,
|
|
906
|
+
repairRow
|
|
907
|
+
};
|
|
908
|
+
authorities.set(queue, authority);
|
|
909
|
+
return queue;
|
|
439
910
|
}
|
|
440
|
-
async function
|
|
441
|
-
|
|
442
|
-
await ensureDurableDirectory(directory, options.directoryMode);
|
|
443
|
-
const tempPath = PATH6.join(directory, `.${PATH6.basename(path)}.${process.pid}.${suffix()}.tmp`);
|
|
444
|
-
await writeDurableFile(tempPath, `${stringifyJson(value)}
|
|
445
|
-
`, options.mode);
|
|
911
|
+
async function readJsonFile(path) {
|
|
912
|
+
let handle;
|
|
446
913
|
try {
|
|
447
|
-
await
|
|
448
|
-
await syncDirectory(directory);
|
|
914
|
+
handle = await open3(path, "r");
|
|
449
915
|
} catch (error) {
|
|
450
|
-
|
|
916
|
+
if (hasCode(error, "ENOENT")) return void 0;
|
|
451
917
|
throw error;
|
|
452
918
|
}
|
|
453
|
-
}
|
|
454
|
-
async function publishJsonFile(path, value, options = {}) {
|
|
455
|
-
const directory = PATH6.dirname(path);
|
|
456
|
-
await ensureDurableDirectory(directory, options.directoryMode);
|
|
457
|
-
const tempPath = PATH6.join(directory, `.${PATH6.basename(path)}.${process.pid}.${suffix()}.tmp`);
|
|
458
|
-
await writeDurableFile(tempPath, `${stringifyJson(value)}
|
|
459
|
-
`, options.mode);
|
|
460
919
|
try {
|
|
461
|
-
await
|
|
462
|
-
await
|
|
463
|
-
return true;
|
|
464
|
-
} catch (error) {
|
|
465
|
-
if (errorCode4(error) === "EEXIST") return false;
|
|
466
|
-
throw error;
|
|
920
|
+
if ((await handle.stat()).size > 65536) throw new RangeError("Receiver JSON record exceeds its 64 KiB bound");
|
|
921
|
+
return JSON.parse(await handle.readFile("utf8"));
|
|
467
922
|
} finally {
|
|
468
|
-
await
|
|
923
|
+
await handle.close();
|
|
469
924
|
}
|
|
470
925
|
}
|
|
471
|
-
function
|
|
472
|
-
return randomBytes4(8).toString("hex");
|
|
473
|
-
}
|
|
474
|
-
function stringifyJson(value) {
|
|
475
|
-
return JSON.stringify(value, replaceJsonValue);
|
|
476
|
-
}
|
|
477
|
-
function replaceJsonValue(_key, candidate) {
|
|
478
|
-
return typeof candidate === "bigint" ? { $eventKitBigInt: candidate.toString() } : candidate;
|
|
479
|
-
}
|
|
480
|
-
function reviveJson(_key, candidate) {
|
|
481
|
-
return 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) ? BigInt(candidate.$eventKitBigInt) : candidate;
|
|
482
|
-
}
|
|
483
|
-
async function unlinkIfMissing3(path) {
|
|
926
|
+
async function unlinkIfMissing2(path) {
|
|
484
927
|
try {
|
|
485
|
-
await
|
|
928
|
+
await unlink3(path);
|
|
486
929
|
} catch (error) {
|
|
487
|
-
if (
|
|
930
|
+
if (!hasCode(error, "ENOENT")) throw error;
|
|
488
931
|
}
|
|
489
932
|
}
|
|
490
|
-
function
|
|
491
|
-
return typeof error === "object" && error !== null && "code" in error
|
|
933
|
+
function hasCode(error, code) {
|
|
934
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/filesystemWakeInbox.ts
|
|
938
|
+
function createFilesystemWakeInbox(options) {
|
|
939
|
+
const root = PATH7.resolve(options.path);
|
|
940
|
+
if (["tokens", "wakes"].some((name) => {
|
|
941
|
+
const directory = PATH7.join(root, name);
|
|
942
|
+
return existsSync2(directory) && readdirSync2(directory).length > 0;
|
|
943
|
+
})) throw new LegacyReceiverStoreError(root);
|
|
944
|
+
ensurePrivateDirectory(root);
|
|
945
|
+
const authority = filesystemReceiverAuthority(options.queue);
|
|
946
|
+
return {
|
|
947
|
+
resolveAttempt: authority.inbox.resolveAttempt,
|
|
948
|
+
async commitAuthorized(input) {
|
|
949
|
+
const committed = await authority.inbox.commitAuthorized(input);
|
|
950
|
+
if ("receipt" in committed) await options.queue.repairRow(authority.receiptQueueId(committed.receipt));
|
|
951
|
+
return committed;
|
|
952
|
+
},
|
|
953
|
+
pruneExpiredTokens: async (nowSeconds) => authority.pruneExpiredTokens(nowSeconds)
|
|
954
|
+
};
|
|
492
955
|
}
|
|
493
956
|
|
|
494
957
|
// src/filesystemEventKitReplayCheckpointStore.ts
|
|
958
|
+
import PATH8 from "node:path";
|
|
495
959
|
function createFilesystemEventKitReplayCheckpointStore(options) {
|
|
496
|
-
const path =
|
|
960
|
+
const path = PATH8.resolve(options.path);
|
|
497
961
|
let tail = Promise.resolve();
|
|
498
962
|
return {
|
|
499
963
|
async load() {
|
|
@@ -520,7 +984,7 @@ function createFilesystemEventKitReplayCheckpointStore(options) {
|
|
|
520
984
|
|
|
521
985
|
// src/filesystemFinalizedStatementPublicationJournal.ts
|
|
522
986
|
import { createHash as createHash2 } from "node:crypto";
|
|
523
|
-
import
|
|
987
|
+
import PATH9 from "node:path";
|
|
524
988
|
import { isDeepStrictEqual } from "node:util";
|
|
525
989
|
var PublicationPhases = [
|
|
526
990
|
"signed",
|
|
@@ -566,7 +1030,7 @@ function publicationDirectory(root, intent) {
|
|
|
566
1030
|
operationId: intent.operationId,
|
|
567
1031
|
source: intent.source
|
|
568
1032
|
})).digest("hex");
|
|
569
|
-
return
|
|
1033
|
+
return PATH9.join(root, key);
|
|
570
1034
|
}
|
|
571
1035
|
function asPublicationRecord(value, path) {
|
|
572
1036
|
if (typeof value !== "object" || value === null) throw new Error(`malformed publication journal record at ${path}`);
|
|
@@ -581,12 +1045,12 @@ function assertBoundRecord(record, intent, signed) {
|
|
|
581
1045
|
}
|
|
582
1046
|
}
|
|
583
1047
|
function createFilesystemFinalizedStatementPublicationJournal(options) {
|
|
584
|
-
const root =
|
|
1048
|
+
const root = PATH9.resolve(options.path);
|
|
585
1049
|
return {
|
|
586
1050
|
async load(intent) {
|
|
587
1051
|
assertIntentShape(intent);
|
|
588
1052
|
const directory = publicationDirectory(root, intent);
|
|
589
|
-
const intentPath =
|
|
1053
|
+
const intentPath = PATH9.join(directory, "intent.json");
|
|
590
1054
|
const rawSigned = await readJsonFileStrict(intentPath);
|
|
591
1055
|
if (rawSigned === void 0) return void 0;
|
|
592
1056
|
const signed = asPublicationRecord(rawSigned, intentPath);
|
|
@@ -594,7 +1058,7 @@ function createFilesystemFinalizedStatementPublicationJournal(options) {
|
|
|
594
1058
|
assertBoundRecord(signed, intent, signed);
|
|
595
1059
|
let latest = signed;
|
|
596
1060
|
for (const phase of PublicationPhases.slice(1)) {
|
|
597
|
-
const path =
|
|
1061
|
+
const path = PATH9.join(directory, `${phase}.json`);
|
|
598
1062
|
const rawRecord = await readJsonFileStrict(path);
|
|
599
1063
|
if (rawRecord === void 0) continue;
|
|
600
1064
|
const record = asPublicationRecord(rawRecord, path);
|
|
@@ -612,7 +1076,7 @@ function createFilesystemFinalizedStatementPublicationJournal(options) {
|
|
|
612
1076
|
}
|
|
613
1077
|
if (!PublicationPhaseSet.has(record.phase)) throw new TypeError("unknown publication phase");
|
|
614
1078
|
const directory = publicationDirectory(root, intent);
|
|
615
|
-
const intentPath =
|
|
1079
|
+
const intentPath = PATH9.join(directory, "intent.json");
|
|
616
1080
|
if (record.phase === "signed") {
|
|
617
1081
|
if (await publishJsonFile(intentPath, record)) return;
|
|
618
1082
|
const existing2 = asPublicationRecord(await readJsonFileStrict(intentPath), intentPath);
|
|
@@ -627,7 +1091,7 @@ function createFilesystemFinalizedStatementPublicationJournal(options) {
|
|
|
627
1091
|
const signed = asPublicationRecord(rawSigned, intentPath);
|
|
628
1092
|
if (signed.phase !== "signed") throw new Error(`publication intent record is not signed at ${intentPath}`);
|
|
629
1093
|
assertBoundRecord(record, intent, signed);
|
|
630
|
-
const path =
|
|
1094
|
+
const path = PATH9.join(directory, `${record.phase}.json`);
|
|
631
1095
|
if (await publishJsonFile(path, record)) return;
|
|
632
1096
|
const existing = asPublicationRecord(await readJsonFileStrict(path), path);
|
|
633
1097
|
if (!isDeepStrictEqual(existing, record)) {
|
|
@@ -639,7 +1103,7 @@ function createFilesystemFinalizedStatementPublicationJournal(options) {
|
|
|
639
1103
|
|
|
640
1104
|
// src/filesystemHandshakeMutationIntentStore.ts
|
|
641
1105
|
import { stat } from "node:fs/promises";
|
|
642
|
-
import
|
|
1106
|
+
import PATH10 from "node:path";
|
|
643
1107
|
|
|
644
1108
|
// src/handshakeMutationIntent.ts
|
|
645
1109
|
import {
|
|
@@ -653,8 +1117,8 @@ import {
|
|
|
653
1117
|
EndpointChallengeResponseZod,
|
|
654
1118
|
EndpointChallengeZod,
|
|
655
1119
|
HandshakeIdZod,
|
|
656
|
-
HashZod,
|
|
657
|
-
NormalizedAddressZod,
|
|
1120
|
+
HashZod as HashZod3,
|
|
1121
|
+
NormalizedAddressZod as NormalizedAddressZod2,
|
|
658
1122
|
PublisherEncryptionKeyRevocationZod,
|
|
659
1123
|
PublisherEncryptionKeyZod,
|
|
660
1124
|
PublisherServiceRevocationZod,
|
|
@@ -664,7 +1128,7 @@ import {
|
|
|
664
1128
|
WakeSubscriptionAcceptanceZod,
|
|
665
1129
|
WakeSubscriptionRevocationZod
|
|
666
1130
|
} from "@xyo-network/event-kit-schemas";
|
|
667
|
-
import { z } from "zod";
|
|
1131
|
+
import { z as z2 } from "zod";
|
|
668
1132
|
var HandshakeMutationIntentKinds = [
|
|
669
1133
|
"publisher-encryption-key-retire",
|
|
670
1134
|
"publisher-service-publish",
|
|
@@ -675,83 +1139,83 @@ var HandshakeMutationIntentKinds = [
|
|
|
675
1139
|
"subscriber-subscription-revoke",
|
|
676
1140
|
"subscriber-subscription-subscribe"
|
|
677
1141
|
];
|
|
678
|
-
var HandshakeMutationIntentKindZod =
|
|
679
|
-
var HandshakeMutationOperationIdZod =
|
|
680
|
-
var WakeSubscriptionPrivateKeyZod =
|
|
681
|
-
encryptionKey:
|
|
1142
|
+
var HandshakeMutationIntentKindZod = z2.enum(HandshakeMutationIntentKinds);
|
|
1143
|
+
var HandshakeMutationOperationIdZod = z2.union([HandshakeIdZod, HashZod3]);
|
|
1144
|
+
var WakeSubscriptionPrivateKeyZod = z2.strictObject({
|
|
1145
|
+
encryptionKey: HashZod3,
|
|
682
1146
|
keyId: HandshakeIdZod,
|
|
683
1147
|
privateKey: Base64Url32BytesZod
|
|
684
1148
|
});
|
|
685
1149
|
var IntentBaseFields = {
|
|
686
1150
|
chainId: ChainIdZod,
|
|
687
1151
|
operationId: HandshakeMutationOperationIdZod,
|
|
688
|
-
publisher:
|
|
689
|
-
requestFingerprint:
|
|
690
|
-
source:
|
|
691
|
-
version:
|
|
1152
|
+
publisher: NormalizedAddressZod2,
|
|
1153
|
+
requestFingerprint: HashZod3,
|
|
1154
|
+
source: NormalizedAddressZod2,
|
|
1155
|
+
version: z2.literal(1)
|
|
692
1156
|
};
|
|
693
|
-
var PublisherServicePublishIntentZod =
|
|
1157
|
+
var PublisherServicePublishIntentZod = z2.strictObject({
|
|
694
1158
|
...IntentBaseFields,
|
|
695
|
-
kind:
|
|
1159
|
+
kind: z2.literal("publisher-service-publish"),
|
|
696
1160
|
operationId: HandshakeIdZod,
|
|
697
1161
|
encryptionKey: PublisherEncryptionKeyZod,
|
|
698
1162
|
privateKey: WakeSubscriptionPrivateKeyZod,
|
|
699
1163
|
service: PublisherServiceZod
|
|
700
1164
|
});
|
|
701
|
-
var PublisherServiceRotateKeyIntentZod =
|
|
1165
|
+
var PublisherServiceRotateKeyIntentZod = z2.strictObject({
|
|
702
1166
|
...IntentBaseFields,
|
|
703
|
-
kind:
|
|
1167
|
+
kind: z2.literal("publisher-service-rotate-key"),
|
|
704
1168
|
operationId: HandshakeIdZod,
|
|
705
1169
|
encryptionKey: PublisherEncryptionKeyZod,
|
|
706
1170
|
privateKey: WakeSubscriptionPrivateKeyZod,
|
|
707
|
-
selectedKey:
|
|
708
|
-
selectedService:
|
|
1171
|
+
selectedKey: HashZod3,
|
|
1172
|
+
selectedService: HashZod3,
|
|
709
1173
|
service: PublisherServiceZod
|
|
710
1174
|
});
|
|
711
|
-
var SubscriberSubscriptionIntentZod =
|
|
1175
|
+
var SubscriberSubscriptionIntentZod = z2.strictObject({
|
|
712
1176
|
...IntentBaseFields,
|
|
713
|
-
kind:
|
|
1177
|
+
kind: z2.literal("subscriber-subscription-subscribe"),
|
|
714
1178
|
operationId: HandshakeIdZod,
|
|
715
1179
|
encryptionKey: PublisherEncryptionKeyZod,
|
|
716
1180
|
grant: WakeGrantObjectZod,
|
|
717
|
-
selectedKey:
|
|
718
|
-
selectedService:
|
|
1181
|
+
selectedKey: HashZod3,
|
|
1182
|
+
selectedService: HashZod3,
|
|
719
1183
|
service: PublisherServiceZod,
|
|
720
1184
|
subscription: EncryptedWakeSubscriptionZod
|
|
721
1185
|
});
|
|
722
|
-
var PublisherAcceptanceIntentZod =
|
|
1186
|
+
var PublisherAcceptanceIntentZod = z2.strictObject({
|
|
723
1187
|
...IntentBaseFields,
|
|
724
1188
|
acceptance: WakeSubscriptionAcceptanceZod,
|
|
725
1189
|
challenge: EndpointChallengeZod,
|
|
726
|
-
kind:
|
|
727
|
-
operationId:
|
|
1190
|
+
kind: z2.literal("publisher-subscription-accept"),
|
|
1191
|
+
operationId: HashZod3,
|
|
728
1192
|
response: EndpointChallengeResponseZod
|
|
729
1193
|
});
|
|
730
|
-
var PublisherServiceRetireIntentZod =
|
|
1194
|
+
var PublisherServiceRetireIntentZod = z2.strictObject({
|
|
731
1195
|
...IntentBaseFields,
|
|
732
|
-
kind:
|
|
733
|
-
operationId:
|
|
1196
|
+
kind: z2.literal("publisher-service-retire"),
|
|
1197
|
+
operationId: HashZod3,
|
|
734
1198
|
revocation: PublisherServiceRevocationZod
|
|
735
1199
|
});
|
|
736
|
-
var PublisherEncryptionKeyRetireIntentZod =
|
|
1200
|
+
var PublisherEncryptionKeyRetireIntentZod = z2.strictObject({
|
|
737
1201
|
...IntentBaseFields,
|
|
738
|
-
kind:
|
|
739
|
-
operationId:
|
|
1202
|
+
kind: z2.literal("publisher-encryption-key-retire"),
|
|
1203
|
+
operationId: HashZod3,
|
|
740
1204
|
revocation: PublisherEncryptionKeyRevocationZod
|
|
741
1205
|
});
|
|
742
|
-
var SubscriberGrantRevokeIntentZod =
|
|
1206
|
+
var SubscriberGrantRevokeIntentZod = z2.strictObject({
|
|
743
1207
|
...IntentBaseFields,
|
|
744
|
-
kind:
|
|
745
|
-
operationId:
|
|
1208
|
+
kind: z2.literal("subscriber-grant-revoke"),
|
|
1209
|
+
operationId: HashZod3,
|
|
746
1210
|
revocation: WakeGrantRevocationZod
|
|
747
1211
|
});
|
|
748
|
-
var SubscriberSubscriptionRevokeIntentZod =
|
|
1212
|
+
var SubscriberSubscriptionRevokeIntentZod = z2.strictObject({
|
|
749
1213
|
...IntentBaseFields,
|
|
750
|
-
kind:
|
|
751
|
-
operationId:
|
|
1214
|
+
kind: z2.literal("subscriber-subscription-revoke"),
|
|
1215
|
+
operationId: HashZod3,
|
|
752
1216
|
revocation: WakeSubscriptionRevocationZod
|
|
753
1217
|
});
|
|
754
|
-
var HandshakeMutationIntentZod =
|
|
1218
|
+
var HandshakeMutationIntentZod = z2.discriminatedUnion("kind", [
|
|
755
1219
|
PublisherEncryptionKeyRetireIntentZod,
|
|
756
1220
|
PublisherServicePublishIntentZod,
|
|
757
1221
|
PublisherServiceRetireIntentZod,
|
|
@@ -771,17 +1235,17 @@ async function handshakeMutationRequestFingerprint(value) {
|
|
|
771
1235
|
const canonical = canonicalJsonBytes(value);
|
|
772
1236
|
const digestInput = new Uint8Array(canonical.byteLength);
|
|
773
1237
|
digestInput.set(canonical);
|
|
774
|
-
const
|
|
775
|
-
const hex = Array.from(new Uint8Array(
|
|
776
|
-
return
|
|
1238
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", digestInput);
|
|
1239
|
+
const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1240
|
+
return HashZod3.parse(hex);
|
|
777
1241
|
}
|
|
778
1242
|
function parseHandshakeMutationIntentLookup(value) {
|
|
779
1243
|
return Object.freeze({
|
|
780
1244
|
chainId: ChainIdZod.parse(value.chainId),
|
|
781
1245
|
kind: HandshakeMutationIntentKindZod.parse(value.kind),
|
|
782
1246
|
operationId: HandshakeMutationOperationIdZod.parse(value.operationId),
|
|
783
|
-
publisher:
|
|
784
|
-
source:
|
|
1247
|
+
publisher: NormalizedAddressZod2.parse(value.publisher),
|
|
1248
|
+
source: NormalizedAddressZod2.parse(value.source)
|
|
785
1249
|
});
|
|
786
1250
|
}
|
|
787
1251
|
async function parseHandshakeMutationIntent(value) {
|
|
@@ -879,10 +1343,10 @@ async function assertRevocationIntent(intent) {
|
|
|
879
1343
|
|
|
880
1344
|
// src/filesystemHandshakeMutationIntentStore.ts
|
|
881
1345
|
function createFilesystemHandshakeMutationIntentStore(options) {
|
|
882
|
-
const root =
|
|
1346
|
+
const root = PATH10.resolve(options.path);
|
|
883
1347
|
function pathFor(rawLookup) {
|
|
884
1348
|
const lookup2 = parseHandshakeMutationIntentLookup(rawLookup);
|
|
885
|
-
return
|
|
1349
|
+
return PATH10.join(
|
|
886
1350
|
root,
|
|
887
1351
|
lookup2.chainId,
|
|
888
1352
|
lookup2.publisher,
|
|
@@ -923,29 +1387,29 @@ function createFilesystemHandshakeMutationIntentStore(options) {
|
|
|
923
1387
|
|
|
924
1388
|
// src/filesystemPublisherEncryptionKeyStore.ts
|
|
925
1389
|
import { stat as stat2 } from "node:fs/promises";
|
|
926
|
-
import
|
|
1390
|
+
import PATH11 from "node:path";
|
|
927
1391
|
import {
|
|
928
1392
|
Base64Url32BytesZod as Base64Url32BytesZod2,
|
|
929
1393
|
HandshakeIdZod as HandshakeIdZod2,
|
|
930
|
-
HashZod as
|
|
931
|
-
NormalizedAddressZod as
|
|
1394
|
+
HashZod as HashZod4,
|
|
1395
|
+
NormalizedAddressZod as NormalizedAddressZod3
|
|
932
1396
|
} from "@xyo-network/event-kit-schemas";
|
|
933
1397
|
function createFilesystemPublisherEncryptionKeyStore(options) {
|
|
934
|
-
const root =
|
|
1398
|
+
const root = PATH11.resolve(options.path);
|
|
935
1399
|
function parseLookup(lookup2) {
|
|
936
1400
|
return {
|
|
937
|
-
encryptionKey:
|
|
1401
|
+
encryptionKey: HashZod4.parse(lookup2.encryptionKey),
|
|
938
1402
|
keyId: HandshakeIdZod2.parse(lookup2.keyId),
|
|
939
|
-
publisher:
|
|
1403
|
+
publisher: NormalizedAddressZod3.parse(lookup2.publisher)
|
|
940
1404
|
};
|
|
941
1405
|
}
|
|
942
1406
|
function pathFor(lookup2) {
|
|
943
|
-
return
|
|
1407
|
+
return PATH11.join(root, lookup2.publisher, `${lookup2.encryptionKey}-${lookup2.keyId}.json`);
|
|
944
1408
|
}
|
|
945
1409
|
function keyIdPath(lookup2) {
|
|
946
|
-
return
|
|
1410
|
+
return PATH11.join(root, lookup2.publisher, "key-ids", `${lookup2.keyId}.json`);
|
|
947
1411
|
}
|
|
948
|
-
async function
|
|
1412
|
+
async function readRecord(lookup2) {
|
|
949
1413
|
const path = pathFor(lookup2);
|
|
950
1414
|
const raw = await readJsonFileStrict(path);
|
|
951
1415
|
if (raw === void 0) return void 0;
|
|
@@ -960,7 +1424,7 @@ function createFilesystemPublisherEncryptionKeyStore(options) {
|
|
|
960
1424
|
return {
|
|
961
1425
|
async getPrivateKey(rawLookup) {
|
|
962
1426
|
const lookup2 = parseLookup(rawLookup);
|
|
963
|
-
const record = await
|
|
1427
|
+
const record = await readRecord(lookup2);
|
|
964
1428
|
if (record === void 0) return void 0;
|
|
965
1429
|
return Object.freeze({
|
|
966
1430
|
encryptionKey: record.encryptionKey,
|
|
@@ -988,7 +1452,7 @@ function createFilesystemPublisherEncryptionKeyStore(options) {
|
|
|
988
1452
|
}
|
|
989
1453
|
}
|
|
990
1454
|
if (await publishJsonFile(path, record, { directoryMode: 448, mode: 384 })) return "stored";
|
|
991
|
-
const existing = await
|
|
1455
|
+
const existing = await readRecord(lookup2);
|
|
992
1456
|
if (stringifyJson(existing) !== stringifyJson(record)) {
|
|
993
1457
|
throw new Error(`publisher encryption-key collision for ${record.encryptionKey}/${record.keyId}`);
|
|
994
1458
|
}
|
|
@@ -1004,9 +1468,9 @@ function parseKeyIdRecord(value) {
|
|
|
1004
1468
|
throw new TypeError("invalid publisher encryption-key keyId record shape");
|
|
1005
1469
|
}
|
|
1006
1470
|
return {
|
|
1007
|
-
encryptionKey:
|
|
1471
|
+
encryptionKey: HashZod4.parse(record.encryptionKey),
|
|
1008
1472
|
keyId: HandshakeIdZod2.parse(record.keyId),
|
|
1009
|
-
publisher:
|
|
1473
|
+
publisher: NormalizedAddressZod3.parse(record.publisher)
|
|
1010
1474
|
};
|
|
1011
1475
|
}
|
|
1012
1476
|
function parsePrivateKeyRecord(value) {
|
|
@@ -1017,28 +1481,28 @@ function parsePrivateKeyRecord(value) {
|
|
|
1017
1481
|
throw new TypeError("invalid publisher encryption-key record shape");
|
|
1018
1482
|
}
|
|
1019
1483
|
return {
|
|
1020
|
-
encryptionKey:
|
|
1484
|
+
encryptionKey: HashZod4.parse(record.encryptionKey),
|
|
1021
1485
|
keyId: HandshakeIdZod2.parse(record.keyId),
|
|
1022
1486
|
privateKey: Base64Url32BytesZod2.parse(record.privateKey),
|
|
1023
|
-
publisher:
|
|
1487
|
+
publisher: NormalizedAddressZod3.parse(record.publisher)
|
|
1024
1488
|
};
|
|
1025
1489
|
}
|
|
1026
1490
|
|
|
1027
1491
|
// src/filesystemPublisherEndpointChallengeStore.ts
|
|
1028
|
-
import
|
|
1492
|
+
import PATH12 from "node:path";
|
|
1029
1493
|
import { endpointChallengeHash } from "@xyo-network/event-kit-protocol";
|
|
1030
1494
|
import {
|
|
1031
1495
|
ChallengeNonceZod,
|
|
1032
1496
|
EndpointChallengeZod as EndpointChallengeZod2,
|
|
1033
|
-
HashZod as
|
|
1497
|
+
HashZod as HashZod5
|
|
1034
1498
|
} from "@xyo-network/event-kit-schemas";
|
|
1035
1499
|
function createFilesystemPublisherEndpointChallengeStore(options) {
|
|
1036
|
-
const root =
|
|
1500
|
+
const root = PATH12.resolve(options.path);
|
|
1037
1501
|
function statePath(challengeHash, state) {
|
|
1038
|
-
return
|
|
1502
|
+
return PATH12.join(root, challengeHash, `${state}.json`);
|
|
1039
1503
|
}
|
|
1040
1504
|
function noncePath(nonce) {
|
|
1041
|
-
return
|
|
1505
|
+
return PATH12.join(root, "nonces", `${nonce}.json`);
|
|
1042
1506
|
}
|
|
1043
1507
|
async function readState(challengeHash, state) {
|
|
1044
1508
|
const raw = await readJsonFileStrict(statePath(challengeHash, state));
|
|
@@ -1059,7 +1523,7 @@ function createFilesystemPublisherEndpointChallengeStore(options) {
|
|
|
1059
1523
|
}
|
|
1060
1524
|
return {
|
|
1061
1525
|
async beginAttempt(input) {
|
|
1062
|
-
const challengeHash =
|
|
1526
|
+
const challengeHash = HashZod5.parse(input.challengeHash);
|
|
1063
1527
|
requireTime(input.nowSeconds);
|
|
1064
1528
|
const observed = await current(challengeHash);
|
|
1065
1529
|
if (observed === void 0) return { kind: "missing" };
|
|
@@ -1073,8 +1537,8 @@ function createFilesystemPublisherEndpointChallengeStore(options) {
|
|
|
1073
1537
|
return { kind: winner.state === "verified" ? "verified" : "already-attempted", record: winner };
|
|
1074
1538
|
},
|
|
1075
1539
|
async completeVerified(input) {
|
|
1076
|
-
const challengeHash =
|
|
1077
|
-
const responseHash =
|
|
1540
|
+
const challengeHash = HashZod5.parse(input.challengeHash);
|
|
1541
|
+
const responseHash = HashZod5.parse(input.responseHash);
|
|
1078
1542
|
requireTime(input.nowSeconds);
|
|
1079
1543
|
const observed = await current(challengeHash);
|
|
1080
1544
|
if (observed === void 0) return { kind: "missing" };
|
|
@@ -1093,10 +1557,10 @@ function createFilesystemPublisherEndpointChallengeStore(options) {
|
|
|
1093
1557
|
if (winner === void 0) throw new Error("verified challenge winner vanished");
|
|
1094
1558
|
return { kind: winner.responseHash === responseHash ? "duplicate" : "collision", record: winner };
|
|
1095
1559
|
},
|
|
1096
|
-
get: async (rawHash) => await current(
|
|
1560
|
+
get: async (rawHash) => await current(HashZod5.parse(rawHash)),
|
|
1097
1561
|
async issue(input) {
|
|
1098
1562
|
const challenge = EndpointChallengeZod2.parse(input.challenge);
|
|
1099
|
-
const challengeHash =
|
|
1563
|
+
const challengeHash = HashZod5.parse(input.challengeHash);
|
|
1100
1564
|
if (await endpointChallengeHash(challenge) !== challengeHash) return { kind: "collision" };
|
|
1101
1565
|
const nonceRecord = { challengeHash, nonce: challenge.nonce };
|
|
1102
1566
|
if (!await publishJsonFile(
|
|
@@ -1127,7 +1591,7 @@ function parseNonceRecord(value) {
|
|
|
1127
1591
|
const keys = Object.keys(record).toSorted();
|
|
1128
1592
|
if (keys.join("|") !== "challengeHash|nonce") throw new TypeError("invalid endpoint-challenge nonce record shape");
|
|
1129
1593
|
return {
|
|
1130
|
-
challengeHash:
|
|
1594
|
+
challengeHash: HashZod5.parse(record.challengeHash),
|
|
1131
1595
|
nonce: ChallengeNonceZod.parse(record.nonce)
|
|
1132
1596
|
};
|
|
1133
1597
|
}
|
|
@@ -1137,13 +1601,13 @@ async function parseChallengeRecord(value, expectedState) {
|
|
|
1137
1601
|
const state = record.state;
|
|
1138
1602
|
if (state !== expectedState) throw new Error("endpoint-challenge record state does not match its path");
|
|
1139
1603
|
const challenge = EndpointChallengeZod2.parse(record.challenge);
|
|
1140
|
-
const challengeHash =
|
|
1604
|
+
const challengeHash = HashZod5.parse(record.challengeHash);
|
|
1141
1605
|
if (await endpointChallengeHash(challenge) !== challengeHash) throw new Error("endpoint-challenge record hash mismatch");
|
|
1142
1606
|
if (expectedState === "verified") {
|
|
1143
1607
|
return {
|
|
1144
1608
|
challenge,
|
|
1145
1609
|
challengeHash,
|
|
1146
|
-
responseHash:
|
|
1610
|
+
responseHash: HashZod5.parse(record.responseHash),
|
|
1147
1611
|
state: expectedState
|
|
1148
1612
|
};
|
|
1149
1613
|
}
|
|
@@ -1167,7 +1631,7 @@ function requireTime(value) {
|
|
|
1167
1631
|
// src/handshakeClients.ts
|
|
1168
1632
|
import {
|
|
1169
1633
|
beginPublisherEndpointChallengeAttempt,
|
|
1170
|
-
canonicalJson,
|
|
1634
|
+
canonicalJson as canonicalJson2,
|
|
1171
1635
|
completePublisherEndpointChallenge,
|
|
1172
1636
|
createEncryptedWakeSubscription,
|
|
1173
1637
|
createPublisherEncryptionKeyRevocation,
|
|
@@ -1187,9 +1651,9 @@ import {
|
|
|
1187
1651
|
import {
|
|
1188
1652
|
ChainIdZod as ChainIdZod2,
|
|
1189
1653
|
HandshakeIdZod as HandshakeIdZod3,
|
|
1190
|
-
HashZod as
|
|
1654
|
+
HashZod as HashZod6,
|
|
1191
1655
|
normalizeAddress,
|
|
1192
|
-
NormalizedAddressZod as
|
|
1656
|
+
NormalizedAddressZod as NormalizedAddressZod4
|
|
1193
1657
|
} from "@xyo-network/event-kit-schemas";
|
|
1194
1658
|
import { createEventKitHandshakeReadModel } from "@xyo-network/event-kit-statement-graph";
|
|
1195
1659
|
import {
|
|
@@ -1211,7 +1675,7 @@ var EventKitHandshakeClientError = class extends Error {
|
|
|
1211
1675
|
}
|
|
1212
1676
|
};
|
|
1213
1677
|
function createSynchronizedEventKitHandshakeReader(options) {
|
|
1214
|
-
const publisher =
|
|
1678
|
+
const publisher = NormalizedAddressZod4.parse(options.publisher);
|
|
1215
1679
|
const chainId = ChainIdZod2.parse(options.session.chainId);
|
|
1216
1680
|
function assertIdentity() {
|
|
1217
1681
|
const checkpoint = options.replay.snapshot();
|
|
@@ -1253,7 +1717,7 @@ function createEventKitHandshakePublicationPort(transport) {
|
|
|
1253
1717
|
publishWakeSubscriptionRevocation: async (input) => await publishWakeSubscriptionRevocation({ ...transport, ...input }),
|
|
1254
1718
|
async source() {
|
|
1255
1719
|
const raw = String(await transport.signer.address());
|
|
1256
|
-
const normalized =
|
|
1720
|
+
const normalized = NormalizedAddressZod4.parse(normalizeAddress(raw));
|
|
1257
1721
|
if (raw !== normalized) {
|
|
1258
1722
|
throw new EventKitHandshakeClientError("identity-mismatch", "XL1 publication signer address is not canonical lowercase hexadecimal without 0x");
|
|
1259
1723
|
}
|
|
@@ -1299,7 +1763,7 @@ function requireService(discovery) {
|
|
|
1299
1763
|
return discovery;
|
|
1300
1764
|
}
|
|
1301
1765
|
function requireExpectedService(discovery, expected) {
|
|
1302
|
-
const expectedHash =
|
|
1766
|
+
const expectedHash = HashZod6.parse(expected);
|
|
1303
1767
|
if (discovery.service.objectHash !== expectedHash) {
|
|
1304
1768
|
throw new EventKitHandshakeClientError("service-mismatch", "Currently selected publisher service does not match the expected object hash");
|
|
1305
1769
|
}
|
|
@@ -1349,6 +1813,8 @@ async function persistMutationIntent(store, intent) {
|
|
|
1349
1813
|
}
|
|
1350
1814
|
async function publisherServiceRequestFingerprint(input) {
|
|
1351
1815
|
return await handshakeMutationRequestFingerprint({
|
|
1816
|
+
deliveryMode: input.deliveryMode,
|
|
1817
|
+
bestEffort: input.bestEffort ?? null,
|
|
1352
1818
|
expiresAtBlock: input.expiresAtBlock ?? null,
|
|
1353
1819
|
maxPositionsPerWake: input.maxPositionsPerWake,
|
|
1354
1820
|
sources: input.sources,
|
|
@@ -1362,12 +1828,14 @@ async function publisherRotationRequestFingerprint(input) {
|
|
|
1362
1828
|
keyExpiresAtBlock: input.keyExpiresAtBlock ?? null,
|
|
1363
1829
|
maxPositionsPerWake: input.maxPositionsPerWake ?? null,
|
|
1364
1830
|
maxSubscriptionLifetimeBlocks: input.maxSubscriptionLifetimeBlocks ?? null,
|
|
1365
|
-
service:
|
|
1831
|
+
service: HashZod6.parse(input.service),
|
|
1366
1832
|
validFromBlock: input.validFromBlock ?? null
|
|
1367
1833
|
});
|
|
1368
1834
|
}
|
|
1369
1835
|
async function subscriberSubscriptionRequestFingerprint(input) {
|
|
1370
1836
|
return await handshakeMutationRequestFingerprint({
|
|
1837
|
+
deliveryMode: input.deliveryMode,
|
|
1838
|
+
bestEffort: input.bestEffort ?? null,
|
|
1371
1839
|
audience: input.audience,
|
|
1372
1840
|
challengeSecret: input.challengeSecret,
|
|
1373
1841
|
deploymentId: input.deploymentId,
|
|
@@ -1392,28 +1860,28 @@ function protocolCandidate(reader, discovery, candidate, keys) {
|
|
|
1392
1860
|
finalizedHead: discovery.finalizedHead,
|
|
1393
1861
|
grant: {
|
|
1394
1862
|
body: candidate.tuple.grant.body,
|
|
1395
|
-
objectHash:
|
|
1863
|
+
objectHash: HashZod6.parse(candidate.tuple.grant.objectHash)
|
|
1396
1864
|
},
|
|
1397
1865
|
key: {
|
|
1398
1866
|
body: candidate.key.body,
|
|
1399
|
-
objectHash:
|
|
1867
|
+
objectHash: HashZod6.parse(candidate.key.objectHash)
|
|
1400
1868
|
},
|
|
1401
1869
|
keys,
|
|
1402
1870
|
publisher: reader.publisher,
|
|
1403
1871
|
service: {
|
|
1404
1872
|
body: candidate.tuple.service.body,
|
|
1405
|
-
objectHash:
|
|
1873
|
+
objectHash: HashZod6.parse(candidate.tuple.service.objectHash)
|
|
1406
1874
|
},
|
|
1407
|
-
subscriber:
|
|
1875
|
+
subscriber: NormalizedAddressZod4.parse(candidate.subscriber),
|
|
1408
1876
|
subscription: {
|
|
1409
1877
|
body: candidate.tuple.subscription.body,
|
|
1410
|
-
objectHash:
|
|
1878
|
+
objectHash: HashZod6.parse(candidate.tuple.subscription.objectHash)
|
|
1411
1879
|
},
|
|
1412
1880
|
tupleBlock: candidate.tuple.tupleEvent.block,
|
|
1413
1881
|
tupleObjectHashes: [
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1882
|
+
HashZod6.parse(candidate.tuple.service.objectHash),
|
|
1883
|
+
HashZod6.parse(candidate.tuple.grant.objectHash),
|
|
1884
|
+
HashZod6.parse(candidate.tuple.subscription.objectHash)
|
|
1417
1885
|
],
|
|
1418
1886
|
tupleSubject: reader.publisher
|
|
1419
1887
|
};
|
|
@@ -1438,7 +1906,7 @@ function createPublisherHandshakeClient(options) {
|
|
|
1438
1906
|
const current = await snapshot();
|
|
1439
1907
|
return await current.readModel.acceptanceCandidates({
|
|
1440
1908
|
...serviceQuery(options.reader, current, serviceId),
|
|
1441
|
-
...selector.subscriber === void 0 ? {} : { subscriber:
|
|
1909
|
+
...selector.subscriber === void 0 ? {} : { subscriber: NormalizedAddressZod4.parse(selector.subscriber) },
|
|
1442
1910
|
...selector.subscriptionId === void 0 ? {} : { subscriptionId: HandshakeIdZod3.parse(selector.subscriptionId) }
|
|
1443
1911
|
});
|
|
1444
1912
|
}
|
|
@@ -1446,8 +1914,8 @@ function createPublisherHandshakeClient(options) {
|
|
|
1446
1914
|
const found = await candidateDiscovery(selector);
|
|
1447
1915
|
if (!found.ok)
|
|
1448
1916
|
throw new EventKitHandshakeClientError("service-unavailable", `Publisher service is unavailable: ${found.reason}`);
|
|
1449
|
-
const subscriber =
|
|
1450
|
-
const subscription = selector.subscription === void 0 ? void 0 :
|
|
1917
|
+
const subscriber = NormalizedAddressZod4.parse(selector.subscriber);
|
|
1918
|
+
const subscription = selector.subscription === void 0 ? void 0 : HashZod6.parse(selector.subscription);
|
|
1451
1919
|
const candidates = found.candidates.filter((candidate2) => candidate2.subscriber === subscriber && (subscription === void 0 || candidate2.tuple.subscription.objectHash === subscription));
|
|
1452
1920
|
if (candidates.length === 0) {
|
|
1453
1921
|
throw new EventKitHandshakeClientError("candidate-not-found", "No exact finalized acceptance candidate was found");
|
|
@@ -1560,7 +2028,7 @@ function createPublisherHandshakeClient(options) {
|
|
|
1560
2028
|
const reconciliation = await reconcileAcceptance(acceptanceHash, publication, intent.challenge.subscriber, intent.challenge.subscription);
|
|
1561
2029
|
return Object.freeze({
|
|
1562
2030
|
acceptance: Object.freeze({ body: intent.acceptance, objectHash: acceptanceHash }),
|
|
1563
|
-
challenge: intent.
|
|
2031
|
+
challenge: intent.acceptance.challenge,
|
|
1564
2032
|
intentDisposition,
|
|
1565
2033
|
operationId: intent.operationId,
|
|
1566
2034
|
publication,
|
|
@@ -1603,14 +2071,14 @@ function createPublisherHandshakeClient(options) {
|
|
|
1603
2071
|
const found = await candidateDiscovery(selector);
|
|
1604
2072
|
if (!found.ok || selector.subscription === void 0)
|
|
1605
2073
|
return found;
|
|
1606
|
-
const subscription =
|
|
2074
|
+
const subscription = HashZod6.parse(selector.subscription);
|
|
1607
2075
|
return Object.freeze({
|
|
1608
2076
|
...found,
|
|
1609
2077
|
candidates: found.candidates.filter((candidate) => candidate.tuple.subscription.objectHash === subscription)
|
|
1610
2078
|
});
|
|
1611
2079
|
},
|
|
1612
2080
|
async beginChallenge(input) {
|
|
1613
|
-
const challengeHash =
|
|
2081
|
+
const challengeHash = HashZod6.parse(input.challengeHash);
|
|
1614
2082
|
const record = await options.challenges.get(challengeHash);
|
|
1615
2083
|
if (record === void 0)
|
|
1616
2084
|
return { kind: "missing" };
|
|
@@ -1640,9 +2108,9 @@ function createPublisherHandshakeClient(options) {
|
|
|
1640
2108
|
},
|
|
1641
2109
|
async completeChallengeAndAccept(input) {
|
|
1642
2110
|
await assertPublicationSource(options.publication, options.reader.publisher);
|
|
1643
|
-
const challengeHash =
|
|
1644
|
-
const subscriber =
|
|
1645
|
-
const subscription =
|
|
2111
|
+
const challengeHash = HashZod6.parse(input.challengeHash);
|
|
2112
|
+
const subscriber = NormalizedAddressZod4.parse(input.subscriber);
|
|
2113
|
+
const subscription = HashZod6.parse(input.subscription);
|
|
1646
2114
|
const record = await options.challenges.get(challengeHash);
|
|
1647
2115
|
if (record === void 0) {
|
|
1648
2116
|
throw new EventKitHandshakeClientError("challenge-not-found", "Endpoint challenge was not found");
|
|
@@ -1754,6 +2222,8 @@ function createPublisherHandshakeClient(options) {
|
|
|
1754
2222
|
validFromBlock
|
|
1755
2223
|
};
|
|
1756
2224
|
const service = createPublisherService({
|
|
2225
|
+
deliveryMode: input.deliveryMode,
|
|
2226
|
+
...input.bestEffort === void 0 ? {} : { bestEffort: input.bestEffort },
|
|
1757
2227
|
...serviceInput,
|
|
1758
2228
|
authorizationChainId: options.reader.chainId,
|
|
1759
2229
|
sources: input.sources,
|
|
@@ -1775,7 +2245,7 @@ function createPublisherHandshakeClient(options) {
|
|
|
1775
2245
|
},
|
|
1776
2246
|
async retireEncryptionKey(key) {
|
|
1777
2247
|
await assertPublicationSource(options.publication, options.reader.publisher);
|
|
1778
|
-
const keyHash =
|
|
2248
|
+
const keyHash = HashZod6.parse(key);
|
|
1779
2249
|
const requestFingerprint = await exactMutationRequestFingerprint({ key: keyHash });
|
|
1780
2250
|
let resolvedIntent = await loadMutationIntent(options.intents, options.reader, "publisher-encryption-key-retire", keyHash, options.reader.publisher, requestFingerprint);
|
|
1781
2251
|
let intentDisposition = "resumed";
|
|
@@ -1813,7 +2283,7 @@ function createPublisherHandshakeClient(options) {
|
|
|
1813
2283
|
},
|
|
1814
2284
|
async retireService(expectedService) {
|
|
1815
2285
|
await assertPublicationSource(options.publication, options.reader.publisher);
|
|
1816
|
-
const service =
|
|
2286
|
+
const service = HashZod6.parse(expectedService);
|
|
1817
2287
|
const requestFingerprint = await exactMutationRequestFingerprint({ service });
|
|
1818
2288
|
let resolvedIntent = await loadMutationIntent(options.intents, options.reader, "publisher-service-retire", service, options.reader.publisher, requestFingerprint);
|
|
1819
2289
|
let intentDisposition = "resumed";
|
|
@@ -1869,10 +2339,12 @@ function createPublisherHandshakeClient(options) {
|
|
|
1869
2339
|
preferredEncryptionKey: generated.descriptorHash,
|
|
1870
2340
|
revision: selected.service.body.revision + 1,
|
|
1871
2341
|
serviceId: selected.service.body.serviceId,
|
|
1872
|
-
supersedes:
|
|
2342
|
+
supersedes: HashZod6.parse(selected.service.objectHash),
|
|
1873
2343
|
validFromBlock
|
|
1874
2344
|
};
|
|
1875
2345
|
const service = createPublisherService({
|
|
2346
|
+
deliveryMode: selected.service.body.deliveryMode,
|
|
2347
|
+
...selected.service.body.bestEffort === void 0 ? {} : { bestEffort: selected.service.body.bestEffort },
|
|
1876
2348
|
...serviceInput,
|
|
1877
2349
|
authorizationChainId: options.reader.chainId,
|
|
1878
2350
|
sources: selected.service.body.events.map((event) => event.source),
|
|
@@ -1886,8 +2358,8 @@ function createPublisherHandshakeClient(options) {
|
|
|
1886
2358
|
privateKey: generated.privateKey,
|
|
1887
2359
|
publisher: options.reader.publisher,
|
|
1888
2360
|
requestFingerprint,
|
|
1889
|
-
selectedKey:
|
|
1890
|
-
selectedService:
|
|
2361
|
+
selectedKey: HashZod6.parse(selected.key.objectHash),
|
|
2362
|
+
selectedService: HashZod6.parse(selected.service.objectHash),
|
|
1891
2363
|
service,
|
|
1892
2364
|
source: options.reader.publisher,
|
|
1893
2365
|
version: 1
|
|
@@ -1914,7 +2386,7 @@ function tupleHashes(view) {
|
|
|
1914
2386
|
}
|
|
1915
2387
|
function createSubscriberHandshakeClient(options) {
|
|
1916
2388
|
assertPublicationIdentity(options.publication, options.reader);
|
|
1917
|
-
const subscriber =
|
|
2389
|
+
const subscriber = NormalizedAddressZod4.parse(options.subscriber);
|
|
1918
2390
|
const serviceId = options.serviceId === void 0 ? void 0 : HandshakeIdZod3.parse(options.serviceId);
|
|
1919
2391
|
async function snapshot() {
|
|
1920
2392
|
return readySnapshot(await options.reader.sync());
|
|
@@ -1994,7 +2466,7 @@ function createSubscriberHandshakeClient(options) {
|
|
|
1994
2466
|
const client = {
|
|
1995
2467
|
async revokeGrant(grant) {
|
|
1996
2468
|
await assertPublicationSource(options.publication, subscriber);
|
|
1997
|
-
const grantHash =
|
|
2469
|
+
const grantHash = HashZod6.parse(grant);
|
|
1998
2470
|
const requestFingerprint = await exactMutationRequestFingerprint({ grant: grantHash });
|
|
1999
2471
|
let resolvedIntent = await loadMutationIntent(options.intents, options.reader, "subscriber-grant-revoke", grantHash, subscriber, requestFingerprint);
|
|
2000
2472
|
let intentDisposition = "resumed";
|
|
@@ -2033,7 +2505,7 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2033
2505
|
},
|
|
2034
2506
|
async revokeSubscription(subscription) {
|
|
2035
2507
|
await assertPublicationSource(options.publication, subscriber);
|
|
2036
|
-
const subscriptionHash =
|
|
2508
|
+
const subscriptionHash = HashZod6.parse(subscription);
|
|
2037
2509
|
const requestFingerprint = await exactMutationRequestFingerprint({ subscription: subscriptionHash });
|
|
2038
2510
|
let resolvedIntent = await loadMutationIntent(options.intents, options.reader, "subscriber-subscription-revoke", subscriptionHash, subscriber, requestFingerprint);
|
|
2039
2511
|
let intentDisposition = "resumed";
|
|
@@ -2075,7 +2547,7 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2075
2547
|
return await current.readModel.discoverPublisherService(serviceQuery(options.reader, current, serviceId));
|
|
2076
2548
|
},
|
|
2077
2549
|
async subscription(subscription) {
|
|
2078
|
-
const subscriptionHash =
|
|
2550
|
+
const subscriptionHash = HashZod6.parse(subscription);
|
|
2079
2551
|
const current = await options.reader.sync();
|
|
2080
2552
|
const view = await lookupAt(current);
|
|
2081
2553
|
if (!view.ok)
|
|
@@ -2133,12 +2605,12 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2133
2605
|
grant: grantHash,
|
|
2134
2606
|
publisher: options.reader.publisher,
|
|
2135
2607
|
publisherKey: {
|
|
2136
|
-
encryptionKey:
|
|
2608
|
+
encryptionKey: HashZod6.parse(selected.key.objectHash),
|
|
2137
2609
|
keyId: selected.key.body.keyId,
|
|
2138
2610
|
publicKey: selected.key.body.publicKey
|
|
2139
2611
|
},
|
|
2140
2612
|
...input.revision === void 0 ? {} : { revision: input.revision },
|
|
2141
|
-
service:
|
|
2613
|
+
service: HashZod6.parse(selected.service.objectHash),
|
|
2142
2614
|
subscriber,
|
|
2143
2615
|
...input.subscriptionId === void 0 ? {} : { subscriptionId: input.subscriptionId },
|
|
2144
2616
|
...input.supersedes === void 0 ? {} : { supersedes: input.supersedes }
|
|
@@ -2152,8 +2624,8 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2152
2624
|
operationId,
|
|
2153
2625
|
publisher: options.reader.publisher,
|
|
2154
2626
|
requestFingerprint,
|
|
2155
|
-
selectedKey:
|
|
2156
|
-
selectedService:
|
|
2627
|
+
selectedKey: HashZod6.parse(selected.key.objectHash),
|
|
2628
|
+
selectedService: HashZod6.parse(selected.service.objectHash),
|
|
2157
2629
|
service: selected.service.body,
|
|
2158
2630
|
source: subscriber,
|
|
2159
2631
|
subscription,
|
|
@@ -2163,30 +2635,570 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2163
2635
|
},
|
|
2164
2636
|
subscriptions: async (subscriptionId) => await lookup2(subscriptionId)
|
|
2165
2637
|
};
|
|
2166
|
-
return Object.freeze(client);
|
|
2638
|
+
return Object.freeze(client);
|
|
2639
|
+
}
|
|
2640
|
+
function subscriberPlaintext(service, input, maxPositionsPerWake) {
|
|
2641
|
+
if (input.deliveryMode !== service.deliveryMode || canonicalJson2(input.bestEffort ?? null) !== canonicalJson2(service.bestEffort ?? null))
|
|
2642
|
+
throw new Error("Requested delivery terms do not match exact selected service");
|
|
2643
|
+
const plaintextInput = {
|
|
2644
|
+
audience: input.audience,
|
|
2645
|
+
challengeSecret: input.challengeSecret,
|
|
2646
|
+
deploymentId: input.deploymentId,
|
|
2647
|
+
endpoint: input.endpoint,
|
|
2648
|
+
queueId: input.queueId
|
|
2649
|
+
};
|
|
2650
|
+
const source = input.source;
|
|
2651
|
+
if (service.events.every((event) => canonicalJson2(event.source) !== canonicalJson2(source))) {
|
|
2652
|
+
throw new Error("Requested source is not advertised by the selected service");
|
|
2653
|
+
}
|
|
2654
|
+
return createWakeSubscriptionPlaintext({
|
|
2655
|
+
deliveryMode: input.deliveryMode,
|
|
2656
|
+
...input.bestEffort === void 0 ? {} : { bestEffort: input.bestEffort },
|
|
2657
|
+
...plaintextInput,
|
|
2658
|
+
source,
|
|
2659
|
+
fromPosition: input.fromPosition,
|
|
2660
|
+
maxPositionsPerWake
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
// src/receiverMigration.ts
|
|
2665
|
+
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
2666
|
+
import {
|
|
2667
|
+
link as link3,
|
|
2668
|
+
lstat as lstat4,
|
|
2669
|
+
mkdir as mkdir3,
|
|
2670
|
+
mkdtemp,
|
|
2671
|
+
open as open5,
|
|
2672
|
+
realpath,
|
|
2673
|
+
rm,
|
|
2674
|
+
unlink as unlink4
|
|
2675
|
+
} from "node:fs/promises";
|
|
2676
|
+
import OS from "node:os";
|
|
2677
|
+
import PATH15 from "node:path";
|
|
2678
|
+
import {
|
|
2679
|
+
HashZod as HashZod8,
|
|
2680
|
+
NormalizedAddressZod as NormalizedAddressZod6,
|
|
2681
|
+
WakeIdZod as WakeIdZod3,
|
|
2682
|
+
WakeJwtAudienceZod as WakeJwtAudienceZod3
|
|
2683
|
+
} from "@xyo-network/event-kit-schemas";
|
|
2684
|
+
|
|
2685
|
+
// src/receiverMigrationInventory.ts
|
|
2686
|
+
import { readFile } from "node:fs/promises";
|
|
2687
|
+
import PATH14 from "node:path";
|
|
2688
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
2689
|
+
import { parseAdmittedWake as parseAdmittedWake3 } from "@xyo-network/event-kit-protocol";
|
|
2690
|
+
import {
|
|
2691
|
+
HashZod as HashZod7,
|
|
2692
|
+
NonnegativeJsonSafeIntegerZod as NonnegativeJsonSafeIntegerZod4,
|
|
2693
|
+
NormalizedAddressZod as NormalizedAddressZod5,
|
|
2694
|
+
WakeIdZod as WakeIdZod2,
|
|
2695
|
+
WakeJwtAudienceZod as WakeJwtAudienceZod2,
|
|
2696
|
+
WakeJwtIdZod as WakeJwtIdZod2
|
|
2697
|
+
} from "@xyo-network/event-kit-schemas";
|
|
2698
|
+
import { z as z3 } from "zod";
|
|
2699
|
+
|
|
2700
|
+
// src/receiverMigrationSnapshot.ts
|
|
2701
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2702
|
+
import { constants as constants2 } from "node:fs";
|
|
2703
|
+
import {
|
|
2704
|
+
lstat as lstat3,
|
|
2705
|
+
mkdir as mkdir2,
|
|
2706
|
+
open as open4,
|
|
2707
|
+
readdir as readdir2
|
|
2708
|
+
} from "node:fs/promises";
|
|
2709
|
+
import PATH13 from "node:path";
|
|
2710
|
+
function migrationRelativePath(root, relative) {
|
|
2711
|
+
if (PATH13.isAbsolute(relative)) throw new TypeError("Migration store paths must be relative");
|
|
2712
|
+
const resolved = PATH13.resolve(root, relative);
|
|
2713
|
+
if (resolved !== root && !resolved.startsWith(`${root}${PATH13.sep}`)) throw new TypeError("Migration path escapes its source store");
|
|
2714
|
+
return resolved;
|
|
2715
|
+
}
|
|
2716
|
+
async function readExact(path) {
|
|
2717
|
+
const file = await open4(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
2718
|
+
try {
|
|
2719
|
+
const before = await file.stat();
|
|
2720
|
+
if (!before.isFile()) throw new TypeError("Migration source must contain ordinary files only");
|
|
2721
|
+
const bytes = await file.readFile();
|
|
2722
|
+
const after = await file.stat();
|
|
2723
|
+
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ino !== after.ino) {
|
|
2724
|
+
throw new Error("Migration source changed while being copied");
|
|
2725
|
+
}
|
|
2726
|
+
return { bytes, mode: before.mode & 511 };
|
|
2727
|
+
} finally {
|
|
2728
|
+
await file.close();
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
async function snapshotMigrationSource(root, destination) {
|
|
2732
|
+
const files = [];
|
|
2733
|
+
async function visit(relative) {
|
|
2734
|
+
const source = PATH13.join(root, relative);
|
|
2735
|
+
const info = await lstat3(source);
|
|
2736
|
+
if (info.isSymbolicLink()) throw new TypeError("Migration source symlinks are unsupported");
|
|
2737
|
+
if (info.isDirectory()) {
|
|
2738
|
+
if (destination !== void 0) await mkdir2(PATH13.join(destination, relative), { mode: 448 });
|
|
2739
|
+
for (const child of (await readdir2(source)).toSorted()) await visit(PATH13.join(relative, child));
|
|
2740
|
+
if (destination !== void 0) await syncDirectory(PATH13.join(destination, relative));
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
const { bytes, mode } = await readExact(source);
|
|
2744
|
+
files.push({
|
|
2745
|
+
bytes: bytes.length,
|
|
2746
|
+
mode,
|
|
2747
|
+
path: relative,
|
|
2748
|
+
sha256: createHash3("sha256").update(bytes).digest("hex")
|
|
2749
|
+
});
|
|
2750
|
+
if (destination === void 0) return;
|
|
2751
|
+
const target = await open4(PATH13.join(destination, relative), "wx", 384);
|
|
2752
|
+
try {
|
|
2753
|
+
await target.writeFile(bytes);
|
|
2754
|
+
await target.sync();
|
|
2755
|
+
} finally {
|
|
2756
|
+
await target.close();
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
await visit("");
|
|
2760
|
+
return files;
|
|
2761
|
+
}
|
|
2762
|
+
async function verifyMigrationSource(root, files) {
|
|
2763
|
+
const current = await snapshotMigrationSource(root);
|
|
2764
|
+
if (JSON.stringify(current) !== JSON.stringify(files)) throw new Error("Migration source changed; stop every owner and repeat on a new copy");
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
// src/receiverMigrationInventory.ts
|
|
2768
|
+
var ReceiptZod = z3.strictObject({
|
|
2769
|
+
audience: WakeJwtAudienceZod2,
|
|
2770
|
+
eventHash: HashZod7,
|
|
2771
|
+
id: HashZod7,
|
|
2772
|
+
publisher: NormalizedAddressZod5,
|
|
2773
|
+
wakeId: WakeIdZod2
|
|
2774
|
+
});
|
|
2775
|
+
var recordShape = {
|
|
2776
|
+
audience: WakeJwtAudienceZod2,
|
|
2777
|
+
eventHash: HashZod7,
|
|
2778
|
+
publisher: NormalizedAddressZod5,
|
|
2779
|
+
receipt: ReceiptZod,
|
|
2780
|
+
wakeId: WakeIdZod2
|
|
2781
|
+
};
|
|
2782
|
+
var WakeRecordZod = z3.strictObject(recordShape);
|
|
2783
|
+
var TokenRecordZod = z3.strictObject({
|
|
2784
|
+
...recordShape,
|
|
2785
|
+
jwtId: WakeJwtIdZod2,
|
|
2786
|
+
retainJwtUntil: NonnegativeJsonSafeIntegerZod4
|
|
2787
|
+
});
|
|
2788
|
+
function validateMigrationReceipt(receipt) {
|
|
2789
|
+
const parsed = ReceiptZod.parse(receipt);
|
|
2790
|
+
if (parsed.id !== receiverIdentity([parsed.audience, parsed.publisher, parsed.wakeId])) throw new TypeError("Legacy receipt ID does not match its scoped identity");
|
|
2791
|
+
return parsed;
|
|
2792
|
+
}
|
|
2793
|
+
function validateRecord(record) {
|
|
2794
|
+
const receipt = validateMigrationReceipt(record.receipt);
|
|
2795
|
+
if (receipt.audience !== record.audience || receipt.publisher !== record.publisher || receipt.wakeId !== record.wakeId || receipt.eventHash !== record.eventHash) {
|
|
2796
|
+
throw new TypeError("Legacy receipt does not match its record");
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
function scope(value) {
|
|
2800
|
+
return receiverIdentity([value.audience, value.publisher, value.wakeId]);
|
|
2801
|
+
}
|
|
2802
|
+
function issue(group, reason) {
|
|
2803
|
+
return {
|
|
2804
|
+
identity: group.identity,
|
|
2805
|
+
paths: [...group.paths],
|
|
2806
|
+
reason
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
function admissionIssue(group, receipt, wake, review) {
|
|
2810
|
+
if (receipt?.id !== group.identity) return issue(group, "Admission receipt is missing or disagrees with reviewed identity");
|
|
2811
|
+
if (review?.action !== "completed" && (wake === void 0 || scope(wake) !== group.identity || wake.eventHash !== receipt.eventHash)) {
|
|
2812
|
+
return issue(group, "Receipt has no exact validated pending body; completion remains unknown");
|
|
2813
|
+
}
|
|
2814
|
+
if (group.tokens.some((token) => token.eventHash !== receipt.eventHash || JSON.stringify(token.receipt) !== JSON.stringify(receipt))) {
|
|
2815
|
+
return issue(group, "Token history conflicts with the reviewed receipt");
|
|
2816
|
+
}
|
|
2817
|
+
return review === void 0 && (group.reasons.length > 0 || group.receipts.some((record) => record.eventHash !== receipt.eventHash) || group.bodies.some((body) => body.wake.eventHash !== receipt.eventHash)) ? issue(group, "Legacy body/receipt mappings disagree; explicit review is required") : void 0;
|
|
2818
|
+
}
|
|
2819
|
+
function queueIssue(group, receipt, wake, review) {
|
|
2820
|
+
return group.reasons.length > 0 || wake === void 0 || receipt !== void 0 || review?.action === "completed" ? issue(group, "Standalone queue migration cannot infer receipt or completion history") : void 0;
|
|
2821
|
+
}
|
|
2822
|
+
async function selectEntry(group, review) {
|
|
2823
|
+
const receipt = review?.receipt === void 0 ? group.receipts[0]?.receipt : validateMigrationReceipt(review.receipt);
|
|
2824
|
+
const wake = review?.wake === void 0 ? group.bodies.find((body) => receipt === void 0 || body.wake.eventHash === receipt.eventHash)?.wake : await parseAdmittedWake3(review.wake);
|
|
2825
|
+
const invalid = group.queueOnly ? queueIssue(group, receipt, wake, review) : admissionIssue(group, receipt, wake, review);
|
|
2826
|
+
if (invalid !== void 0) return invalid;
|
|
2827
|
+
const queueOrder = review?.action === "completed" ? void 0 : selectQueueOrder(group, wake, review);
|
|
2828
|
+
return queueOrder !== void 0 && !("kind" in queueOrder) ? queueOrder : migrationEntry(group, receipt, wake, queueOrder, review);
|
|
2829
|
+
}
|
|
2830
|
+
function migrationEntry(group, receipt, wake, queueOrder, review) {
|
|
2831
|
+
return {
|
|
2832
|
+
evidence: review?.evidence ?? "Exact validated legacy receipt and pending/inflight body; no fresh grant inferred",
|
|
2833
|
+
identity: group.identity,
|
|
2834
|
+
paths: [...group.paths],
|
|
2835
|
+
...receipt === void 0 ? {} : { receipt },
|
|
2836
|
+
...queueOrder === void 0 ? {} : { queueOrder },
|
|
2837
|
+
state: review?.action === "completed" ? "completed" : "pending",
|
|
2838
|
+
tokens: group.tokens,
|
|
2839
|
+
...wake === void 0 ? {} : { wake }
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
function compareQueueOrder(left, right) {
|
|
2843
|
+
if (left.kind === "reviewed-after-known") return right.kind === "reviewed-after-known" ? 0 : 1;
|
|
2844
|
+
if (right.kind === "reviewed-after-known") return -1;
|
|
2845
|
+
if (left.kind === "sqlite-row-id" && right.kind === "sqlite-row-id") return left.id - right.id;
|
|
2846
|
+
if (left.kind === "filesystem-entry-name" && right.kind === "filesystem-entry-name") {
|
|
2847
|
+
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
2848
|
+
}
|
|
2849
|
+
throw new TypeError("Migration cannot combine unrelated source queue orders");
|
|
2850
|
+
}
|
|
2851
|
+
function selectQueueOrder(group, wake, review) {
|
|
2852
|
+
const matches = group.bodies.filter((body) => body.wake.eventHash === wake.eventHash).toSorted((left, right) => compareQueueOrder(left.order, right.order));
|
|
2853
|
+
const original = matches[0]?.order;
|
|
2854
|
+
if (original !== void 0) return review?.queueOrder === void 0 ? original : issue(group, "Reviewed placement cannot override a matching original queue reference");
|
|
2855
|
+
return review?.queueOrder === void 0 ? issue(group, "Original queue order is unknown; an explicit after-known placement review is required") : { kind: "reviewed-after-known", evidence: review.queueOrder.evidence };
|
|
2856
|
+
}
|
|
2857
|
+
function compareEntries(left, right) {
|
|
2858
|
+
if (left.state === "completed") return right.state === "completed" ? 0 : 1;
|
|
2859
|
+
return right.state === "completed" ? -1 : compareQueueOrder(left.queueOrder, right.queueOrder);
|
|
2860
|
+
}
|
|
2861
|
+
function createCollector() {
|
|
2862
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2863
|
+
const groupFor = (identity, path, queueOnly = false) => {
|
|
2864
|
+
let group = groups.get(identity);
|
|
2865
|
+
if (group === void 0) {
|
|
2866
|
+
group = {
|
|
2867
|
+
identity,
|
|
2868
|
+
paths: /* @__PURE__ */ new Set(),
|
|
2869
|
+
bodies: [],
|
|
2870
|
+
receipts: [],
|
|
2871
|
+
reasons: [],
|
|
2872
|
+
tokens: [],
|
|
2873
|
+
queueOnly
|
|
2874
|
+
};
|
|
2875
|
+
groups.set(identity, group);
|
|
2876
|
+
}
|
|
2877
|
+
group.paths.add(path);
|
|
2878
|
+
return group;
|
|
2879
|
+
};
|
|
2880
|
+
return {
|
|
2881
|
+
groups,
|
|
2882
|
+
groupFor,
|
|
2883
|
+
recognized: /* @__PURE__ */ new Set(),
|
|
2884
|
+
invalid(path, reason) {
|
|
2885
|
+
groupFor(`artifact:${path}`, path).reasons.push(reason);
|
|
2886
|
+
}
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2889
|
+
function filesystemKind(queueRelative, inboxRelative) {
|
|
2890
|
+
if (/^(pending|inflight)\/[\w.-]+\.json$/.test(queueRelative)) return "body";
|
|
2891
|
+
if (/^wakes\/[a-f0-9]{64}\.json$/.test(inboxRelative)) return "receipt";
|
|
2892
|
+
return /^tokens\/[a-f0-9]{64}\.json$/.test(inboxRelative) ? "token" : void 0;
|
|
2893
|
+
}
|
|
2894
|
+
async function readFilesystemRecord(collector, absolute, path, kind) {
|
|
2895
|
+
const raw = JSON.parse(await readFile(absolute, "utf8"));
|
|
2896
|
+
if (kind === "body") {
|
|
2897
|
+
const wake = await parseAdmittedWake3(raw);
|
|
2898
|
+
collector.groupFor(scope(wake), path).bodies.push({
|
|
2899
|
+
order: {
|
|
2900
|
+
kind: "filesystem-entry-name",
|
|
2901
|
+
name: PATH14.basename(path),
|
|
2902
|
+
path
|
|
2903
|
+
},
|
|
2904
|
+
path,
|
|
2905
|
+
wake
|
|
2906
|
+
});
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
const record = kind === "receipt" ? WakeRecordZod.parse(raw) : TokenRecordZod.parse(raw);
|
|
2910
|
+
validateRecord(record);
|
|
2911
|
+
const key = kind === "receipt" ? scope(record) : receiverIdentity([record.audience, record.publisher, record.jwtId]);
|
|
2912
|
+
if (PATH14.basename(path) !== `${key}.json`) throw new TypeError("Legacy record filename disagrees with identity");
|
|
2913
|
+
const group = collector.groupFor(scope(record), path);
|
|
2914
|
+
if (kind === "receipt") group.receipts.push(record);
|
|
2915
|
+
else group.tokens.push(record);
|
|
2916
|
+
}
|
|
2917
|
+
async function scanFilesystem(source, root, files, collector) {
|
|
2918
|
+
const queueRoot = migrationRelativePath(root, source.queueDirectory);
|
|
2919
|
+
const inboxRoot = migrationRelativePath(root, source.inboxDirectory);
|
|
2920
|
+
for (const file of files) {
|
|
2921
|
+
const absolute = PATH14.join(root, file.path);
|
|
2922
|
+
const kind = filesystemKind(PATH14.relative(queueRoot, absolute), PATH14.relative(inboxRoot, absolute));
|
|
2923
|
+
if (kind === void 0) continue;
|
|
2924
|
+
collector.recognized.add(file.path);
|
|
2925
|
+
try {
|
|
2926
|
+
await readFilesystemRecord(collector, absolute, file.path, kind);
|
|
2927
|
+
} catch {
|
|
2928
|
+
collector.invalid(file.path, "Malformed, corrupt, or incorrectly bound legacy record; exact bytes archived");
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
async function scanSqlite(source, root, collector) {
|
|
2933
|
+
if (source.scope !== "queue-only") throw new TypeError("SQLite queue migration requires explicit queue-only scope");
|
|
2934
|
+
const file = migrationRelativePath(root, source.databaseFile);
|
|
2935
|
+
for (const name of [source.databaseFile, `${source.databaseFile}-wal`, `${source.databaseFile}-shm`, `${source.databaseFile}-journal`]) collector.recognized.add(PATH14.normalize(name));
|
|
2936
|
+
const database = new DatabaseSync2(file, { readOnly: true });
|
|
2937
|
+
try {
|
|
2938
|
+
const tables = database.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
|
|
2939
|
+
if (tables.length !== 1 || tables[0]?.name !== "wake_queue") throw new TypeError("Expected a legacy queue-only SQLite store");
|
|
2940
|
+
for (const row of database.prepare("SELECT id,body,state FROM wake_queue ORDER BY id").all()) {
|
|
2941
|
+
const path = `${source.databaseFile}#wake_queue/${String(row.id)}`;
|
|
2942
|
+
try {
|
|
2943
|
+
if (typeof row.id !== "number" || !Number.isSafeInteger(row.id) || typeof row.body !== "string" || row.state !== "pending" && row.state !== "inflight") throw new TypeError("Invalid legacy queue row");
|
|
2944
|
+
const wake = await parseAdmittedWake3(JSON.parse(row.body));
|
|
2945
|
+
collector.groupFor(`queue:${String(row.id)}`, path, true).bodies.push({
|
|
2946
|
+
order: {
|
|
2947
|
+
kind: "sqlite-row-id",
|
|
2948
|
+
id: row.id,
|
|
2949
|
+
path
|
|
2950
|
+
},
|
|
2951
|
+
path,
|
|
2952
|
+
wake
|
|
2953
|
+
});
|
|
2954
|
+
} catch {
|
|
2955
|
+
collector.invalid(path, "Invalid legacy SQLite queue row; exact database bytes archived");
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
} finally {
|
|
2959
|
+
database.close();
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
async function inventoryReceiverMigration(source, copiedRoot, files, reviews) {
|
|
2963
|
+
const collector = createCollector();
|
|
2964
|
+
if (source.kind === "filesystem") await scanFilesystem(source, copiedRoot, files, collector);
|
|
2965
|
+
else await scanSqlite(source, copiedRoot, collector);
|
|
2966
|
+
for (const file of files) {
|
|
2967
|
+
if (!collector.recognized.has(file.path)) collector.invalid(file.path, "Unclassified source artifact; explicit review or quarantine is required");
|
|
2968
|
+
}
|
|
2969
|
+
return await finalizeGroups(collector.groups, reviews);
|
|
2970
|
+
}
|
|
2971
|
+
function reviewMap(groups, reviews) {
|
|
2972
|
+
const reviewed = /* @__PURE__ */ new Map();
|
|
2973
|
+
for (const review of reviews) {
|
|
2974
|
+
if (!["pending", "completed", "quarantine"].includes(review.action) || review.evidence.trim().length === 0 || reviewed.has(review.identity)) throw new TypeError("Reviews require unique identities, an explicit action and external evidence references");
|
|
2975
|
+
if (!groups.has(review.identity)) throw new TypeError("Review does not correspond to an inventoried identity");
|
|
2976
|
+
if (review.queueOrder !== void 0 && (review.action !== "pending" || review.queueOrder.kind !== "after-known" || review.queueOrder.evidence.trim().length === 0)) throw new TypeError("Queue placement requires a pending review, after-known choice and separate placement evidence");
|
|
2977
|
+
reviewed.set(review.identity, review);
|
|
2978
|
+
}
|
|
2979
|
+
return reviewed;
|
|
2980
|
+
}
|
|
2981
|
+
async function finalizeGroups(groups, reviews) {
|
|
2982
|
+
const reviewed = reviewMap(groups, reviews);
|
|
2983
|
+
const entries = [];
|
|
2984
|
+
const quarantined = [];
|
|
2985
|
+
const unknown = [];
|
|
2986
|
+
for (const group of groups.values()) {
|
|
2987
|
+
const review = reviewed.get(group.identity);
|
|
2988
|
+
if (review?.action === "quarantine") {
|
|
2989
|
+
quarantined.push(issue(group, review.evidence));
|
|
2990
|
+
continue;
|
|
2991
|
+
}
|
|
2992
|
+
if (group.identity.startsWith("artifact:")) {
|
|
2993
|
+
unknown.push(issue(group, group.reasons.join("; ")));
|
|
2994
|
+
continue;
|
|
2995
|
+
}
|
|
2996
|
+
const selected = await selectEntry(group, review);
|
|
2997
|
+
if ("reason" in selected) unknown.push(selected);
|
|
2998
|
+
else {
|
|
2999
|
+
entries.push(selected);
|
|
3000
|
+
const rejectedBodies = group.bodies.filter((body) => selected.receipt !== void 0 && body.wake.eventHash !== selected.receipt.eventHash);
|
|
3001
|
+
if (rejectedBodies.length > 0) quarantined.push({
|
|
3002
|
+
identity: `reviewed-conflicts:${group.identity}`,
|
|
3003
|
+
paths: rejectedBodies.map((body) => body.path),
|
|
3004
|
+
reason: "Reviewed mapping excluded conflicting bodies; exact bytes remain in the backup"
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
return {
|
|
3009
|
+
entries: entries.toSorted(compareEntries),
|
|
3010
|
+
quarantined,
|
|
3011
|
+
unknown
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
// src/receiverMigration.ts
|
|
3016
|
+
var rollback = "restore-backup-only-before-new-writers-resume; after-that-repair-forward; reverse-migration-unsupported";
|
|
3017
|
+
async function outsideSource(source, destination) {
|
|
3018
|
+
let parent = PATH15.dirname(PATH15.resolve(destination));
|
|
3019
|
+
const suffix2 = [PATH15.basename(destination)];
|
|
3020
|
+
let canonicalParent;
|
|
3021
|
+
for (; ; ) {
|
|
3022
|
+
try {
|
|
3023
|
+
canonicalParent = await realpath(parent);
|
|
3024
|
+
break;
|
|
3025
|
+
} catch (error) {
|
|
3026
|
+
if (typeof error !== "object" || error === null || !("code" in error) || error.code !== "ENOENT") throw error;
|
|
3027
|
+
suffix2.unshift(PATH15.basename(parent));
|
|
3028
|
+
parent = PATH15.dirname(parent);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
const resolved = PATH15.join(canonicalParent, ...suffix2);
|
|
3032
|
+
if (resolved === source || resolved.startsWith(`${source}${PATH15.sep}`)) throw new TypeError("Migration output must be outside the source store");
|
|
3033
|
+
return resolved;
|
|
3034
|
+
}
|
|
3035
|
+
async function writeReport(path, report) {
|
|
3036
|
+
const file = await open5(path, "wx", 384);
|
|
3037
|
+
try {
|
|
3038
|
+
await file.writeFile(`${JSON.stringify(report, null, 2)}
|
|
3039
|
+
`);
|
|
3040
|
+
await file.sync();
|
|
3041
|
+
} finally {
|
|
3042
|
+
await file.close();
|
|
3043
|
+
}
|
|
3044
|
+
await syncDirectory(PATH15.dirname(path));
|
|
3045
|
+
}
|
|
3046
|
+
function importEntries(database, entries) {
|
|
3047
|
+
database.exec("BEGIN IMMEDIATE");
|
|
3048
|
+
try {
|
|
3049
|
+
for (const entry of entries) {
|
|
3050
|
+
const terminalToken = entry.state === "completed" ? randomBytes5(16).toString("hex") : null;
|
|
3051
|
+
const result = database.prepare("INSERT INTO receiver_queue(body,state,terminal_token,terminal_operation) VALUES(?,?,?,?)").run(entry.state === "completed" ? null : JSON.stringify(entry.wake), entry.state, terminalToken, terminalToken === null ? null : "ack");
|
|
3052
|
+
if (entry.receipt === void 0) continue;
|
|
3053
|
+
const receipt = entry.receipt;
|
|
3054
|
+
database.prepare("INSERT INTO receiver_wakes(identity,audience,publisher,wake_id,event_hash,receipt,authorization,queue_id) VALUES(?,?,?,?,?,?,?,?)").run(
|
|
3055
|
+
receipt.id,
|
|
3056
|
+
receipt.audience,
|
|
3057
|
+
receipt.publisher,
|
|
3058
|
+
receipt.wakeId,
|
|
3059
|
+
receipt.eventHash,
|
|
3060
|
+
JSON.stringify(receipt),
|
|
3061
|
+
JSON.stringify({
|
|
3062
|
+
kind: "legacy-migration",
|
|
3063
|
+
evidence: entry.evidence,
|
|
3064
|
+
artifacts: entry.paths,
|
|
3065
|
+
freshAuthorization: false,
|
|
3066
|
+
...terminalToken === null ? {} : { completion: { kind: "reviewed-migration-decision", terminalToken } }
|
|
3067
|
+
}),
|
|
3068
|
+
result.lastInsertRowid
|
|
3069
|
+
);
|
|
3070
|
+
for (const token of entry.tokens) {
|
|
3071
|
+
database.prepare("INSERT INTO receiver_tokens(identity,wake_identity,event_hash,wake_id,retain_until) VALUES(?,?,?,?,?)").run(receiverIdentity([token.audience, token.publisher, token.jwtId]), receipt.id, token.eventHash, token.wakeId, token.retainJwtUntil);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
database.exec("COMMIT");
|
|
3075
|
+
} catch (error) {
|
|
3076
|
+
database.exec("ROLLBACK");
|
|
3077
|
+
throw error;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
async function publishMigratedStore(target, entries, verifySource) {
|
|
3081
|
+
ensurePrivateDirectory(PATH15.dirname(target));
|
|
3082
|
+
const staging = PATH15.join(PATH15.dirname(target), `.receiver-migration-${randomBytes5(8).toString("hex")}`);
|
|
3083
|
+
await mkdir3(staging, { mode: 448 });
|
|
3084
|
+
const file = PATH15.join(staging, "receiver.sqlite");
|
|
3085
|
+
try {
|
|
3086
|
+
const receiver = openReceiverAuthority(file);
|
|
3087
|
+
try {
|
|
3088
|
+
importEntries(receiver.database, entries);
|
|
3089
|
+
receiver.database.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
3090
|
+
} finally {
|
|
3091
|
+
receiver.close();
|
|
3092
|
+
}
|
|
3093
|
+
await verifySource();
|
|
3094
|
+
await link3(file, target);
|
|
3095
|
+
await syncDirectory(PATH15.dirname(target));
|
|
3096
|
+
await unlink4(file);
|
|
3097
|
+
} finally {
|
|
3098
|
+
await rm(staging, { recursive: true, force: true });
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
async function prepareLocations(options) {
|
|
3102
|
+
if (options?.consumersStopped !== true) throw new TypeError("Migration requires every source writer and consumer to be stopped");
|
|
3103
|
+
if (!["filesystem", "sqlite-queue"].includes(options.source.kind)) throw new TypeError("Unknown receiver migration source format");
|
|
3104
|
+
if (options.source.kind === "sqlite-queue" && options.source.scope !== "queue-only") throw new TypeError("SQLite queue migration requires explicit queue-only scope");
|
|
3105
|
+
const sourcePath = await realpath(options.source.path);
|
|
3106
|
+
if (!(await lstat4(options.source.path)).isDirectory()) throw new TypeError("Migration source must be an owned store directory");
|
|
3107
|
+
const backupPath = options.backupPath === void 0 ? void 0 : await outsideSource(sourcePath, options.backupPath);
|
|
3108
|
+
const targetPath = options.targetPath === void 0 ? void 0 : await outsideSource(sourcePath, options.targetPath);
|
|
3109
|
+
if (!options.dryRun && (backupPath === void 0 || targetPath === void 0)) throw new TypeError("Execution requires distinct backupPath and targetPath");
|
|
3110
|
+
if (backupPath !== void 0 && targetPath !== void 0 && (targetPath === backupPath || targetPath.startsWith(`${backupPath}${PATH15.sep}`) || backupPath.startsWith(`${targetPath}${PATH15.sep}`))) {
|
|
3111
|
+
throw new TypeError("Backup and target paths must be separate");
|
|
3112
|
+
}
|
|
3113
|
+
return {
|
|
3114
|
+
backupPath,
|
|
3115
|
+
sourcePath,
|
|
3116
|
+
targetPath
|
|
3117
|
+
};
|
|
2167
3118
|
}
|
|
2168
|
-
function
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
3119
|
+
async function prepareCopy(options, paths, scratch) {
|
|
3120
|
+
if (options.dryRun) return await snapshotMigrationSource(paths.sourcePath, PATH15.join(scratch, "source"));
|
|
3121
|
+
await mkdir3(PATH15.dirname(paths.backupPath), { recursive: true, mode: 448 });
|
|
3122
|
+
await mkdir3(paths.backupPath, { mode: 448 });
|
|
3123
|
+
const files = await snapshotMigrationSource(paths.sourcePath, PATH15.join(paths.backupPath, "source"));
|
|
3124
|
+
await syncDirectory(paths.backupPath);
|
|
3125
|
+
await snapshotMigrationSource(PATH15.join(paths.backupPath, "source"), PATH15.join(scratch, "source"));
|
|
3126
|
+
return files;
|
|
3127
|
+
}
|
|
3128
|
+
function migrationReport(paths, files, plan, options) {
|
|
3129
|
+
const dryRun = options.dryRun === true;
|
|
3130
|
+
const ready = plan.unknown.length === 0;
|
|
3131
|
+
return {
|
|
3132
|
+
...paths.backupPath === void 0 || dryRun ? {} : { backupPath: paths.backupPath },
|
|
3133
|
+
completedIdentities: plan.entries.filter((entry) => entry.state === "completed").length,
|
|
3134
|
+
executed: ready && !dryRun,
|
|
3135
|
+
files,
|
|
3136
|
+
pendingIdentities: plan.entries.filter((entry) => entry.state === "pending").length,
|
|
3137
|
+
quarantined: plan.quarantined,
|
|
3138
|
+
ready,
|
|
3139
|
+
reviews: (options.reviews ?? []).map((review) => ({
|
|
3140
|
+
action: review.action,
|
|
3141
|
+
evidence: review.evidence,
|
|
3142
|
+
identity: review.identity,
|
|
3143
|
+
...review.queueOrder === void 0 ? {} : { queueOrder: review.queueOrder }
|
|
3144
|
+
})),
|
|
3145
|
+
rollback,
|
|
3146
|
+
selected: plan.entries.map((entry) => ({
|
|
3147
|
+
...entry.state === "completed" ? { completionProvenance: "Fresh reviewed migration tombstone; no historical lease or ack inferred" } : {},
|
|
3148
|
+
eventHash: entry.receipt?.eventHash ?? entry.wake?.eventHash,
|
|
3149
|
+
identity: entry.identity,
|
|
3150
|
+
paths: entry.paths,
|
|
3151
|
+
...entry.queueOrder === void 0 ? {} : { queueOrder: entry.queueOrder },
|
|
3152
|
+
state: entry.state
|
|
3153
|
+
})),
|
|
3154
|
+
sourceBytes: files.reduce((total, file) => total + file.bytes, 0),
|
|
3155
|
+
sourcePath: paths.sourcePath,
|
|
3156
|
+
sourceUnchanged: true,
|
|
3157
|
+
tokenRecords: plan.entries.reduce((total, entry) => total + entry.tokens.length, 0),
|
|
3158
|
+
unknown: plan.unknown,
|
|
3159
|
+
...paths.targetPath === void 0 || dryRun ? {} : { targetPath: paths.targetPath }
|
|
2175
3160
|
};
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
3161
|
+
}
|
|
3162
|
+
async function migrateReceiverStore(options) {
|
|
3163
|
+
const paths = await prepareLocations(options);
|
|
3164
|
+
const scratch = await mkdtemp(PATH15.join(OS.tmpdir(), "event-kit-receiver-migration-"));
|
|
3165
|
+
try {
|
|
3166
|
+
const files = await prepareCopy(options, paths, scratch);
|
|
3167
|
+
const verify = async () => {
|
|
3168
|
+
await verifyMigrationSource(paths.sourcePath, files);
|
|
3169
|
+
};
|
|
3170
|
+
await verify();
|
|
3171
|
+
const plan = await inventoryReceiverMigration(options.source, PATH15.join(scratch, "source"), files, options.reviews ?? []);
|
|
3172
|
+
if (!options.dryRun) await writeReport(PATH15.join(paths.backupPath, "migration-plan.json"), { ...migrationReport(paths, files, plan, options), executed: false });
|
|
3173
|
+
if (plan.unknown.length === 0 && !options.dryRun) await publishMigratedStore(paths.targetPath, plan.entries, verify);
|
|
3174
|
+
await verify();
|
|
3175
|
+
const report = migrationReport(paths, files, plan, options);
|
|
3176
|
+
if (!options.dryRun) await writeReport(PATH15.join(paths.backupPath, "migration-report.json"), report);
|
|
3177
|
+
return report;
|
|
3178
|
+
} finally {
|
|
3179
|
+
await rm(scratch, { recursive: true, force: true });
|
|
2179
3180
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
3181
|
+
}
|
|
3182
|
+
function migrateIdOnlyReducerCheckpoint(checkpoint, mappings) {
|
|
3183
|
+
if (!Number.isSafeInteger(checkpoint.lastProcessedPosition) || checkpoint.lastProcessedPosition < -1) throw new TypeError("Invalid historical checkpoint position");
|
|
3184
|
+
const ids = new Set(checkpoint.appliedWakeIds.map((id) => WakeIdZod3.parse(id)));
|
|
3185
|
+
const records = /* @__PURE__ */ new Map();
|
|
3186
|
+
for (const mapping of mappings) {
|
|
3187
|
+
const record = {
|
|
3188
|
+
audience: WakeJwtAudienceZod3.parse(mapping.audience),
|
|
3189
|
+
eventHash: HashZod8.parse(mapping.eventHash),
|
|
3190
|
+
publisher: NormalizedAddressZod6.parse(mapping.publisher),
|
|
3191
|
+
wakeId: WakeIdZod3.parse(mapping.wakeId)
|
|
3192
|
+
};
|
|
3193
|
+
if (!ids.has(record.wakeId) || records.has(record.wakeId)) throw new TypeError("Checkpoint mapping is extraneous or ambiguous");
|
|
3194
|
+
records.set(record.wakeId, record);
|
|
3195
|
+
}
|
|
3196
|
+
if (records.size !== ids.size) throw new TypeError("Every historical wake ID requires an exact scoped hash mapping");
|
|
3197
|
+
return { appliedWakes: [...ids].map((id) => records.get(id)), lastProcessedPosition: checkpoint.lastProcessedPosition };
|
|
2186
3198
|
}
|
|
2187
3199
|
|
|
2188
3200
|
// src/runtime/boundedFetch.ts
|
|
2189
|
-
import { Buffer as
|
|
3201
|
+
import { Buffer as Buffer3 } from "node:buffer";
|
|
2190
3202
|
|
|
2191
3203
|
// src/runtime/abort.ts
|
|
2192
3204
|
import { clearTimeout, setTimeout } from "node:timers";
|
|
@@ -2228,13 +3240,13 @@ function createBoundedPublisherFetch(options = {}) {
|
|
|
2228
3240
|
...init?.signal == null ? [] : [init.signal],
|
|
2229
3241
|
...input instanceof Request ? [input.signal] : []
|
|
2230
3242
|
];
|
|
2231
|
-
const
|
|
3243
|
+
const scope2 = publisherAbortScope(signals, timeout);
|
|
2232
3244
|
try {
|
|
2233
|
-
|
|
3245
|
+
scope2.signal.throwIfAborted();
|
|
2234
3246
|
const response = await nativeFetch(input, {
|
|
2235
3247
|
...init,
|
|
2236
3248
|
redirect: "error",
|
|
2237
|
-
signal:
|
|
3249
|
+
signal: scope2.signal
|
|
2238
3250
|
});
|
|
2239
3251
|
if (response.body === null)
|
|
2240
3252
|
return response;
|
|
@@ -2262,13 +3274,13 @@ function createBoundedPublisherFetch(options = {}) {
|
|
|
2262
3274
|
headers.delete("content-encoding");
|
|
2263
3275
|
headers.delete("transfer-encoding");
|
|
2264
3276
|
headers.set("content-length", String(size));
|
|
2265
|
-
return new Response(
|
|
3277
|
+
return new Response(Buffer3.concat(chunks), {
|
|
2266
3278
|
status: response.status,
|
|
2267
3279
|
statusText: response.statusText,
|
|
2268
3280
|
headers
|
|
2269
3281
|
});
|
|
2270
3282
|
} finally {
|
|
2271
|
-
|
|
3283
|
+
scope2.dispose();
|
|
2272
3284
|
}
|
|
2273
3285
|
})();
|
|
2274
3286
|
active.add(operation);
|
|
@@ -2293,7 +3305,7 @@ function createBoundedPublisherEthersTransport(options = {}) {
|
|
|
2293
3305
|
cancellation?.addListener(() => controller.abort());
|
|
2294
3306
|
cancellation?.checkSignal();
|
|
2295
3307
|
const response = await bounded.fetchFn(request2.url, {
|
|
2296
|
-
body: request2.body === null ? void 0 :
|
|
3308
|
+
body: request2.body === null ? void 0 : Buffer3.from(request2.body),
|
|
2297
3309
|
headers: request2.headers,
|
|
2298
3310
|
method: request2.method,
|
|
2299
3311
|
signal: controller.signal
|
|
@@ -2317,6 +3329,7 @@ function createBoundedPublisherEthersTransport(options = {}) {
|
|
|
2317
3329
|
}
|
|
2318
3330
|
|
|
2319
3331
|
// src/runtime/handshakeDiscovery.ts
|
|
3332
|
+
import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
|
|
2320
3333
|
import { decryptWakeSubscription, sourceWakeFilterHash } from "@xyo-network/event-kit-protocol";
|
|
2321
3334
|
import { EndpointChallengeResponseZod as EndpointChallengeResponseZod2 } from "@xyo-network/event-kit-schemas";
|
|
2322
3335
|
function createPublisherHandshakeDiscovery(options) {
|
|
@@ -2332,6 +3345,9 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2332
3345
|
rejectedSubscriptions: 0,
|
|
2333
3346
|
challengeFailures: 0
|
|
2334
3347
|
};
|
|
3348
|
+
function selectedTuple(tuple) {
|
|
3349
|
+
return (options.serviceHash === void 0 || tuple.service.objectHash === options.serviceHash) && (options.deliveryMode === void 0 || tuple.service.body.deliveryMode === options.deliveryMode) && (options.deliveryMode === void 0 || tuple.subscription.body.deliveryMode === options.deliveryMode) && (options.deliveryMode === void 0 || isDeepStrictEqual2(tuple.service.body.bestEffort, options.bestEffort));
|
|
3350
|
+
}
|
|
2335
3351
|
async function activeView(signal) {
|
|
2336
3352
|
signal.throwIfAborted();
|
|
2337
3353
|
viewing ??= (async () => {
|
|
@@ -2349,7 +3365,7 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2349
3365
|
...status,
|
|
2350
3366
|
finalizedHead: view.finalizedHead,
|
|
2351
3367
|
indexedThrough: view.indexedThrough,
|
|
2352
|
-
pendingSubscriptions: view.subscriptions.filter((item) => item.state === "pending").length,
|
|
3368
|
+
pendingSubscriptions: view.subscriptions.filter((item) => item.state === "pending" && item.tuples.some(selectedTuple)).length,
|
|
2353
3369
|
rejectedSubscriptions: view.rejections.length
|
|
2354
3370
|
};
|
|
2355
3371
|
return view;
|
|
@@ -2414,7 +3430,7 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2414
3430
|
if (!found.ok)
|
|
2415
3431
|
throw new Error("Publisher acceptance candidates are unavailable");
|
|
2416
3432
|
const offset = options.state.get("challenge-rate", "offset") ?? 0;
|
|
2417
|
-
const candidates = found.candidates;
|
|
3433
|
+
const candidates = found.candidates.filter((candidate) => selectedTuple(candidate.tuple));
|
|
2418
3434
|
let attempted = 0;
|
|
2419
3435
|
for (let index = 0; index < Math.min(candidates.length, 128) && attempted < 4; index += 1) {
|
|
2420
3436
|
const candidate = candidates[(offset + index) % candidates.length];
|
|
@@ -2430,30 +3446,40 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2430
3446
|
await acceptCandidate(candidate.subscriber, candidate.tuple.subscription.objectHash, signal);
|
|
2431
3447
|
}
|
|
2432
3448
|
}
|
|
3449
|
+
async function loadScopedKeys(view, active) {
|
|
3450
|
+
const scopedKeys = active.flatMap((item) => item.tuples.filter((tuple) => tuple.subscription.objectHash === item.selectedSubscription).map((tuple) => ({
|
|
3451
|
+
encryptionKey: tuple.subscription.body.encryptionKey,
|
|
3452
|
+
keyId: tuple.subscription.body.keyId,
|
|
3453
|
+
publisher: options.publisher
|
|
3454
|
+
})));
|
|
3455
|
+
if (view.hasCurrentAdvertisement && (options.serviceHash === void 0 || view.service.objectHash === options.serviceHash) && (options.deliveryMode === void 0 || view.service.body.deliveryMode === options.deliveryMode)) {
|
|
3456
|
+
scopedKeys.push({
|
|
3457
|
+
encryptionKey: view.key.objectHash,
|
|
3458
|
+
keyId: view.key.body.keyId,
|
|
3459
|
+
publisher: options.publisher
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
const loadedKeys = await Promise.all(scopedKeys.map(async (key) => await options.keys.getPrivateKey(key)));
|
|
3463
|
+
status = {
|
|
3464
|
+
...status,
|
|
3465
|
+
keyLoaded: scopedKeys.length > 0 && loadedKeys.every((key) => key !== void 0),
|
|
3466
|
+
activeKeyIds: [...new Set(scopedKeys.map((key) => key.keyId))]
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
2433
3469
|
return {
|
|
2434
3470
|
async discover(signal) {
|
|
2435
3471
|
const view = await activeView(signal);
|
|
2436
|
-
const
|
|
2437
|
-
|
|
2438
|
-
encryptionKey: key.objectHash,
|
|
2439
|
-
keyId: key.body.keyId,
|
|
2440
|
-
publisher: options.publisher
|
|
2441
|
-
});
|
|
2442
|
-
status = {
|
|
2443
|
-
...status,
|
|
2444
|
-
keyLoaded: loaded !== void 0,
|
|
2445
|
-
activeKeyIds: key === void 0 ? [] : [key.body.keyId]
|
|
2446
|
-
};
|
|
2447
|
-
const subscriptions = [];
|
|
2448
|
-
const active = view.subscriptions.filter((item) => item.state === "active");
|
|
3472
|
+
const active = view.subscriptions.filter((item) => item.state === "active" && item.tuples.some((tuple) => tuple.subscription.objectHash === item.selectedSubscription && selectedTuple(tuple)));
|
|
3473
|
+
await loadScopedKeys(view, active);
|
|
2449
3474
|
if (active.length > (options.maxSubscriptions ?? 64))
|
|
2450
3475
|
throw new Error("Publisher active subscription limit exceeded");
|
|
3476
|
+
const subscriptions = [];
|
|
2451
3477
|
for (const lifecycle of active) {
|
|
2452
3478
|
if (lifecycle.state !== "active" || lifecycle.activeFromBlock === void 0)
|
|
2453
3479
|
continue;
|
|
2454
3480
|
try {
|
|
2455
3481
|
const tuple = lifecycle.tuples.find((item) => item.subscription.objectHash === lifecycle.selectedSubscription);
|
|
2456
|
-
if (tuple === void 0)
|
|
3482
|
+
if (tuple === void 0 || !selectedTuple(tuple))
|
|
2457
3483
|
throw new Error("Active publisher subscription has no selected tuple");
|
|
2458
3484
|
const encrypted = tuple.subscription.body;
|
|
2459
3485
|
if (encrypted.chainId !== options.authorizationChainId)
|
|
@@ -2473,6 +3499,9 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2473
3499
|
options.transport.assertAllowed(plaintext.endpoint);
|
|
2474
3500
|
subscriptions.push({
|
|
2475
3501
|
subscriptionHash: tuple.subscription.objectHash,
|
|
3502
|
+
serviceHash: tuple.service.objectHash,
|
|
3503
|
+
deliveryMode: plaintext.filter.deliveryMode,
|
|
3504
|
+
...plaintext.filter.bestEffort === void 0 ? {} : { bestEffort: plaintext.filter.bestEffort },
|
|
2476
3505
|
authorizationChainId: options.authorizationChainId,
|
|
2477
3506
|
controller: tuple.subscriber,
|
|
2478
3507
|
audience: plaintext.audience,
|
|
@@ -2492,16 +3521,16 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2492
3521
|
},
|
|
2493
3522
|
async isActive(subscription, signal) {
|
|
2494
3523
|
const view = await activeView(signal);
|
|
2495
|
-
return view.subscriptions.some((item) => item.state === "active" && item.selectedSubscription === subscription.subscriptionHash && item.tuples.some((tuple) => tuple.subscription.objectHash === subscription.subscriptionHash && tuple.subscriber === subscription.controller));
|
|
3524
|
+
return view.subscriptions.some((item) => item.state === "active" && item.selectedSubscription === subscription.subscriptionHash && item.tuples.some((tuple) => tuple.subscription.objectHash === subscription.subscriptionHash && tuple.subscriber === subscription.controller && selectedTuple(tuple)));
|
|
2496
3525
|
},
|
|
2497
3526
|
async acceptPending(signal) {
|
|
2498
3527
|
if (accepting === void 0) {
|
|
2499
|
-
const
|
|
3528
|
+
const scope2 = publisherAbortScope([owner.signal, signal]);
|
|
2500
3529
|
accepting = (async () => {
|
|
2501
3530
|
try {
|
|
2502
|
-
await accept(
|
|
3531
|
+
await accept(scope2.signal);
|
|
2503
3532
|
} finally {
|
|
2504
|
-
|
|
3533
|
+
scope2.dispose();
|
|
2505
3534
|
accepting = void 0;
|
|
2506
3535
|
}
|
|
2507
3536
|
})();
|
|
@@ -2516,25 +3545,25 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2516
3545
|
}
|
|
2517
3546
|
|
|
2518
3547
|
// src/runtime/httpsTransport.ts
|
|
2519
|
-
import { Buffer as
|
|
3548
|
+
import { Buffer as Buffer4 } from "node:buffer";
|
|
2520
3549
|
import { lookup } from "node:dns/promises";
|
|
2521
3550
|
import { request } from "node:https";
|
|
2522
3551
|
import { BlockList, isIP } from "node:net";
|
|
2523
|
-
import { z as
|
|
3552
|
+
import { z as z4 } from "zod";
|
|
2524
3553
|
|
|
2525
3554
|
// src/runtime/sources.ts
|
|
2526
|
-
import { createHash as
|
|
2527
|
-
import { canonicalJson as
|
|
2528
|
-
import { completedUtcIntervalPosition, WakeSourceZod } from "@xyo-network/event-kit-schemas";
|
|
3555
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3556
|
+
import { canonicalJson as canonicalJson3 } from "@xyo-network/event-kit-protocol";
|
|
3557
|
+
import { completedUtcIntervalPosition, WakeSourceZod as WakeSourceZod2 } from "@xyo-network/event-kit-schemas";
|
|
2529
3558
|
import { PayloadBuilder } from "@xyo-network/sdk";
|
|
2530
3559
|
import { asXL1BlockNumber } from "@xyo-network/xl1-sdk";
|
|
2531
3560
|
import { FetchRequest, JsonRpcProvider } from "ethers";
|
|
2532
3561
|
function publisherSourceKey(source) {
|
|
2533
|
-
return
|
|
3562
|
+
return createHash4("sha256").update(canonicalJson3(WakeSourceZod2.parse(source))).digest("hex");
|
|
2534
3563
|
}
|
|
2535
3564
|
async function boundedPublisherOperation(operation, signal, timeoutMs = 1e4) {
|
|
2536
|
-
const
|
|
2537
|
-
const combined =
|
|
3565
|
+
const scope2 = publisherAbortScope([signal], timeoutMs);
|
|
3566
|
+
const combined = scope2.signal;
|
|
2538
3567
|
let rejectAbort;
|
|
2539
3568
|
const aborted = new Promise((_resolve, reject) => {
|
|
2540
3569
|
rejectAbort = () => reject(new Error("Publisher operation aborted or timed out"));
|
|
@@ -2546,11 +3575,11 @@ async function boundedPublisherOperation(operation, signal, timeoutMs = 1e4) {
|
|
|
2546
3575
|
} finally {
|
|
2547
3576
|
if (rejectAbort !== void 0)
|
|
2548
3577
|
combined.removeEventListener("abort", rejectAbort);
|
|
2549
|
-
|
|
3578
|
+
scope2.dispose();
|
|
2550
3579
|
}
|
|
2551
3580
|
}
|
|
2552
3581
|
function createXl1PublisherSource(options) {
|
|
2553
|
-
const source =
|
|
3582
|
+
const source = WakeSourceZod2.parse(options.source);
|
|
2554
3583
|
if (source.kind !== "xl1-finalized")
|
|
2555
3584
|
throw new Error("Expected an XL1 finalized source");
|
|
2556
3585
|
return {
|
|
@@ -2574,7 +3603,7 @@ function createXl1PublisherSource(options) {
|
|
|
2574
3603
|
};
|
|
2575
3604
|
}
|
|
2576
3605
|
function createEthereumFinalizedPublisherSource(options) {
|
|
2577
|
-
const source =
|
|
3606
|
+
const source = WakeSourceZod2.parse(options.source);
|
|
2578
3607
|
if (source.kind !== "ethereum-finalized")
|
|
2579
3608
|
throw new Error("Expected an Ethereum finalized source");
|
|
2580
3609
|
const { chainId, genesisHash } = source;
|
|
@@ -2640,7 +3669,7 @@ function createEthereumFinalizedPublisherSource(options) {
|
|
|
2640
3669
|
};
|
|
2641
3670
|
}
|
|
2642
3671
|
function createUtcIntervalPublisherSource(options) {
|
|
2643
|
-
const source =
|
|
3672
|
+
const source = WakeSourceZod2.parse(options.source);
|
|
2644
3673
|
if (source.kind !== "utc-interval")
|
|
2645
3674
|
throw new Error("Expected a UTC interval source");
|
|
2646
3675
|
const nowSeconds = options.nowSeconds ?? (() => Math.floor(Date.now() / 1e3));
|
|
@@ -2657,14 +3686,14 @@ function createUtcIntervalPublisherSource(options) {
|
|
|
2657
3686
|
}
|
|
2658
3687
|
|
|
2659
3688
|
// src/runtime/httpsTransport.ts
|
|
2660
|
-
var OptionsZod =
|
|
2661
|
-
allowedEndpoints:
|
|
2662
|
-
endpointHeaders:
|
|
2663
|
-
endpoint:
|
|
2664
|
-
headers:
|
|
3689
|
+
var OptionsZod = z4.strictObject({
|
|
3690
|
+
allowedEndpoints: z4.array(z4.url()).min(1).max(64),
|
|
3691
|
+
endpointHeaders: z4.array(z4.strictObject({
|
|
3692
|
+
endpoint: z4.url(),
|
|
3693
|
+
headers: z4.strictObject({ "x-vercel-protection-bypass": z4.string().min(1).max(4096).regex(/^[\u{21}-\u{7E}]+$/u) })
|
|
2665
3694
|
})).max(64).default([]),
|
|
2666
|
-
maxResponseBytes:
|
|
2667
|
-
requestTimeoutMs:
|
|
3695
|
+
maxResponseBytes: z4.number().int().min(1).max(1048576).default(65536),
|
|
3696
|
+
requestTimeoutMs: z4.number().int().min(100).max(6e4).default(1e4)
|
|
2668
3697
|
});
|
|
2669
3698
|
var NON_PUBLIC_IPV4_SUBNETS = [
|
|
2670
3699
|
["0.0.0.0", 8],
|
|
@@ -2744,8 +3773,8 @@ function createPublisherHttpsTransport(input) {
|
|
|
2744
3773
|
assertAllowed(endpoint);
|
|
2745
3774
|
signal.throwIfAborted();
|
|
2746
3775
|
const url = new URL(endpoint);
|
|
2747
|
-
const
|
|
2748
|
-
const combined =
|
|
3776
|
+
const scope2 = publisherAbortScope([signal], options.requestTimeoutMs);
|
|
3777
|
+
const combined = scope2.signal;
|
|
2749
3778
|
try {
|
|
2750
3779
|
const addresses = await boundedPublisherOperation(lookup(url.hostname.replaceAll(/^\[|\]$/gu, ""), { all: true, verbatim: true }), combined, options.requestTimeoutMs);
|
|
2751
3780
|
combined.throwIfAborted();
|
|
@@ -2754,7 +3783,7 @@ function createPublisherHttpsTransport(input) {
|
|
|
2754
3783
|
const selected = addresses[0];
|
|
2755
3784
|
if (selected === void 0)
|
|
2756
3785
|
throw new Error("Publisher DNS lookup was empty");
|
|
2757
|
-
const encoded =
|
|
3786
|
+
const encoded = Buffer4.from(JSON.stringify(body));
|
|
2758
3787
|
if (encoded.byteLength > 65536)
|
|
2759
3788
|
throw new Error("Publisher request body is too large");
|
|
2760
3789
|
return await new Promise((resolve, reject) => {
|
|
@@ -2795,7 +3824,7 @@ function createPublisherHttpsTransport(input) {
|
|
|
2795
3824
|
response.on("error", () => reject(new Error("Publisher HTTPS response failed")));
|
|
2796
3825
|
response.on("end", () => {
|
|
2797
3826
|
try {
|
|
2798
|
-
const responseBody = size === 0 ? null : JSON.parse(
|
|
3827
|
+
const responseBody = size === 0 ? null : JSON.parse(Buffer4.concat(chunks).toString("utf8"));
|
|
2799
3828
|
const retry = Number(response.headers["retry-after"]);
|
|
2800
3829
|
resolve({
|
|
2801
3830
|
status,
|
|
@@ -2811,42 +3840,152 @@ function createPublisherHttpsTransport(input) {
|
|
|
2811
3840
|
call.end(encoded);
|
|
2812
3841
|
});
|
|
2813
3842
|
} finally {
|
|
2814
|
-
|
|
3843
|
+
scope2.dispose();
|
|
2815
3844
|
}
|
|
2816
3845
|
}
|
|
2817
3846
|
};
|
|
2818
3847
|
}
|
|
2819
3848
|
|
|
2820
3849
|
// src/runtime/publisherRuntime.ts
|
|
2821
|
-
import {
|
|
2822
|
-
import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
|
|
3850
|
+
import { isDeepStrictEqual as isDeepStrictEqual4 } from "node:util";
|
|
2823
3851
|
import { createMemoryLocator, PeriodicActor } from "@ariestools/actor";
|
|
2824
3852
|
import { creatable } from "@ariestools/sdk";
|
|
2825
|
-
import {
|
|
2826
|
-
import {
|
|
3853
|
+
import { BestEffortAdmissionAcknowledgmentZod } from "@xyo-network/event-kit-protocol";
|
|
3854
|
+
import { BestEffortDeliveryPolicyZod, WakeDeliveryModeZod } from "@xyo-network/event-kit-schemas";
|
|
3855
|
+
import { z as z5 } from "zod";
|
|
3856
|
+
|
|
3857
|
+
// src/runtime/publisherDeliveryState.ts
|
|
3858
|
+
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
3859
|
+
import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
|
|
3860
|
+
import { WakeEnvelopeSchema as WakeEnvelopeSchema2, WakeEnvelopeZod as WakeEnvelopeZod2 } from "@xyo-network/event-kit-schemas";
|
|
3861
|
+
function assertDeliveryState(record, subscription) {
|
|
3862
|
+
if (!isDeepStrictEqual3(record.subscription, subscription))
|
|
3863
|
+
throw new Error("Subscription revision changed without a new identity");
|
|
3864
|
+
if (!Number.isSafeInteger(record.cursor) || record.cursor < subscription.fromPosition - 1 || [record.delivered, record.failures, record.nextAttemptMs].some((value) => !(Number.isSafeInteger(value) && value >= 0)) || [record.attempts, record.superseded, record.abandoned, record.totalAttempts, record.lastHintAt, record.lastNotifiedAt, record.lastHintTarget].some((value) => value !== void 0 && !(Number.isSafeInteger(value) && value >= 0)) || !["active", "blocked", "cancelled"].includes(record.state))
|
|
3865
|
+
throw new Error("Persisted publisher delivery state is invalid");
|
|
3866
|
+
if (record.pending === void 0) return;
|
|
3867
|
+
const pending = WakeEnvelopeZod2.parse(record.pending);
|
|
3868
|
+
if (subscription.deliveryMode === "durable" && pending.fromPosition !== record.cursor + 1 || pending.deliveryMode !== subscription.deliveryMode || pending.toPosition - pending.fromPosition + 1 > subscription.maxPositionsPerWake || pending.authorizationChainId !== subscription.authorizationChainId || pending.deploymentId !== subscription.deploymentId || pending.queueId !== subscription.queueId || pending.filterHash !== subscription.filterHash || !isDeepStrictEqual3(pending.source, subscription.source))
|
|
3869
|
+
throw new Error("Persisted publisher outbox binding is invalid");
|
|
3870
|
+
}
|
|
3871
|
+
function makeEvent(subscription, fromPosition, toPosition, now, wakeId = randomBytes6(16).toString("hex")) {
|
|
3872
|
+
return WakeEnvelopeZod2.parse({
|
|
3873
|
+
schema: WakeEnvelopeSchema2,
|
|
3874
|
+
authorizationChainId: subscription.authorizationChainId,
|
|
3875
|
+
deliveryMode: subscription.deliveryMode,
|
|
3876
|
+
deploymentId: subscription.deploymentId,
|
|
3877
|
+
filterHash: subscription.filterHash,
|
|
3878
|
+
fromPosition,
|
|
3879
|
+
issuedAt: now,
|
|
3880
|
+
source: subscription.source,
|
|
3881
|
+
toPosition,
|
|
3882
|
+
wakeId,
|
|
3883
|
+
...subscription.queueId === void 0 ? {} : { queueId: subscription.queueId }
|
|
3884
|
+
});
|
|
3885
|
+
}
|
|
3886
|
+
var DeliveryResponseError = class extends Error {
|
|
3887
|
+
retryAfterMs;
|
|
3888
|
+
status;
|
|
3889
|
+
constructor(status, retryAfterMs) {
|
|
3890
|
+
super("Publisher admission failed");
|
|
3891
|
+
this.status = status;
|
|
3892
|
+
this.retryAfterMs = retryAfterMs;
|
|
3893
|
+
}
|
|
3894
|
+
get retryable() {
|
|
3895
|
+
return this.status === 408 || this.status === 429 || this.status >= 500;
|
|
3896
|
+
}
|
|
3897
|
+
};
|
|
3898
|
+
function prepareBestEffortDelivery(options) {
|
|
3899
|
+
const {
|
|
3900
|
+
bestEffort,
|
|
3901
|
+
first,
|
|
3902
|
+
heads,
|
|
3903
|
+
now,
|
|
3904
|
+
record,
|
|
3905
|
+
state
|
|
3906
|
+
} = options;
|
|
3907
|
+
const subscription = record.subscription;
|
|
3908
|
+
const head = heads.get(publisherSourceKey(subscription.source));
|
|
3909
|
+
const prior = record.pending;
|
|
3910
|
+
const expired = isHintExpired(record, now, bestEffort);
|
|
3911
|
+
if (head === void 0 || head instanceof Error || head.position < subscription.fromPosition) {
|
|
3912
|
+
if (prior !== void 0 && !expired) return { ...record, pending: prior };
|
|
3913
|
+
if (prior !== void 0) state.set("best-effort-deliveries", subscription.subscriptionHash, {
|
|
3914
|
+
...record,
|
|
3915
|
+
pending: void 0,
|
|
3916
|
+
abandoned: (record.abandoned ?? 0) + 1
|
|
3917
|
+
});
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
const refresh = refreshDue(record, now, bestEffort);
|
|
3921
|
+
if (freshHintRequired(record, head.position, first, refresh, expired)) {
|
|
3922
|
+
const pending = makeEvent(subscription, head.position, head.position, now);
|
|
3923
|
+
const prepared = {
|
|
3924
|
+
...record,
|
|
3925
|
+
pending,
|
|
3926
|
+
attempts: 0,
|
|
3927
|
+
lastHintAt: now,
|
|
3928
|
+
lastHintTarget: head.position,
|
|
3929
|
+
superseded: (record.superseded ?? 0) + (prior !== void 0 && !expired ? 1 : 0),
|
|
3930
|
+
abandoned: (record.abandoned ?? 0) + (expired ? 1 : 0),
|
|
3931
|
+
nextAttemptMs: 0,
|
|
3932
|
+
state: "active"
|
|
3933
|
+
};
|
|
3934
|
+
state.set("best-effort-deliveries", subscription.subscriptionHash, prepared);
|
|
3935
|
+
return prepared;
|
|
3936
|
+
}
|
|
3937
|
+
if (expired) state.set("best-effort-deliveries", subscription.subscriptionHash, {
|
|
3938
|
+
...record,
|
|
3939
|
+
pending: void 0,
|
|
3940
|
+
abandoned: (record.abandoned ?? 0) + 1
|
|
3941
|
+
});
|
|
3942
|
+
return prior === void 0 || expired ? void 0 : { ...record, pending: prior };
|
|
3943
|
+
}
|
|
3944
|
+
function isHintExpired(record, now, policy) {
|
|
3945
|
+
return record.pending !== void 0 && (now > record.pending.issuedAt + policy.maxHintAgeSeconds || (record.attempts ?? 0) >= policy.maxAttempts);
|
|
3946
|
+
}
|
|
3947
|
+
function refreshDue(record, now, policy) {
|
|
3948
|
+
return now - (record.lastHintAt ?? record.lastNotifiedAt ?? 0) >= policy.refreshIntervalSeconds;
|
|
3949
|
+
}
|
|
3950
|
+
function freshHintRequired(record, head, first, refresh, expired) {
|
|
3951
|
+
return first || head > (record.pending?.toPosition ?? record.lastHintTarget ?? record.cursor) || refresh && (record.pending === void 0 || expired);
|
|
3952
|
+
}
|
|
3953
|
+
function failedDelivery(record, current, mode, error, now) {
|
|
3954
|
+
const bestEffort = mode === "best-effort";
|
|
3955
|
+
const wait = error instanceof DeliveryResponseError && error.retryAfterMs !== void 0 ? error.retryAfterMs : Math.min(3e5, 1e3 * 2 ** Math.min(record.failures, 8));
|
|
3956
|
+
return {
|
|
3957
|
+
...record,
|
|
3958
|
+
failures: record.failures + 1,
|
|
3959
|
+
totalAttempts: bestEffort ? current?.totalAttempts ?? record.totalAttempts ?? 0 : record.totalAttempts,
|
|
3960
|
+
attempts: bestEffort ? current?.attempts ?? record.attempts ?? 0 : record.attempts,
|
|
3961
|
+
state: !bestEffort && error instanceof DeliveryResponseError && !error.retryable ? "blocked" : "active",
|
|
3962
|
+
nextAttemptMs: now * 1e3 + Math.min(36e5, wait),
|
|
3963
|
+
lastError: "delivery-failed"
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
2827
3966
|
|
|
2828
3967
|
// src/runtime/sourceSigner.ts
|
|
2829
|
-
import { randomBytes as
|
|
3968
|
+
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
2830
3969
|
import {
|
|
2831
|
-
NormalizedAddressZod as
|
|
2832
|
-
WakeEnvelopeZod,
|
|
3970
|
+
NormalizedAddressZod as NormalizedAddressZod7,
|
|
3971
|
+
WakeEnvelopeZod as WakeEnvelopeZod3,
|
|
2833
3972
|
WakeJwtSchema
|
|
2834
3973
|
} from "@xyo-network/event-kit-schemas";
|
|
2835
3974
|
import { PayloadBuilder as PayloadBuilder2 } from "@xyo-network/sdk";
|
|
2836
3975
|
function createAccountSourceWakeSigner(options) {
|
|
2837
|
-
const publisher =
|
|
3976
|
+
const publisher = NormalizedAddressZod7.parse(options.publisher);
|
|
2838
3977
|
if (String(options.account.address).replace(/^0x/u, "").toLowerCase() !== publisher)
|
|
2839
3978
|
throw new Error("Publisher signing account mismatch");
|
|
2840
3979
|
return {
|
|
2841
3980
|
async sign(input, audience) {
|
|
2842
|
-
const event =
|
|
3981
|
+
const event = WakeEnvelopeZod3.parse(input);
|
|
2843
3982
|
const eventHash = await PayloadBuilder2.dataHash(event);
|
|
2844
3983
|
const now = options.nowSeconds?.() ?? Math.floor(Date.now() / 1e3);
|
|
2845
3984
|
if (!Number.isSafeInteger(now) || now < 0)
|
|
2846
3985
|
throw new Error("Publisher signing clock is invalid");
|
|
2847
3986
|
const { token: jwt } = await options.account.signJwt({
|
|
2848
3987
|
audience,
|
|
2849
|
-
claims: { eventHash, jti:
|
|
3988
|
+
claims: { eventHash, jti: randomBytes7(16).toString("hex") },
|
|
2850
3989
|
now,
|
|
2851
3990
|
schema: WakeJwtSchema,
|
|
2852
3991
|
ttl: 60
|
|
@@ -2865,30 +4004,6 @@ function createAccountSourceWakeSigner(options) {
|
|
|
2865
4004
|
function activationReady(status) {
|
|
2866
4005
|
return (status?.keyLoaded ?? true) && (status?.pendingSubscriptions ?? 0) === 0;
|
|
2867
4006
|
}
|
|
2868
|
-
function assertDeliveryState(record, subscription) {
|
|
2869
|
-
if (!isDeepStrictEqual2(record.subscription, subscription))
|
|
2870
|
-
throw new Error("Subscription revision changed without a new identity");
|
|
2871
|
-
if (!Number.isSafeInteger(record.cursor) || record.cursor < subscription.fromPosition - 1 || [record.delivered, record.failures, record.nextAttemptMs].some((value) => !(Number.isSafeInteger(value) && value >= 0)) || !["active", "blocked", "cancelled"].includes(record.state))
|
|
2872
|
-
throw new Error("Persisted publisher delivery state is invalid");
|
|
2873
|
-
if (record.pending === void 0) return;
|
|
2874
|
-
const pending = WakeEnvelopeZod2.parse(record.pending);
|
|
2875
|
-
if (pending.fromPosition !== record.cursor + 1 || pending.toPosition - pending.fromPosition + 1 > subscription.maxPositionsPerWake || pending.authorizationChainId !== subscription.authorizationChainId || pending.deploymentId !== subscription.deploymentId || pending.queueId !== subscription.queueId || pending.filterHash !== subscription.filterHash || !isDeepStrictEqual2(pending.source, subscription.source))
|
|
2876
|
-
throw new Error("Persisted publisher outbox binding is invalid");
|
|
2877
|
-
}
|
|
2878
|
-
function makeEvent(subscription, fromPosition, toPosition, now, wakeId = randomBytes6(16).toString("hex")) {
|
|
2879
|
-
return WakeEnvelopeZod2.parse({
|
|
2880
|
-
schema: WakeEnvelopeSchema,
|
|
2881
|
-
authorizationChainId: subscription.authorizationChainId,
|
|
2882
|
-
deploymentId: subscription.deploymentId,
|
|
2883
|
-
filterHash: subscription.filterHash,
|
|
2884
|
-
fromPosition,
|
|
2885
|
-
issuedAt: now,
|
|
2886
|
-
source: subscription.source,
|
|
2887
|
-
toPosition,
|
|
2888
|
-
wakeId,
|
|
2889
|
-
...subscription.queueId === void 0 ? {} : { queueId: subscription.queueId }
|
|
2890
|
-
});
|
|
2891
|
-
}
|
|
2892
4007
|
var PublisherRuntimeActor = class extends PeriodicActor {
|
|
2893
4008
|
async runPass(context) {
|
|
2894
4009
|
await this.params.tick(context.signal);
|
|
@@ -2897,26 +4012,30 @@ var PublisherRuntimeActor = class extends PeriodicActor {
|
|
|
2897
4012
|
PublisherRuntimeActor = __decorateClass([
|
|
2898
4013
|
creatable()
|
|
2899
4014
|
], PublisherRuntimeActor);
|
|
2900
|
-
var TimingZod =
|
|
2901
|
-
concurrency:
|
|
2902
|
-
intervalMs:
|
|
2903
|
-
maxSubscriptions:
|
|
2904
|
-
requestTimeoutMs:
|
|
4015
|
+
var TimingZod = z5.object({
|
|
4016
|
+
concurrency: z5.number().int().min(1).max(32).default(4),
|
|
4017
|
+
intervalMs: z5.number().int().min(100).max(6e4).default(1e3),
|
|
4018
|
+
maxSubscriptions: z5.number().int().min(1).max(1024).default(64),
|
|
4019
|
+
requestTimeoutMs: z5.number().int().min(100).max(6e4).default(1e4)
|
|
2905
4020
|
});
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
};
|
|
4021
|
+
function runtimeSettings(options) {
|
|
4022
|
+
const deliveryMode = WakeDeliveryModeZod.parse(options.deliveryMode);
|
|
4023
|
+
const bestEffort = options.bestEffort === void 0 ? void 0 : BestEffortDeliveryPolicyZod.parse(options.bestEffort);
|
|
4024
|
+
if (deliveryMode === "best-effort" !== (bestEffort !== void 0)) throw new TypeError("Explicit delivery mode requires its exact policy");
|
|
4025
|
+
return {
|
|
4026
|
+
bestEffort,
|
|
4027
|
+
deliveryMode,
|
|
4028
|
+
deliveryNamespace: deliveryMode === "best-effort" ? "best-effort-deliveries" : "deliveries",
|
|
4029
|
+
timing: TimingZod.parse(options)
|
|
4030
|
+
};
|
|
4031
|
+
}
|
|
2918
4032
|
async function createPersistentEventPublisherRuntime(options) {
|
|
2919
|
-
const
|
|
4033
|
+
const {
|
|
4034
|
+
bestEffort,
|
|
4035
|
+
deliveryMode,
|
|
4036
|
+
deliveryNamespace,
|
|
4037
|
+
timing
|
|
4038
|
+
} = runtimeSettings(options);
|
|
2920
4039
|
if (options.sources.length < 1 || options.sources.length > 16)
|
|
2921
4040
|
throw new Error("Publisher requires between one and sixteen bounded sources");
|
|
2922
4041
|
const clock = options.nowSeconds ?? (() => Math.floor(Date.now() / 1e3));
|
|
@@ -2928,7 +4047,12 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
2928
4047
|
const sourceReaders = new Map(options.sources.map((reader) => [publisherSourceKey(reader.source), reader]));
|
|
2929
4048
|
if (sourceReaders.size !== options.sources.length)
|
|
2930
4049
|
throw new Error("Duplicate publisher source identity");
|
|
4050
|
+
const state = options.state;
|
|
4051
|
+
const existingMode = state.get("metadata", "runtime-delivery-mode");
|
|
4052
|
+
if (existingMode !== void 0 && existingMode !== deliveryMode) throw new Error("Publisher state delivery mode mismatch; preserve custody and use an explicitly qualified state binding");
|
|
4053
|
+
state.set("metadata", "runtime-delivery-mode", deliveryMode);
|
|
2931
4054
|
const abort = new AbortController();
|
|
4055
|
+
const seenThisProcess = /* @__PURE__ */ new Set();
|
|
2932
4056
|
let running = false;
|
|
2933
4057
|
let ready = false;
|
|
2934
4058
|
let clockHealthy = true;
|
|
@@ -2936,11 +4060,16 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
2936
4060
|
let lastError;
|
|
2937
4061
|
let sourcesHealthy = false;
|
|
2938
4062
|
let stop;
|
|
2939
|
-
const state = options.state;
|
|
2940
4063
|
const timed = (promise, signal) => boundedPublisherOperation(promise, signal, timing.requestTimeoutMs);
|
|
2941
4064
|
function loadDelivery(subscription, now) {
|
|
4065
|
+
if (subscription.deliveryMode !== deliveryMode || !isDeepStrictEqual4(subscription.bestEffort, bestEffort))
|
|
4066
|
+
throw new Error("Subscription delivery mode or policy disagrees with runtime");
|
|
4067
|
+
if (deliveryMode === "best-effort" && (subscription.maxPositionsPerWake !== 1 || subscription.serviceHash === void 0)) throw new Error("Best Effort requires point range");
|
|
4068
|
+
if (options.serviceHash !== void 0 && subscription.serviceHash !== options.serviceHash)
|
|
4069
|
+
throw new Error("Subscription does not match the configured exact service descriptor");
|
|
2942
4070
|
const key = subscription.subscriptionHash;
|
|
2943
|
-
|
|
4071
|
+
if (deliveryMode === "best-effort" && state.get(deliveryNamespace, key) === void 0 && state.entries(deliveryNamespace).length >= timing.maxSubscriptions) throw new Error("Best Effort state capacity exceeded; live records are not evicted");
|
|
4072
|
+
let record = state.get(deliveryNamespace, key);
|
|
2944
4073
|
if (record === void 0) {
|
|
2945
4074
|
makeEvent(subscription, subscription.fromPosition, subscription.fromPosition, now, "0".repeat(32));
|
|
2946
4075
|
if (!Number.isSafeInteger(subscription.maxPositionsPerWake) || subscription.maxPositionsPerWake < 1 || subscription.maxPositionsPerWake > 4096) {
|
|
@@ -2954,12 +4083,26 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
2954
4083
|
nextAttemptMs: 0,
|
|
2955
4084
|
state: "active"
|
|
2956
4085
|
};
|
|
2957
|
-
state.set(
|
|
4086
|
+
state.set(deliveryNamespace, key, record);
|
|
2958
4087
|
}
|
|
2959
4088
|
assertDeliveryState(record, subscription);
|
|
2960
4089
|
return record;
|
|
2961
4090
|
}
|
|
2962
4091
|
function prepareDelivery(record, now, heads) {
|
|
4092
|
+
if (deliveryMode === "best-effort" && bestEffort !== void 0) {
|
|
4093
|
+
const first = !seenThisProcess.has(record.subscription.subscriptionHash);
|
|
4094
|
+
const head2 = heads.get(publisherSourceKey(record.subscription.source));
|
|
4095
|
+
if (head2 !== void 0 && !(head2 instanceof Error) && head2.position >= record.subscription.fromPosition)
|
|
4096
|
+
seenThisProcess.add(record.subscription.subscriptionHash);
|
|
4097
|
+
return prepareBestEffortDelivery({
|
|
4098
|
+
bestEffort,
|
|
4099
|
+
first,
|
|
4100
|
+
heads,
|
|
4101
|
+
now,
|
|
4102
|
+
record,
|
|
4103
|
+
state
|
|
4104
|
+
});
|
|
4105
|
+
}
|
|
2963
4106
|
if (record.pending !== void 0) return { ...record, pending: record.pending };
|
|
2964
4107
|
const subscription = record.subscription;
|
|
2965
4108
|
const head = heads.get(publisherSourceKey(subscription.source));
|
|
@@ -2971,43 +4114,74 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
2971
4114
|
state.transaction(() => {
|
|
2972
4115
|
const logicalKey = JSON.stringify([subscription.subscriptionHash, pending.fromPosition, pending.toPosition]);
|
|
2973
4116
|
if (!state.insert("delivery-events", logicalKey, pending)) throw new Error("Publisher logical range already exists outside its contiguous cursor");
|
|
2974
|
-
state.set(
|
|
4117
|
+
state.set(deliveryNamespace, subscription.subscriptionHash, prepared);
|
|
2975
4118
|
});
|
|
2976
4119
|
return prepared;
|
|
2977
4120
|
}
|
|
2978
4121
|
async function stillActive(record, signal) {
|
|
2979
4122
|
if (await timed(options.discovery.isActive(record.subscription, signal), signal)) return true;
|
|
2980
|
-
state.set(
|
|
4123
|
+
state.set(deliveryNamespace, record.subscription.subscriptionHash, {
|
|
2981
4124
|
...record,
|
|
2982
4125
|
pending: void 0,
|
|
2983
4126
|
state: "cancelled"
|
|
2984
4127
|
});
|
|
2985
4128
|
return false;
|
|
2986
4129
|
}
|
|
4130
|
+
function acknowledgeBestEffort(record, body, eventHash, signal, now) {
|
|
4131
|
+
const { pending, subscription } = record;
|
|
4132
|
+
const acknowledgment = BestEffortAdmissionAcknowledgmentZod.safeParse(body);
|
|
4133
|
+
if (!acknowledgment.success || acknowledgment.data.eventHash !== eventHash || acknowledgment.data.wakeId !== pending.wakeId || acknowledgment.data.expiresAt !== pending.issuedAt + (bestEffort?.maxHintAgeSeconds ?? 0) + 60 || acknowledgment.data.acceptedAt < pending.issuedAt - 60 || acknowledgment.data.acceptedAt > now + 60)
|
|
4134
|
+
throw new Error("Publisher did not receive a matching finite acknowledgment");
|
|
4135
|
+
signal.throwIfAborted();
|
|
4136
|
+
state.set(deliveryNamespace, subscription.subscriptionHash, {
|
|
4137
|
+
subscription,
|
|
4138
|
+
cursor: pending.toPosition,
|
|
4139
|
+
delivered: record.delivered + 1,
|
|
4140
|
+
failures: 0,
|
|
4141
|
+
attempts: 0,
|
|
4142
|
+
superseded: record.superseded ?? 0,
|
|
4143
|
+
abandoned: record.abandoned ?? 0,
|
|
4144
|
+
totalAttempts: (record.totalAttempts ?? 0) + 1,
|
|
4145
|
+
lastNotifiedAt: now,
|
|
4146
|
+
lastHintAt: pending.issuedAt,
|
|
4147
|
+
lastHintTarget: pending.toPosition,
|
|
4148
|
+
nextAttemptMs: 0,
|
|
4149
|
+
state: "active"
|
|
4150
|
+
});
|
|
4151
|
+
}
|
|
2987
4152
|
async function sendDelivery(record, signal, now) {
|
|
2988
4153
|
const { pending, subscription } = record;
|
|
2989
4154
|
signal.throwIfAborted();
|
|
2990
4155
|
const signed = await signer.sign(pending, subscription.audience);
|
|
2991
4156
|
signal.throwIfAborted();
|
|
2992
|
-
const attempt = state.get("delivery-attempts", pending.wakeId)?.count ?? 0;
|
|
2993
|
-
state.set(
|
|
4157
|
+
const attempt = deliveryMode === "best-effort" ? record.attempts ?? 0 : state.get("delivery-attempts", pending.wakeId)?.count ?? 0;
|
|
4158
|
+
if (deliveryMode === "best-effort") state.set(deliveryNamespace, subscription.subscriptionHash, {
|
|
4159
|
+
...record,
|
|
4160
|
+
attempts: attempt + 1,
|
|
4161
|
+
totalAttempts: (record.totalAttempts ?? 0) + 1
|
|
4162
|
+
});
|
|
4163
|
+
else state.set("delivery-attempts", pending.wakeId, {
|
|
2994
4164
|
count: attempt + 1,
|
|
2995
4165
|
startedAt: now,
|
|
2996
4166
|
status: "in-flight"
|
|
2997
4167
|
});
|
|
2998
4168
|
const response = await options.transport.post(subscription.endpoint, signed.event, signal, signed.jwt);
|
|
2999
|
-
state.set("delivery-attempts", pending.wakeId, {
|
|
4169
|
+
if (deliveryMode === "durable") state.set("delivery-attempts", pending.wakeId, {
|
|
3000
4170
|
count: attempt + 1,
|
|
3001
4171
|
startedAt: now,
|
|
3002
4172
|
status: response.status
|
|
3003
4173
|
});
|
|
3004
4174
|
if (response.status !== 200 && response.status !== 202) throw new DeliveryResponseError(response.status, response.retryAfterMs);
|
|
3005
|
-
|
|
4175
|
+
if (deliveryMode === "best-effort") {
|
|
4176
|
+
acknowledgeBestEffort(record, response.body, signed.eventHash, signal, now);
|
|
4177
|
+
return;
|
|
4178
|
+
}
|
|
4179
|
+
const receipt = z5.object({ receiptId: z5.string().min(1), wakeId: z5.literal(pending.wakeId) }).safeParse(response.body);
|
|
3006
4180
|
if (!receipt.success) throw new Error("Publisher admission did not return a matching durable receipt");
|
|
3007
4181
|
signal.throwIfAborted();
|
|
3008
4182
|
state.transaction(() => {
|
|
3009
4183
|
state.set("delivery-receipts", pending.wakeId, { receiptId: receipt.data.receiptId, receivedAt: now });
|
|
3010
|
-
state.set(
|
|
4184
|
+
state.set(deliveryNamespace, subscription.subscriptionHash, {
|
|
3011
4185
|
subscription,
|
|
3012
4186
|
cursor: pending.toPosition,
|
|
3013
4187
|
delivered: record.delivered + 1,
|
|
@@ -3019,7 +4193,7 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3019
4193
|
}
|
|
3020
4194
|
async function deliver(subscription, signal, now, heads) {
|
|
3021
4195
|
let record = loadDelivery(subscription, now);
|
|
3022
|
-
if (record.state !== "active" || record.nextAttemptMs > now * 1e3) return;
|
|
4196
|
+
if (deliveryMode === "durable" && (record.state !== "active" || record.nextAttemptMs > now * 1e3)) return;
|
|
3023
4197
|
try {
|
|
3024
4198
|
if (!await stillActive(record, signal)) return;
|
|
3025
4199
|
options.transport.assertAllowed(subscription.endpoint);
|
|
@@ -3027,17 +4201,16 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3027
4201
|
const prepared = prepareDelivery(record, now, heads);
|
|
3028
4202
|
if (prepared === void 0) return;
|
|
3029
4203
|
record = prepared;
|
|
4204
|
+
if (record.nextAttemptMs > now * 1e3) return;
|
|
3030
4205
|
if (!await stillActive(record, signal)) return;
|
|
3031
4206
|
await sendDelivery(prepared, signal, now);
|
|
3032
4207
|
} catch (error) {
|
|
3033
4208
|
if (signal.aborted) return;
|
|
3034
|
-
state.set(
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
state
|
|
3038
|
-
|
|
3039
|
-
lastError: "delivery-failed"
|
|
3040
|
-
});
|
|
4209
|
+
state.set(
|
|
4210
|
+
deliveryNamespace,
|
|
4211
|
+
subscription.subscriptionHash,
|
|
4212
|
+
failedDelivery(record, state.get(deliveryNamespace, subscription.subscriptionHash), deliveryMode, error, now)
|
|
4213
|
+
);
|
|
3041
4214
|
}
|
|
3042
4215
|
}
|
|
3043
4216
|
async function readSources(signal) {
|
|
@@ -3067,13 +4240,13 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3067
4240
|
if (subscriptions.length > timing.maxSubscriptions) throw new Error("Publisher subscription limit exceeded");
|
|
3068
4241
|
const active = new Set(subscriptions.map((subscription) => subscription.subscriptionHash));
|
|
3069
4242
|
if (active.size !== subscriptions.length) throw new Error("Duplicate subscription identity");
|
|
3070
|
-
const unavailable = state.entries(
|
|
4243
|
+
const unavailable = state.entries(deliveryNamespace).filter((item) => !active.has(item.key) && item.value.state !== "cancelled");
|
|
3071
4244
|
let available = true;
|
|
3072
4245
|
for (let offset = 0; offset < unavailable.length; offset += timing.concurrency) {
|
|
3073
4246
|
signal.throwIfAborted();
|
|
3074
4247
|
const results = await Promise.allSettled(unavailable.slice(offset, offset + timing.concurrency).map(async ({ key, value }) => {
|
|
3075
4248
|
const stillAuthorized = await timed(options.discovery.isActive(value.subscription, signal), signal);
|
|
3076
|
-
if (!stillAuthorized) state.set(
|
|
4249
|
+
if (!stillAuthorized) state.set(deliveryNamespace, key, {
|
|
3077
4250
|
...value,
|
|
3078
4251
|
pending: void 0,
|
|
3079
4252
|
state: "cancelled"
|
|
@@ -3085,8 +4258,8 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3085
4258
|
return available;
|
|
3086
4259
|
}
|
|
3087
4260
|
async function tick(actorSignal) {
|
|
3088
|
-
const
|
|
3089
|
-
const signal =
|
|
4261
|
+
const scope2 = publisherAbortScope([abort.signal, actorSignal]);
|
|
4262
|
+
const signal = scope2.signal;
|
|
3090
4263
|
try {
|
|
3091
4264
|
signal.throwIfAborted();
|
|
3092
4265
|
const now = clock();
|
|
@@ -3117,7 +4290,7 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3117
4290
|
if (!signal.aborted)
|
|
3118
4291
|
throw error;
|
|
3119
4292
|
} finally {
|
|
3120
|
-
|
|
4293
|
+
scope2.dispose();
|
|
3121
4294
|
}
|
|
3122
4295
|
}
|
|
3123
4296
|
const actor = await PublisherRuntimeActor.create({
|
|
@@ -3167,9 +4340,14 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3167
4340
|
throw new AggregateError(failures, "Publisher runtime shutdown failed");
|
|
3168
4341
|
})(),
|
|
3169
4342
|
status() {
|
|
3170
|
-
const deliveries = state.entries(
|
|
4343
|
+
const deliveries = state.entries(deliveryNamespace).map((item) => item.value);
|
|
3171
4344
|
return {
|
|
3172
4345
|
running,
|
|
4346
|
+
deliveryMode,
|
|
4347
|
+
...bestEffort === void 0 ? {} : { bestEffort: Object.freeze({ ...bestEffort }) },
|
|
4348
|
+
superseded: deliveries.reduce((sum, item) => sum + (item.superseded ?? 0), 0),
|
|
4349
|
+
abandoned: deliveries.reduce((sum, item) => sum + (item.abandoned ?? 0), 0),
|
|
4350
|
+
totalAttempts: deliveries.reduce((sum, item) => sum + (item.totalAttempts ?? 0), 0),
|
|
3173
4351
|
ready: running && ready,
|
|
3174
4352
|
clockHealthy,
|
|
3175
4353
|
degraded: !ready || deliveries.some((item) => item.state !== "cancelled" && item.failures > 0),
|
|
@@ -3181,10 +4359,15 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3181
4359
|
passes,
|
|
3182
4360
|
...lastError === void 0 ? {} : { lastError },
|
|
3183
4361
|
...options.discovery.status === void 0 ? {} : { handshake: options.discovery.status() },
|
|
3184
|
-
sources: state.entries("source-heads").map((item) => ({ id: item.key, ...item.value })),
|
|
3185
|
-
subscriptions: state.entries(
|
|
4362
|
+
sources: state.entries("source-heads").filter((item) => sourceReaders.has(item.key)).map((item) => ({ id: item.key, ...item.value })),
|
|
4363
|
+
subscriptions: state.entries(deliveryNamespace).map((item) => ({
|
|
3186
4364
|
id: item.key,
|
|
3187
4365
|
cursor: item.value.cursor,
|
|
4366
|
+
...deliveryMode === "best-effort" ? {
|
|
4367
|
+
...item.value.cursor < item.value.subscription.fromPosition ? {} : { lastAdmissionTarget: item.value.cursor },
|
|
4368
|
+
attempts: item.value.attempts ?? 0,
|
|
4369
|
+
lastHintAt: item.value.lastHintAt
|
|
4370
|
+
} : {},
|
|
3188
4371
|
state: item.value.state,
|
|
3189
4372
|
pending: item.value.pending !== void 0,
|
|
3190
4373
|
failures: item.value.failures
|
|
@@ -3275,40 +4458,41 @@ async function openPublisherDataLake(options) {
|
|
|
3275
4458
|
}
|
|
3276
4459
|
|
|
3277
4460
|
// src/runtime/sqliteHandshakeStores.ts
|
|
3278
|
-
import { isDeepStrictEqual as
|
|
4461
|
+
import { isDeepStrictEqual as isDeepStrictEqual5 } from "node:util";
|
|
3279
4462
|
import { endpointChallengeHash as endpointChallengeHash2 } from "@xyo-network/event-kit-protocol";
|
|
3280
4463
|
import {
|
|
3281
4464
|
Base64Url32BytesZod as Base64Url32BytesZod3,
|
|
3282
4465
|
EndpointChallengeZod as EndpointChallengeZod3,
|
|
3283
4466
|
HandshakeIdZod as HandshakeIdZod4,
|
|
3284
|
-
HashZod as
|
|
3285
|
-
NormalizedAddressZod as
|
|
4467
|
+
HashZod as HashZod9,
|
|
4468
|
+
NormalizedAddressZod as NormalizedAddressZod8
|
|
3286
4469
|
} from "@xyo-network/event-kit-schemas";
|
|
3287
|
-
import { z as
|
|
3288
|
-
var KeyZod =
|
|
3289
|
-
encryptionKey:
|
|
4470
|
+
import { z as z6 } from "zod";
|
|
4471
|
+
var KeyZod = z6.strictObject({
|
|
4472
|
+
encryptionKey: HashZod9,
|
|
3290
4473
|
keyId: HandshakeIdZod4,
|
|
3291
4474
|
privateKey: Base64Url32BytesZod3,
|
|
3292
|
-
publisher:
|
|
4475
|
+
publisher: NormalizedAddressZod8
|
|
3293
4476
|
});
|
|
3294
4477
|
var phases = ["signed", "content-written", "content-verified", "broadcast", "included", "finalized"];
|
|
3295
4478
|
function requireTime2(value) {
|
|
3296
4479
|
if (!Number.isSafeInteger(value) || value < 0) throw new Error("Publisher clock is invalid");
|
|
3297
4480
|
}
|
|
3298
4481
|
function samePublicationIntent(left, right) {
|
|
3299
|
-
return left.chainId === right.chainId && left.source === right.source && left.operationId === right.operationId &&
|
|
4482
|
+
return left.chainId === right.chainId && left.source === right.source && left.operationId === right.operationId && isDeepStrictEqual5(left.eventHashes, right.eventHashes) && isDeepStrictEqual5(left.attachmentHashes, right.attachmentHashes);
|
|
3300
4483
|
}
|
|
3301
4484
|
function publicationKey(intent) {
|
|
3302
|
-
if (!/^[0-9a-f]{40}$/u.test(intent.chainId) || !/^[0-9a-f]{40}$/u.test(intent.source) || intent.operationId.length === 0 || intent.operationId.length > 256 || [...intent.eventHashes, ...intent.attachmentHashes].some((hash) => !
|
|
4485
|
+
if (!/^[0-9a-f]{40}$/u.test(intent.chainId) || !/^[0-9a-f]{40}$/u.test(intent.source) || intent.operationId.length === 0 || intent.operationId.length > 256 || [...intent.eventHashes, ...intent.attachmentHashes].some((hash) => !HashZod9.safeParse(hash).success)) {
|
|
3303
4486
|
throw new Error("Invalid publication intent");
|
|
3304
4487
|
}
|
|
3305
4488
|
return JSON.stringify([intent.chainId, intent.source, intent.operationId]);
|
|
3306
4489
|
}
|
|
3307
|
-
function createSqlitePublisherHandshakeStores(state) {
|
|
4490
|
+
function createSqlitePublisherHandshakeStores(state, options = {}) {
|
|
4491
|
+
const replayKey = options.replayNamespace === void 0 ? "checkpoint" : `checkpoint:${z6.string().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/u).parse(options.replayNamespace)}`;
|
|
3308
4492
|
const replay = {
|
|
3309
|
-
load: async () => state.get("replay",
|
|
4493
|
+
load: async () => state.get("replay", replayKey),
|
|
3310
4494
|
async save(checkpoint) {
|
|
3311
|
-
state.set("replay",
|
|
4495
|
+
state.set("replay", replayKey, checkpoint);
|
|
3312
4496
|
}
|
|
3313
4497
|
};
|
|
3314
4498
|
const intents = {
|
|
@@ -3332,11 +4516,11 @@ function createSqlitePublisherHandshakeStores(state) {
|
|
|
3332
4516
|
};
|
|
3333
4517
|
const keys = {
|
|
3334
4518
|
async getPrivateKey(input) {
|
|
3335
|
-
const key = `${
|
|
4519
|
+
const key = `${NormalizedAddressZod8.parse(input.publisher)}:${HandshakeIdZod4.parse(input.keyId)}`;
|
|
3336
4520
|
const raw = state.get("encryption-keys", key);
|
|
3337
4521
|
if (raw === void 0) return;
|
|
3338
4522
|
const record = KeyZod.parse(raw);
|
|
3339
|
-
if (record.encryptionKey !==
|
|
4523
|
+
if (record.encryptionKey !== HashZod9.parse(input.encryptionKey)) throw new Error("Encryption key identity mismatch");
|
|
3340
4524
|
const { publisher: _publisher, ...privateKey } = record;
|
|
3341
4525
|
return privateKey;
|
|
3342
4526
|
},
|
|
@@ -3346,7 +4530,7 @@ function createSqlitePublisherHandshakeStores(state) {
|
|
|
3346
4530
|
const key = `${record.publisher}:${record.keyId}`;
|
|
3347
4531
|
const prior = state.get("encryption-keys", key);
|
|
3348
4532
|
if (prior !== void 0) {
|
|
3349
|
-
if (!
|
|
4533
|
+
if (!isDeepStrictEqual5(prior, record)) throw new Error("Encryption key collision");
|
|
3350
4534
|
return "duplicate";
|
|
3351
4535
|
}
|
|
3352
4536
|
state.set("encryption-keys", key, record);
|
|
@@ -3354,19 +4538,19 @@ function createSqlitePublisherHandshakeStores(state) {
|
|
|
3354
4538
|
});
|
|
3355
4539
|
}
|
|
3356
4540
|
};
|
|
3357
|
-
const current = (hash) => state.get("challenges",
|
|
4541
|
+
const current = (hash) => state.get("challenges", HashZod9.parse(hash));
|
|
3358
4542
|
const live = (record, now) => record.challenge.issuedAt <= now && now < record.challenge.expiresAt;
|
|
3359
4543
|
const challenges = {
|
|
3360
4544
|
get: async (hash) => current(hash),
|
|
3361
4545
|
async issue(input) {
|
|
3362
4546
|
const challenge = EndpointChallengeZod3.parse(input.challenge);
|
|
3363
|
-
const hash =
|
|
4547
|
+
const hash = HashZod9.parse(input.challengeHash);
|
|
3364
4548
|
if (await endpointChallengeHash2(challenge) !== hash) return { kind: "collision" };
|
|
3365
4549
|
return state.transaction(() => {
|
|
3366
4550
|
const priorNonce = state.get("challenge-nonces", challenge.nonce);
|
|
3367
4551
|
if (priorNonce !== void 0 && priorNonce !== hash) return { kind: "collision" };
|
|
3368
4552
|
const prior = current(hash);
|
|
3369
|
-
if (prior !== void 0) return
|
|
4553
|
+
if (prior !== void 0) return isDeepStrictEqual5(prior.challenge, challenge) ? { kind: "duplicate", record: prior } : { kind: "collision" };
|
|
3370
4554
|
const record = {
|
|
3371
4555
|
challenge,
|
|
3372
4556
|
challengeHash: hash,
|
|
@@ -3392,7 +4576,7 @@ function createSqlitePublisherHandshakeStores(state) {
|
|
|
3392
4576
|
},
|
|
3393
4577
|
async completeVerified(input) {
|
|
3394
4578
|
requireTime2(input.nowSeconds);
|
|
3395
|
-
const responseHash =
|
|
4579
|
+
const responseHash = HashZod9.parse(input.responseHash);
|
|
3396
4580
|
return state.transaction(() => {
|
|
3397
4581
|
const record = current(input.challengeHash);
|
|
3398
4582
|
if (record === void 0) return { kind: "missing" };
|
|
@@ -3419,7 +4603,7 @@ function createSqlitePublisherHandshakeStores(state) {
|
|
|
3419
4603
|
for (const phase of phases) {
|
|
3420
4604
|
const record = state.get(`publication:${phase}`, key);
|
|
3421
4605
|
if (record === void 0) continue;
|
|
3422
|
-
if (record.phase !== phase || !samePublicationIntent(record, intent) || !
|
|
4606
|
+
if (record.phase !== phase || !samePublicationIntent(record, intent) || !isDeepStrictEqual5(record.evidence, signed.evidence)) {
|
|
3423
4607
|
throw new Error("Publication evidence collision");
|
|
3424
4608
|
}
|
|
3425
4609
|
latest = record;
|
|
@@ -3428,17 +4612,17 @@ function createSqlitePublisherHandshakeStores(state) {
|
|
|
3428
4612
|
},
|
|
3429
4613
|
async save(record) {
|
|
3430
4614
|
const key = publicationKey(record);
|
|
3431
|
-
|
|
4615
|
+
HashZod9.parse(record.evidence.transactionHash);
|
|
3432
4616
|
if (!phases.includes(record.phase)) throw new Error("Unknown publication phase");
|
|
3433
4617
|
state.transaction(() => {
|
|
3434
4618
|
const signed = state.get("publication:signed", key);
|
|
3435
4619
|
if (signed === void 0 && record.phase !== "signed") throw new Error("Publication must bind signed evidence first");
|
|
3436
|
-
if (signed !== void 0 && (!samePublicationIntent(signed, record) || !
|
|
4620
|
+
if (signed !== void 0 && (!samePublicationIntent(signed, record) || !isDeepStrictEqual5(signed.evidence, record.evidence))) {
|
|
3437
4621
|
throw new Error("Publication intent or evidence collision");
|
|
3438
4622
|
}
|
|
3439
4623
|
const namespace = `publication:${record.phase}`;
|
|
3440
4624
|
const prior = state.get(namespace, key);
|
|
3441
|
-
if (prior !== void 0 && !
|
|
4625
|
+
if (prior !== void 0 && !isDeepStrictEqual5(prior, record)) throw new Error("Publication phase collision");
|
|
3442
4626
|
if (prior === void 0) state.set(namespace, key, record);
|
|
3443
4627
|
});
|
|
3444
4628
|
}
|
|
@@ -3453,15 +4637,15 @@ function createSqlitePublisherHandshakeStores(state) {
|
|
|
3453
4637
|
}
|
|
3454
4638
|
|
|
3455
4639
|
// src/runtime/sqlitePublisherState.ts
|
|
3456
|
-
import { randomBytes as
|
|
3457
|
-
import { constants } from "node:fs";
|
|
4640
|
+
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
4641
|
+
import { constants as constants3 } from "node:fs";
|
|
3458
4642
|
import {
|
|
3459
|
-
lstat,
|
|
3460
|
-
mkdir as
|
|
3461
|
-
open as
|
|
4643
|
+
lstat as lstat5,
|
|
4644
|
+
mkdir as mkdir4,
|
|
4645
|
+
open as open6
|
|
3462
4646
|
} from "node:fs/promises";
|
|
3463
4647
|
import { hostname } from "node:os";
|
|
3464
|
-
import
|
|
4648
|
+
import PATH16 from "node:path";
|
|
3465
4649
|
function decode(value) {
|
|
3466
4650
|
return JSON.parse(value, (_key, candidate) => {
|
|
3467
4651
|
return 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) ? BigInt(candidate.$eventKitBigInt) : candidate;
|
|
@@ -3482,13 +4666,13 @@ function processExists(pid) {
|
|
|
3482
4666
|
async function openSqlitePublisherState(options) {
|
|
3483
4667
|
if (options.path === ":memory:" || options.identity.length === 0)
|
|
3484
4668
|
throw new Error("Persistent publisher needs a file and an identity");
|
|
3485
|
-
const path =
|
|
3486
|
-
const directory =
|
|
3487
|
-
await
|
|
3488
|
-
const metadata = await
|
|
4669
|
+
const path = PATH16.resolve(options.path);
|
|
4670
|
+
const directory = PATH16.dirname(path);
|
|
4671
|
+
await mkdir4(directory, { recursive: true, mode: 448 });
|
|
4672
|
+
const metadata = await lstat5(directory);
|
|
3489
4673
|
if (!metadata.isDirectory() || (metadata.mode & 63) !== 0)
|
|
3490
4674
|
throw new Error("Publisher state directory must be a real directory with 0700 permissions");
|
|
3491
|
-
const file = await
|
|
4675
|
+
const file = await open6(path, constants3.O_CREAT | constants3.O_RDWR | constants3.O_NOFOLLOW, 384);
|
|
3492
4676
|
try {
|
|
3493
4677
|
const metadata2 = await file.stat();
|
|
3494
4678
|
if (!metadata2.isFile() || (metadata2.mode & 63) !== 0)
|
|
@@ -3497,9 +4681,9 @@ async function openSqlitePublisherState(options) {
|
|
|
3497
4681
|
} finally {
|
|
3498
4682
|
await file.close();
|
|
3499
4683
|
}
|
|
3500
|
-
const { DatabaseSync:
|
|
3501
|
-
const database = new
|
|
3502
|
-
const token =
|
|
4684
|
+
const { DatabaseSync: DatabaseSync3 } = await import("node:sqlite");
|
|
4685
|
+
const database = new DatabaseSync3(path);
|
|
4686
|
+
const token = randomBytes8(16).toString("hex");
|
|
3503
4687
|
let closed = false;
|
|
3504
4688
|
let inTransaction = false;
|
|
3505
4689
|
const transaction = (work) => {
|
|
@@ -3590,67 +4774,362 @@ async function openSqlitePublisherState(options) {
|
|
|
3590
4774
|
};
|
|
3591
4775
|
}
|
|
3592
4776
|
|
|
3593
|
-
// src/
|
|
3594
|
-
import {
|
|
3595
|
-
import
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
4777
|
+
// src/sqliteBestEffortInbox.ts
|
|
4778
|
+
import { createHash as createHash5, randomBytes as randomBytes9 } from "node:crypto";
|
|
4779
|
+
import {
|
|
4780
|
+
canonicalJson as canonicalJson4,
|
|
4781
|
+
commitBestEffortSecurity,
|
|
4782
|
+
createBestEffortSecurityState,
|
|
4783
|
+
parseAdmittedWake as parseAdmittedWake4,
|
|
4784
|
+
parseBestEffortSecurityState,
|
|
4785
|
+
wakeSourceHash
|
|
4786
|
+
} from "@xyo-network/event-kit-protocol";
|
|
4787
|
+
import {
|
|
4788
|
+
ChainIdZod as ChainIdZod3,
|
|
4789
|
+
DeploymentIdZod,
|
|
4790
|
+
HashZod as HashZod10,
|
|
4791
|
+
NonnegativeJsonSafeIntegerZod as NonnegativeJsonSafeIntegerZod5,
|
|
4792
|
+
NormalizedAddressZod as NormalizedAddressZod9,
|
|
4793
|
+
WakeJwtAudienceZod as WakeJwtAudienceZod4,
|
|
4794
|
+
WakeSourceZod as WakeSourceZod3
|
|
4795
|
+
} from "@xyo-network/event-kit-schemas";
|
|
4796
|
+
import { z as z7 } from "zod";
|
|
4797
|
+
var BindingZod = z7.strictObject({
|
|
4798
|
+
audience: WakeJwtAudienceZod4,
|
|
4799
|
+
authorizationChainId: ChainIdZod3,
|
|
4800
|
+
consumerId: z7.string().min(1).max(255),
|
|
4801
|
+
controller: NormalizedAddressZod9,
|
|
4802
|
+
deploymentId: DeploymentIdZod,
|
|
4803
|
+
filterHash: HashZod10,
|
|
4804
|
+
publisher: NormalizedAddressZod9,
|
|
4805
|
+
queueId: z7.string().min(1).max(255).optional(),
|
|
4806
|
+
source: WakeSourceZod3,
|
|
4807
|
+
subscriptionHash: HashZod10
|
|
4808
|
+
});
|
|
4809
|
+
var SnapshotPositionZod = z7.strictObject({
|
|
4810
|
+
position: NonnegativeJsonSafeIntegerZod5,
|
|
4811
|
+
hash: HashZod10.optional()
|
|
4812
|
+
});
|
|
4813
|
+
var TargetZod = z7.strictObject({
|
|
4814
|
+
binding: BindingZod,
|
|
4815
|
+
completedGeneration: NonnegativeJsonSafeIntegerZod5,
|
|
4816
|
+
generation: NonnegativeJsonSafeIntegerZod5,
|
|
4817
|
+
id: HashZod10,
|
|
4818
|
+
requestedPosition: NonnegativeJsonSafeIntegerZod5,
|
|
4819
|
+
fence: NonnegativeJsonSafeIntegerZod5,
|
|
4820
|
+
previousSnapshot: SnapshotPositionZod.optional(),
|
|
4821
|
+
lease: z7.strictObject({
|
|
4822
|
+
expiresAt: NonnegativeJsonSafeIntegerZod5,
|
|
4823
|
+
token: z7.string().regex(/^[0-9a-f]{32}$/u)
|
|
4824
|
+
}).optional()
|
|
4825
|
+
}).refine(
|
|
4826
|
+
(target) => target.generation > 0 && target.completedGeneration <= target.generation && (target.lease === void 0 || target.fence > 0 && target.generation > target.completedGeneration) && (target.previousSnapshot === void 0 || target.binding.source.kind === "utc-interval" || target.previousSnapshot.hash !== void 0),
|
|
4827
|
+
"Invalid Best Effort dirty generation"
|
|
4828
|
+
);
|
|
4829
|
+
var CatalogZod = z7.strictObject({
|
|
4830
|
+
id: HashZod10,
|
|
4831
|
+
binding: z7.string(),
|
|
4832
|
+
target_hash: HashZod10,
|
|
4833
|
+
fence: NonnegativeJsonSafeIntegerZod5,
|
|
4834
|
+
generation: NonnegativeJsonSafeIntegerZod5
|
|
4835
|
+
}).refine((record) => record.generation > 0, "Invalid Best Effort catalog generation");
|
|
4836
|
+
function bindingId(binding) {
|
|
4837
|
+
return createHash5("sha256").update(canonicalJson4(binding)).digest("hex");
|
|
4838
|
+
}
|
|
4839
|
+
function targetHash(body) {
|
|
4840
|
+
return createHash5("sha256").update(body).digest("hex");
|
|
4841
|
+
}
|
|
4842
|
+
function parseCatalog(value) {
|
|
4843
|
+
const record = CatalogZod.parse(value);
|
|
4844
|
+
const binding = BindingZod.parse(JSON.parse(record.binding));
|
|
4845
|
+
if (record.id !== bindingId(binding) || record.binding !== canonicalJson4(binding))
|
|
4846
|
+
throw new TypeError("Persisted Best Effort catalog changed exact binding");
|
|
4847
|
+
return record;
|
|
4848
|
+
}
|
|
4849
|
+
function parseTarget(body, expectedId) {
|
|
4850
|
+
if (typeof body !== "string") throw new TypeError("Malformed persisted Best Effort target");
|
|
4851
|
+
const parsed = TargetZod.parse(JSON.parse(body));
|
|
4852
|
+
if (parsed.id !== expectedId || bindingId(parsed.binding) !== expectedId)
|
|
4853
|
+
throw new TypeError("Persisted Best Effort target changed exact scope");
|
|
4854
|
+
return parsed;
|
|
4855
|
+
}
|
|
4856
|
+
function publicTarget(target) {
|
|
3629
4857
|
return {
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
4858
|
+
binding: target.binding,
|
|
4859
|
+
completedGeneration: target.completedGeneration,
|
|
4860
|
+
generation: target.generation,
|
|
4861
|
+
id: target.id,
|
|
4862
|
+
requestedPosition: target.requestedPosition
|
|
4863
|
+
};
|
|
4864
|
+
}
|
|
4865
|
+
function increment(value) {
|
|
4866
|
+
if (!Number.isSafeInteger(value + 1)) throw new RangeError("Best Effort fence/generation exhausted");
|
|
4867
|
+
return value + 1;
|
|
4868
|
+
}
|
|
4869
|
+
async function createSqliteBestEffortInbox(options) {
|
|
4870
|
+
const consumerId = z7.string().min(1).max(255).parse(options.consumerId);
|
|
4871
|
+
const maxTargets = options.maxTargets ?? 1024;
|
|
4872
|
+
if (!Number.isSafeInteger(options.maxSecurityRecords) || options.maxSecurityRecords < 1 || options.maxSecurityRecords > 65536)
|
|
4873
|
+
throw new TypeError("Invalid finite Best Effort security capacity");
|
|
4874
|
+
if (!Number.isSafeInteger(maxTargets) || maxTargets < 1 || maxTargets > 65536)
|
|
4875
|
+
throw new TypeError("Invalid finite Best Effort target capacity");
|
|
4876
|
+
const nowSeconds = options.nowSeconds ?? (() => Math.floor(Date.now() / 1e3));
|
|
4877
|
+
const connection = openPrivateSqlite({ path: options.path, format: "event-kit-best-effort-inbox" });
|
|
4878
|
+
const { database } = connection;
|
|
4879
|
+
const transaction = (work) => connection.transaction(work);
|
|
4880
|
+
try {
|
|
4881
|
+
const settings = canonicalJson4({
|
|
4882
|
+
consumerId,
|
|
4883
|
+
maxSecurityRecords: options.maxSecurityRecords,
|
|
4884
|
+
maxTargets
|
|
4885
|
+
});
|
|
4886
|
+
transaction(() => {
|
|
4887
|
+
const marker = database.prepare("SELECT initialized FROM eventkit_store WHERE id = 1").get();
|
|
4888
|
+
if (marker?.initialized !== 0 && marker?.initialized !== 1)
|
|
4889
|
+
throw new TypeError("Invalid Best Effort store initialization authority");
|
|
4890
|
+
const tables = database.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
|
|
4891
|
+
const names = new Set(tables.map((row) => row.name));
|
|
4892
|
+
if (marker.initialized === 1) {
|
|
4893
|
+
if (["best_effort_security", "best_effort_targets", "best_effort_settings", "best_effort_catalog"].some((name) => !names.has(name)))
|
|
4894
|
+
throw new TypeError("Initialized Best Effort store lost authoritative tables; restore its custody before reopening");
|
|
4895
|
+
const previous = database.prepare("SELECT body FROM best_effort_settings WHERE id = 1").get();
|
|
4896
|
+
if (previous === void 0) throw new TypeError("Missing immutable Best Effort store settings");
|
|
4897
|
+
if (previous.body !== settings) throw new TypeError("Best Effort store consumer or capacity binding changed");
|
|
4898
|
+
return;
|
|
4899
|
+
}
|
|
4900
|
+
if (tables.some((row) => row.name !== "eventkit_store"))
|
|
4901
|
+
throw new TypeError("Uninitialized Best Effort bootstrap contains unexpected custody");
|
|
4902
|
+
database.exec(`
|
|
4903
|
+
CREATE TABLE best_effort_security (id INTEGER PRIMARY KEY CHECK (id = 1), body TEXT NOT NULL);
|
|
4904
|
+
CREATE TABLE best_effort_targets (id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
|
4905
|
+
CREATE TABLE best_effort_settings (id INTEGER PRIMARY KEY CHECK (id = 1), body TEXT NOT NULL, catalog_count INTEGER NOT NULL);
|
|
4906
|
+
CREATE TABLE best_effort_catalog (id TEXT PRIMARY KEY, binding TEXT NOT NULL, target_hash TEXT NOT NULL, fence INTEGER NOT NULL, generation INTEGER NOT NULL);
|
|
4907
|
+
`);
|
|
4908
|
+
database.prepare("INSERT INTO best_effort_settings (id, body, catalog_count) VALUES (1, ?, 0)").run(settings);
|
|
4909
|
+
database.prepare("INSERT INTO best_effort_security (id, body) VALUES (1, ?)").run(JSON.stringify(createBestEffortSecurityState()));
|
|
4910
|
+
database.prepare("UPDATE eventkit_store SET initialized = 1 WHERE id = 1").run();
|
|
4911
|
+
});
|
|
4912
|
+
const readSecurity = () => {
|
|
4913
|
+
const row = database.prepare("SELECT body FROM best_effort_security WHERE id = 1").get();
|
|
4914
|
+
if (typeof row?.body !== "string") throw new TypeError("Missing persisted Best Effort security state");
|
|
4915
|
+
const parsed = parseBestEffortSecurityState(JSON.parse(row.body));
|
|
4916
|
+
if (parsed.jwtRecords.length > options.maxSecurityRecords || parsed.wakeRecords.length > options.maxSecurityRecords)
|
|
4917
|
+
throw new TypeError("Persisted Best Effort security exceeds its configured bound");
|
|
4918
|
+
return parsed;
|
|
4919
|
+
};
|
|
4920
|
+
const readCatalog = (id) => {
|
|
4921
|
+
const row = database.prepare("SELECT id, binding, target_hash, fence, generation FROM best_effort_catalog WHERE id = ?").get(id);
|
|
4922
|
+
return row === void 0 ? void 0 : parseCatalog(row);
|
|
4923
|
+
};
|
|
4924
|
+
const verifiedTarget = (body, catalog) => {
|
|
4925
|
+
const target = parseTarget(body, catalog.id);
|
|
4926
|
+
if (target.binding.consumerId !== consumerId) throw new TypeError("Persisted Best Effort target changed consumer");
|
|
4927
|
+
if (typeof body !== "string" || catalog.target_hash !== targetHash(body) || catalog.binding !== canonicalJson4(target.binding) || catalog.fence !== target.fence || catalog.generation !== target.generation)
|
|
4928
|
+
throw new TypeError("Persisted Best Effort target differs from its permanent catalog authority");
|
|
4929
|
+
return target;
|
|
4930
|
+
};
|
|
4931
|
+
const readTargets = () => {
|
|
4932
|
+
const setting = database.prepare("SELECT catalog_count FROM best_effort_settings WHERE id = 1").get();
|
|
4933
|
+
const expectedCount = NonnegativeJsonSafeIntegerZod5.parse(setting?.catalog_count);
|
|
4934
|
+
if (expectedCount > maxTargets) throw new TypeError("Persisted Best Effort catalog capacity exceeded");
|
|
4935
|
+
const catalogs = database.prepare("SELECT id, binding, target_hash, fence, generation FROM best_effort_catalog ORDER BY id").all().map(parseCatalog);
|
|
4936
|
+
const rows = database.prepare("SELECT id, body FROM best_effort_targets ORDER BY id").all();
|
|
4937
|
+
if (catalogs.length !== expectedCount || rows.length !== expectedCount)
|
|
4938
|
+
throw new TypeError("Persisted Best Effort binding catalog lost authoritative target custody");
|
|
4939
|
+
const byId = new Map(catalogs.map((catalog) => [catalog.id, catalog]));
|
|
4940
|
+
return rows.map((row) => {
|
|
4941
|
+
if (typeof row.id !== "string") throw new TypeError("Malformed persisted Best Effort target identity");
|
|
4942
|
+
const catalog = byId.get(row.id);
|
|
4943
|
+
if (catalog === void 0) throw new TypeError("Persisted Best Effort target lost permanent binding custody");
|
|
4944
|
+
return verifiedTarget(row.body, catalog);
|
|
4945
|
+
});
|
|
4946
|
+
};
|
|
4947
|
+
const readTarget = (id) => {
|
|
4948
|
+
const row = database.prepare("SELECT body FROM best_effort_targets WHERE id = ?").get(id);
|
|
4949
|
+
const catalog = readCatalog(id);
|
|
4950
|
+
if (row === void 0 && catalog === void 0) return;
|
|
4951
|
+
if (row === void 0 || catalog === void 0)
|
|
4952
|
+
throw new TypeError("Persisted Best Effort binding lost its authoritative target/catalog custody");
|
|
4953
|
+
return verifiedTarget(row.body, catalog);
|
|
4954
|
+
};
|
|
4955
|
+
const saveTarget = (target) => {
|
|
4956
|
+
const catalog = readCatalog(target.id);
|
|
4957
|
+
const body = JSON.stringify(target);
|
|
4958
|
+
const binding = canonicalJson4(target.binding);
|
|
4959
|
+
if (catalog === void 0) {
|
|
4960
|
+
if (database.prepare("SELECT id FROM best_effort_targets WHERE id = ?").get(target.id) !== void 0)
|
|
4961
|
+
throw new TypeError("Existing Best Effort target lost its permanent binding catalog");
|
|
4962
|
+
database.prepare("INSERT INTO best_effort_catalog (id, binding, target_hash, fence, generation) VALUES (?, ?, ?, ?, ?)").run(target.id, binding, targetHash(body), target.fence, target.generation);
|
|
4963
|
+
database.prepare("UPDATE best_effort_settings SET catalog_count = catalog_count + 1 WHERE id = 1").run();
|
|
4964
|
+
} else {
|
|
4965
|
+
if (catalog.binding !== binding || target.fence < catalog.fence || target.generation < catalog.generation)
|
|
4966
|
+
throw new TypeError("Best Effort target mutation would regress permanent binding authority");
|
|
4967
|
+
database.prepare("UPDATE best_effort_catalog SET target_hash = ?, fence = ?, generation = ? WHERE id = ?").run(targetHash(body), target.fence, target.generation, target.id);
|
|
4968
|
+
}
|
|
4969
|
+
database.prepare("INSERT INTO best_effort_targets (id, body) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET body = excluded.body").run(target.id, body);
|
|
4970
|
+
};
|
|
4971
|
+
transaction(() => {
|
|
4972
|
+
const security = readSecurity();
|
|
4973
|
+
const targets = readTargets();
|
|
4974
|
+
if (security.jwtRecords.length > options.maxSecurityRecords || security.wakeRecords.length > options.maxSecurityRecords || targets.length > maxTargets || targets.some((target) => target.binding.consumerId !== consumerId))
|
|
4975
|
+
throw new TypeError("Persisted Best Effort state exceeds its exact bound");
|
|
4976
|
+
});
|
|
4977
|
+
async function requestWork(target) {
|
|
4978
|
+
if (target.generation === target.completedGeneration || options.requestWork === void 0) return false;
|
|
4979
|
+
try {
|
|
4980
|
+
await options.requestWork(publicTarget(target));
|
|
4981
|
+
return true;
|
|
4982
|
+
} catch {
|
|
4983
|
+
return false;
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4986
|
+
const currentTarget = (captured) => {
|
|
4987
|
+
const current = readTarget(captured.id);
|
|
4988
|
+
const now = NonnegativeJsonSafeIntegerZod5.parse(nowSeconds());
|
|
4989
|
+
return current?.lease?.token === captured.lease?.token && current?.fence === captured.fence && current?.generation === captured.generation && current.lease !== void 0 && current.lease.expiresAt > now ? current : void 0;
|
|
4990
|
+
};
|
|
4991
|
+
return {
|
|
4992
|
+
async commitAuthorized(input) {
|
|
4993
|
+
input = structuredClone(input);
|
|
4994
|
+
const wake = await parseAdmittedWake4(input.wake);
|
|
4995
|
+
const grant = input.grant.grant;
|
|
4996
|
+
if (grant.authorizationKind !== "activated-subscription" || grant.target !== wake.publisher || grant.authorizationChainId !== wake.event.authorizationChainId || grant.deploymentId !== wake.deploymentId || grant.filterHash !== wake.event.filterHash || grant.queueId !== wake.queueId || grant.sourceHash !== await wakeSourceHash(wake.event.source))
|
|
4997
|
+
throw new TypeError("Best Effort grant does not match its exact admitted profile");
|
|
4998
|
+
const binding = BindingZod.parse({
|
|
4999
|
+
consumerId,
|
|
5000
|
+
audience: wake.audience,
|
|
5001
|
+
authorizationChainId: wake.event.authorizationChainId,
|
|
5002
|
+
controller: grant.controller,
|
|
5003
|
+
deploymentId: wake.deploymentId,
|
|
5004
|
+
filterHash: wake.event.filterHash,
|
|
5005
|
+
publisher: wake.publisher,
|
|
5006
|
+
source: wake.event.source,
|
|
5007
|
+
subscriptionHash: grant.subscriptionHash,
|
|
5008
|
+
...wake.queueId === void 0 ? {} : { queueId: wake.queueId }
|
|
5009
|
+
});
|
|
5010
|
+
const id = bindingId(binding);
|
|
5011
|
+
const committed = transaction(() => {
|
|
5012
|
+
const authority = { security: readSecurity(), targets: readTargets() };
|
|
5013
|
+
const next = commitBestEffortSecurity(authority.security, { ...input, wake }, options.maxSecurityRecords);
|
|
5014
|
+
if (next.result.kind !== "accepted" && next.result.kind !== "duplicate") return { result: next.result };
|
|
5015
|
+
const previous = authority.targets.find((target2) => target2.id === id);
|
|
5016
|
+
if (previous === void 0 && next.result.kind === "duplicate")
|
|
5017
|
+
throw new TypeError("Persisted duplicate lost its authoritative Best Effort target");
|
|
5018
|
+
if (previous === void 0 && authority.targets.length >= maxTargets)
|
|
5019
|
+
return { result: { kind: "capacity-exceeded" } };
|
|
5020
|
+
const target = previous === void 0 ? {
|
|
5021
|
+
binding,
|
|
5022
|
+
id,
|
|
5023
|
+
completedGeneration: 0,
|
|
5024
|
+
generation: 1,
|
|
5025
|
+
fence: 0,
|
|
5026
|
+
requestedPosition: wake.toPosition
|
|
5027
|
+
} : next.result.kind === "duplicate" ? previous : {
|
|
5028
|
+
...previous,
|
|
5029
|
+
generation: increment(previous.generation),
|
|
5030
|
+
requestedPosition: Math.max(previous.requestedPosition, wake.toPosition)
|
|
5031
|
+
};
|
|
5032
|
+
database.prepare("UPDATE best_effort_security SET body = ? WHERE id = 1").run(JSON.stringify(next.state));
|
|
5033
|
+
saveTarget(target);
|
|
5034
|
+
return { result: next.result, target };
|
|
5035
|
+
});
|
|
5036
|
+
if (committed.target !== void 0) await requestWork(committed.target);
|
|
5037
|
+
return committed.result;
|
|
5038
|
+
},
|
|
5039
|
+
dirtyTargets: async () => transaction(() => readTargets().filter((target) => target.generation > target.completedGeneration).map(publicTarget)),
|
|
5040
|
+
async repairDirty() {
|
|
5041
|
+
let failed = 0;
|
|
5042
|
+
let scheduled = 0;
|
|
5043
|
+
const targets = transaction(() => readTargets());
|
|
5044
|
+
for (const target of targets) {
|
|
5045
|
+
if (options.requestWork === void 0 || target.generation === target.completedGeneration) continue;
|
|
5046
|
+
if (await requestWork(target)) scheduled += 1;
|
|
5047
|
+
else failed += 1;
|
|
5048
|
+
}
|
|
5049
|
+
return { failed, scheduled };
|
|
5050
|
+
},
|
|
5051
|
+
async recoverExpired(now) {
|
|
5052
|
+
NonnegativeJsonSafeIntegerZod5.parse(now);
|
|
5053
|
+
return transaction(() => {
|
|
5054
|
+
let recovered = 0;
|
|
5055
|
+
for (const target of readTargets()) {
|
|
5056
|
+
if (target.lease === void 0 || target.lease.expiresAt > now) continue;
|
|
5057
|
+
saveTarget({ ...target, lease: void 0 });
|
|
5058
|
+
recovered += 1;
|
|
5059
|
+
}
|
|
5060
|
+
return recovered;
|
|
5061
|
+
});
|
|
5062
|
+
},
|
|
5063
|
+
async claim(input) {
|
|
5064
|
+
NonnegativeJsonSafeIntegerZod5.parse(input.nowSeconds);
|
|
5065
|
+
if (!Number.isSafeInteger(input.leaseSeconds) || input.leaseSeconds < 1 || !Number.isSafeInteger(input.nowSeconds + input.leaseSeconds))
|
|
5066
|
+
throw new TypeError("Invalid Best Effort worker lease lifetime");
|
|
5067
|
+
const captured = transaction(() => {
|
|
5068
|
+
const target = readTargets().find((item) => item.generation > item.completedGeneration && item.lease === void 0);
|
|
5069
|
+
if (target === void 0) return;
|
|
5070
|
+
const claimed = {
|
|
5071
|
+
...target,
|
|
5072
|
+
fence: increment(target.fence),
|
|
5073
|
+
lease: { token: randomBytes9(16).toString("hex"), expiresAt: input.nowSeconds + input.leaseSeconds }
|
|
5074
|
+
};
|
|
5075
|
+
saveTarget(claimed);
|
|
5076
|
+
return claimed;
|
|
5077
|
+
});
|
|
5078
|
+
if (captured?.lease === void 0) return void 0;
|
|
3638
5079
|
return {
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
5080
|
+
...publicTarget(captured),
|
|
5081
|
+
fence: captured.fence,
|
|
5082
|
+
leaseToken: captured.lease.token,
|
|
5083
|
+
...captured.previousSnapshot === void 0 ? {} : { previousSnapshot: captured.previousSnapshot },
|
|
5084
|
+
isCurrent: async () => transaction(() => currentTarget(captured) !== void 0),
|
|
5085
|
+
async complete(snapshot) {
|
|
5086
|
+
const position = SnapshotPositionZod.parse({ position: snapshot.position, ...snapshot.hash === void 0 ? {} : { hash: snapshot.hash } });
|
|
5087
|
+
return transaction(() => {
|
|
5088
|
+
const current = currentTarget(captured);
|
|
5089
|
+
if (current === void 0) return false;
|
|
5090
|
+
if (current.binding.source.kind !== "utc-interval" && position.hash === void 0)
|
|
5091
|
+
throw new TypeError("Finalized snapshot completion requires its verified hash");
|
|
5092
|
+
const previous = current.previousSnapshot;
|
|
5093
|
+
if (previous !== void 0 && (position.position < previous.position || position.position === previous.position && position.hash !== previous.hash))
|
|
5094
|
+
throw new TypeError("Best Effort completion regressed its committed snapshot");
|
|
5095
|
+
saveTarget({
|
|
5096
|
+
...current,
|
|
5097
|
+
completedGeneration: current.generation,
|
|
5098
|
+
previousSnapshot: position,
|
|
5099
|
+
lease: void 0
|
|
5100
|
+
});
|
|
5101
|
+
return true;
|
|
5102
|
+
});
|
|
3642
5103
|
},
|
|
3643
5104
|
async release() {
|
|
3644
|
-
|
|
5105
|
+
transaction(() => {
|
|
5106
|
+
const current = readTarget(captured.id);
|
|
5107
|
+
if (current?.lease?.token === captured.lease?.token && current?.fence === captured.fence)
|
|
5108
|
+
saveTarget({ ...current, lease: void 0 });
|
|
5109
|
+
});
|
|
3645
5110
|
}
|
|
3646
5111
|
};
|
|
3647
|
-
}
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
5112
|
+
},
|
|
5113
|
+
close: () => connection.close()
|
|
5114
|
+
};
|
|
5115
|
+
} catch (error) {
|
|
5116
|
+
connection.close();
|
|
5117
|
+
throw error;
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
|
|
5121
|
+
// src/sqliteWakeQueue.ts
|
|
5122
|
+
async function createSqliteWakeQueue(options) {
|
|
5123
|
+
const authority = openReceiverAuthority(options.path);
|
|
5124
|
+
return { ...authority.queue, close: authority.close };
|
|
5125
|
+
}
|
|
5126
|
+
async function createSqliteWakeReceiver(options) {
|
|
5127
|
+
const authority = openReceiverAuthority(options.path);
|
|
5128
|
+
return {
|
|
5129
|
+
queue: authority.queue,
|
|
5130
|
+
inbox: authority.inbox,
|
|
5131
|
+
close: authority.close,
|
|
5132
|
+
pruneExpiredTokens: async (nowSeconds) => authority.pruneExpiredTokens(nowSeconds)
|
|
3654
5133
|
};
|
|
3655
5134
|
}
|
|
3656
5135
|
export {
|
|
@@ -3661,6 +5140,7 @@ export {
|
|
|
3661
5140
|
assertSameIntentBinding,
|
|
3662
5141
|
boundedPublisherOperation,
|
|
3663
5142
|
createAccountSourceWakeSigner,
|
|
5143
|
+
createBestEffortSnapshotWorker,
|
|
3664
5144
|
createBoundedPublisherEthersTransport,
|
|
3665
5145
|
createBoundedPublisherFetch,
|
|
3666
5146
|
createEthereumFinalizedPublisherSource,
|
|
@@ -3679,8 +5159,10 @@ export {
|
|
|
3679
5159
|
createPublisherHandshakeClient,
|
|
3680
5160
|
createPublisherHandshakeDiscovery,
|
|
3681
5161
|
createPublisherHttpsTransport,
|
|
5162
|
+
createSqliteBestEffortInbox,
|
|
3682
5163
|
createSqlitePublisherHandshakeStores,
|
|
3683
5164
|
createSqliteWakeQueue,
|
|
5165
|
+
createSqliteWakeReceiver,
|
|
3684
5166
|
createSubscriberHandshakeClient,
|
|
3685
5167
|
createSynchronizedEventKitHandshakeReader,
|
|
3686
5168
|
createUtcIntervalPublisherSource,
|
|
@@ -3688,6 +5170,8 @@ export {
|
|
|
3688
5170
|
handshakeMutationRequestFingerprint,
|
|
3689
5171
|
intentLookupKey,
|
|
3690
5172
|
isPublicPublisherAddress,
|
|
5173
|
+
migrateIdOnlyReducerCheckpoint,
|
|
5174
|
+
migrateReceiverStore,
|
|
3691
5175
|
openPublisherDataLake,
|
|
3692
5176
|
openPublisherRestGateway,
|
|
3693
5177
|
openSqlitePublisherState,
|