@xyo-network/event-kit-node 2.1.0 → 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 +12 -11
- package/dist/node/handshake.mjs.map +2 -2
- package/dist/node/index.d.ts +8 -1
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.mjs +1909 -609
- 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.mjs.map +2 -2
- 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;
|
|
153
|
-
}
|
|
154
|
-
await unlinkIfMissing(pendingPath);
|
|
155
|
-
const wake = await readJsonFile(inflightPath);
|
|
156
|
-
if (wake === void 0) {
|
|
157
|
-
await unlinkIfMissing(inflightPath);
|
|
158
|
-
continue;
|
|
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");
|
|
159
335
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
},
|
|
172
|
-
async pendingCount() {
|
|
173
|
-
await ensureDirs();
|
|
174
|
-
return (await readdir(pendingDir)).length;
|
|
175
342
|
},
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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;
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if (!isNotFound(error)) throw error;
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
await syncDirectory(directory);
|
|
230
411
|
} catch (error) {
|
|
231
|
-
if (!
|
|
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
|
}
|
|
@@ -1364,7 +1828,7 @@ async function publisherRotationRequestFingerprint(input) {
|
|
|
1364
1828
|
keyExpiresAtBlock: input.keyExpiresAtBlock ?? null,
|
|
1365
1829
|
maxPositionsPerWake: input.maxPositionsPerWake ?? null,
|
|
1366
1830
|
maxSubscriptionLifetimeBlocks: input.maxSubscriptionLifetimeBlocks ?? null,
|
|
1367
|
-
service:
|
|
1831
|
+
service: HashZod6.parse(input.service),
|
|
1368
1832
|
validFromBlock: input.validFromBlock ?? null
|
|
1369
1833
|
});
|
|
1370
1834
|
}
|
|
@@ -1396,28 +1860,28 @@ function protocolCandidate(reader, discovery, candidate, keys) {
|
|
|
1396
1860
|
finalizedHead: discovery.finalizedHead,
|
|
1397
1861
|
grant: {
|
|
1398
1862
|
body: candidate.tuple.grant.body,
|
|
1399
|
-
objectHash:
|
|
1863
|
+
objectHash: HashZod6.parse(candidate.tuple.grant.objectHash)
|
|
1400
1864
|
},
|
|
1401
1865
|
key: {
|
|
1402
1866
|
body: candidate.key.body,
|
|
1403
|
-
objectHash:
|
|
1867
|
+
objectHash: HashZod6.parse(candidate.key.objectHash)
|
|
1404
1868
|
},
|
|
1405
1869
|
keys,
|
|
1406
1870
|
publisher: reader.publisher,
|
|
1407
1871
|
service: {
|
|
1408
1872
|
body: candidate.tuple.service.body,
|
|
1409
|
-
objectHash:
|
|
1873
|
+
objectHash: HashZod6.parse(candidate.tuple.service.objectHash)
|
|
1410
1874
|
},
|
|
1411
|
-
subscriber:
|
|
1875
|
+
subscriber: NormalizedAddressZod4.parse(candidate.subscriber),
|
|
1412
1876
|
subscription: {
|
|
1413
1877
|
body: candidate.tuple.subscription.body,
|
|
1414
|
-
objectHash:
|
|
1878
|
+
objectHash: HashZod6.parse(candidate.tuple.subscription.objectHash)
|
|
1415
1879
|
},
|
|
1416
1880
|
tupleBlock: candidate.tuple.tupleEvent.block,
|
|
1417
1881
|
tupleObjectHashes: [
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1882
|
+
HashZod6.parse(candidate.tuple.service.objectHash),
|
|
1883
|
+
HashZod6.parse(candidate.tuple.grant.objectHash),
|
|
1884
|
+
HashZod6.parse(candidate.tuple.subscription.objectHash)
|
|
1421
1885
|
],
|
|
1422
1886
|
tupleSubject: reader.publisher
|
|
1423
1887
|
};
|
|
@@ -1442,7 +1906,7 @@ function createPublisherHandshakeClient(options) {
|
|
|
1442
1906
|
const current = await snapshot();
|
|
1443
1907
|
return await current.readModel.acceptanceCandidates({
|
|
1444
1908
|
...serviceQuery(options.reader, current, serviceId),
|
|
1445
|
-
...selector.subscriber === void 0 ? {} : { subscriber:
|
|
1909
|
+
...selector.subscriber === void 0 ? {} : { subscriber: NormalizedAddressZod4.parse(selector.subscriber) },
|
|
1446
1910
|
...selector.subscriptionId === void 0 ? {} : { subscriptionId: HandshakeIdZod3.parse(selector.subscriptionId) }
|
|
1447
1911
|
});
|
|
1448
1912
|
}
|
|
@@ -1450,8 +1914,8 @@ function createPublisherHandshakeClient(options) {
|
|
|
1450
1914
|
const found = await candidateDiscovery(selector);
|
|
1451
1915
|
if (!found.ok)
|
|
1452
1916
|
throw new EventKitHandshakeClientError("service-unavailable", `Publisher service is unavailable: ${found.reason}`);
|
|
1453
|
-
const subscriber =
|
|
1454
|
-
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);
|
|
1455
1919
|
const candidates = found.candidates.filter((candidate2) => candidate2.subscriber === subscriber && (subscription === void 0 || candidate2.tuple.subscription.objectHash === subscription));
|
|
1456
1920
|
if (candidates.length === 0) {
|
|
1457
1921
|
throw new EventKitHandshakeClientError("candidate-not-found", "No exact finalized acceptance candidate was found");
|
|
@@ -1564,7 +2028,7 @@ function createPublisherHandshakeClient(options) {
|
|
|
1564
2028
|
const reconciliation = await reconcileAcceptance(acceptanceHash, publication, intent.challenge.subscriber, intent.challenge.subscription);
|
|
1565
2029
|
return Object.freeze({
|
|
1566
2030
|
acceptance: Object.freeze({ body: intent.acceptance, objectHash: acceptanceHash }),
|
|
1567
|
-
challenge: intent.
|
|
2031
|
+
challenge: intent.acceptance.challenge,
|
|
1568
2032
|
intentDisposition,
|
|
1569
2033
|
operationId: intent.operationId,
|
|
1570
2034
|
publication,
|
|
@@ -1607,14 +2071,14 @@ function createPublisherHandshakeClient(options) {
|
|
|
1607
2071
|
const found = await candidateDiscovery(selector);
|
|
1608
2072
|
if (!found.ok || selector.subscription === void 0)
|
|
1609
2073
|
return found;
|
|
1610
|
-
const subscription =
|
|
2074
|
+
const subscription = HashZod6.parse(selector.subscription);
|
|
1611
2075
|
return Object.freeze({
|
|
1612
2076
|
...found,
|
|
1613
2077
|
candidates: found.candidates.filter((candidate) => candidate.tuple.subscription.objectHash === subscription)
|
|
1614
2078
|
});
|
|
1615
2079
|
},
|
|
1616
2080
|
async beginChallenge(input) {
|
|
1617
|
-
const challengeHash =
|
|
2081
|
+
const challengeHash = HashZod6.parse(input.challengeHash);
|
|
1618
2082
|
const record = await options.challenges.get(challengeHash);
|
|
1619
2083
|
if (record === void 0)
|
|
1620
2084
|
return { kind: "missing" };
|
|
@@ -1644,9 +2108,9 @@ function createPublisherHandshakeClient(options) {
|
|
|
1644
2108
|
},
|
|
1645
2109
|
async completeChallengeAndAccept(input) {
|
|
1646
2110
|
await assertPublicationSource(options.publication, options.reader.publisher);
|
|
1647
|
-
const challengeHash =
|
|
1648
|
-
const subscriber =
|
|
1649
|
-
const subscription =
|
|
2111
|
+
const challengeHash = HashZod6.parse(input.challengeHash);
|
|
2112
|
+
const subscriber = NormalizedAddressZod4.parse(input.subscriber);
|
|
2113
|
+
const subscription = HashZod6.parse(input.subscription);
|
|
1650
2114
|
const record = await options.challenges.get(challengeHash);
|
|
1651
2115
|
if (record === void 0) {
|
|
1652
2116
|
throw new EventKitHandshakeClientError("challenge-not-found", "Endpoint challenge was not found");
|
|
@@ -1781,7 +2245,7 @@ function createPublisherHandshakeClient(options) {
|
|
|
1781
2245
|
},
|
|
1782
2246
|
async retireEncryptionKey(key) {
|
|
1783
2247
|
await assertPublicationSource(options.publication, options.reader.publisher);
|
|
1784
|
-
const keyHash =
|
|
2248
|
+
const keyHash = HashZod6.parse(key);
|
|
1785
2249
|
const requestFingerprint = await exactMutationRequestFingerprint({ key: keyHash });
|
|
1786
2250
|
let resolvedIntent = await loadMutationIntent(options.intents, options.reader, "publisher-encryption-key-retire", keyHash, options.reader.publisher, requestFingerprint);
|
|
1787
2251
|
let intentDisposition = "resumed";
|
|
@@ -1819,7 +2283,7 @@ function createPublisherHandshakeClient(options) {
|
|
|
1819
2283
|
},
|
|
1820
2284
|
async retireService(expectedService) {
|
|
1821
2285
|
await assertPublicationSource(options.publication, options.reader.publisher);
|
|
1822
|
-
const service =
|
|
2286
|
+
const service = HashZod6.parse(expectedService);
|
|
1823
2287
|
const requestFingerprint = await exactMutationRequestFingerprint({ service });
|
|
1824
2288
|
let resolvedIntent = await loadMutationIntent(options.intents, options.reader, "publisher-service-retire", service, options.reader.publisher, requestFingerprint);
|
|
1825
2289
|
let intentDisposition = "resumed";
|
|
@@ -1875,7 +2339,7 @@ function createPublisherHandshakeClient(options) {
|
|
|
1875
2339
|
preferredEncryptionKey: generated.descriptorHash,
|
|
1876
2340
|
revision: selected.service.body.revision + 1,
|
|
1877
2341
|
serviceId: selected.service.body.serviceId,
|
|
1878
|
-
supersedes:
|
|
2342
|
+
supersedes: HashZod6.parse(selected.service.objectHash),
|
|
1879
2343
|
validFromBlock
|
|
1880
2344
|
};
|
|
1881
2345
|
const service = createPublisherService({
|
|
@@ -1894,8 +2358,8 @@ function createPublisherHandshakeClient(options) {
|
|
|
1894
2358
|
privateKey: generated.privateKey,
|
|
1895
2359
|
publisher: options.reader.publisher,
|
|
1896
2360
|
requestFingerprint,
|
|
1897
|
-
selectedKey:
|
|
1898
|
-
selectedService:
|
|
2361
|
+
selectedKey: HashZod6.parse(selected.key.objectHash),
|
|
2362
|
+
selectedService: HashZod6.parse(selected.service.objectHash),
|
|
1899
2363
|
service,
|
|
1900
2364
|
source: options.reader.publisher,
|
|
1901
2365
|
version: 1
|
|
@@ -1922,7 +2386,7 @@ function tupleHashes(view) {
|
|
|
1922
2386
|
}
|
|
1923
2387
|
function createSubscriberHandshakeClient(options) {
|
|
1924
2388
|
assertPublicationIdentity(options.publication, options.reader);
|
|
1925
|
-
const subscriber =
|
|
2389
|
+
const subscriber = NormalizedAddressZod4.parse(options.subscriber);
|
|
1926
2390
|
const serviceId = options.serviceId === void 0 ? void 0 : HandshakeIdZod3.parse(options.serviceId);
|
|
1927
2391
|
async function snapshot() {
|
|
1928
2392
|
return readySnapshot(await options.reader.sync());
|
|
@@ -2002,7 +2466,7 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2002
2466
|
const client = {
|
|
2003
2467
|
async revokeGrant(grant) {
|
|
2004
2468
|
await assertPublicationSource(options.publication, subscriber);
|
|
2005
|
-
const grantHash =
|
|
2469
|
+
const grantHash = HashZod6.parse(grant);
|
|
2006
2470
|
const requestFingerprint = await exactMutationRequestFingerprint({ grant: grantHash });
|
|
2007
2471
|
let resolvedIntent = await loadMutationIntent(options.intents, options.reader, "subscriber-grant-revoke", grantHash, subscriber, requestFingerprint);
|
|
2008
2472
|
let intentDisposition = "resumed";
|
|
@@ -2041,7 +2505,7 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2041
2505
|
},
|
|
2042
2506
|
async revokeSubscription(subscription) {
|
|
2043
2507
|
await assertPublicationSource(options.publication, subscriber);
|
|
2044
|
-
const subscriptionHash =
|
|
2508
|
+
const subscriptionHash = HashZod6.parse(subscription);
|
|
2045
2509
|
const requestFingerprint = await exactMutationRequestFingerprint({ subscription: subscriptionHash });
|
|
2046
2510
|
let resolvedIntent = await loadMutationIntent(options.intents, options.reader, "subscriber-subscription-revoke", subscriptionHash, subscriber, requestFingerprint);
|
|
2047
2511
|
let intentDisposition = "resumed";
|
|
@@ -2083,7 +2547,7 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2083
2547
|
return await current.readModel.discoverPublisherService(serviceQuery(options.reader, current, serviceId));
|
|
2084
2548
|
},
|
|
2085
2549
|
async subscription(subscription) {
|
|
2086
|
-
const subscriptionHash =
|
|
2550
|
+
const subscriptionHash = HashZod6.parse(subscription);
|
|
2087
2551
|
const current = await options.reader.sync();
|
|
2088
2552
|
const view = await lookupAt(current);
|
|
2089
2553
|
if (!view.ok)
|
|
@@ -2141,12 +2605,12 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2141
2605
|
grant: grantHash,
|
|
2142
2606
|
publisher: options.reader.publisher,
|
|
2143
2607
|
publisherKey: {
|
|
2144
|
-
encryptionKey:
|
|
2608
|
+
encryptionKey: HashZod6.parse(selected.key.objectHash),
|
|
2145
2609
|
keyId: selected.key.body.keyId,
|
|
2146
2610
|
publicKey: selected.key.body.publicKey
|
|
2147
2611
|
},
|
|
2148
2612
|
...input.revision === void 0 ? {} : { revision: input.revision },
|
|
2149
|
-
service:
|
|
2613
|
+
service: HashZod6.parse(selected.service.objectHash),
|
|
2150
2614
|
subscriber,
|
|
2151
2615
|
...input.subscriptionId === void 0 ? {} : { subscriptionId: input.subscriptionId },
|
|
2152
2616
|
...input.supersedes === void 0 ? {} : { supersedes: input.supersedes }
|
|
@@ -2160,8 +2624,8 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2160
2624
|
operationId,
|
|
2161
2625
|
publisher: options.reader.publisher,
|
|
2162
2626
|
requestFingerprint,
|
|
2163
|
-
selectedKey:
|
|
2164
|
-
selectedService:
|
|
2627
|
+
selectedKey: HashZod6.parse(selected.key.objectHash),
|
|
2628
|
+
selectedService: HashZod6.parse(selected.service.objectHash),
|
|
2165
2629
|
service: selected.service.body,
|
|
2166
2630
|
source: subscriber,
|
|
2167
2631
|
subscription,
|
|
@@ -2174,7 +2638,7 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2174
2638
|
return Object.freeze(client);
|
|
2175
2639
|
}
|
|
2176
2640
|
function subscriberPlaintext(service, input, maxPositionsPerWake) {
|
|
2177
|
-
if (input.deliveryMode !== service.deliveryMode ||
|
|
2641
|
+
if (input.deliveryMode !== service.deliveryMode || canonicalJson2(input.bestEffort ?? null) !== canonicalJson2(service.bestEffort ?? null))
|
|
2178
2642
|
throw new Error("Requested delivery terms do not match exact selected service");
|
|
2179
2643
|
const plaintextInput = {
|
|
2180
2644
|
audience: input.audience,
|
|
@@ -2184,7 +2648,7 @@ function subscriberPlaintext(service, input, maxPositionsPerWake) {
|
|
|
2184
2648
|
queueId: input.queueId
|
|
2185
2649
|
};
|
|
2186
2650
|
const source = input.source;
|
|
2187
|
-
if (service.events.every((event) =>
|
|
2651
|
+
if (service.events.every((event) => canonicalJson2(event.source) !== canonicalJson2(source))) {
|
|
2188
2652
|
throw new Error("Requested source is not advertised by the selected service");
|
|
2189
2653
|
}
|
|
2190
2654
|
return createWakeSubscriptionPlaintext({
|
|
@@ -2197,8 +2661,544 @@ function subscriberPlaintext(service, input, maxPositionsPerWake) {
|
|
|
2197
2661
|
});
|
|
2198
2662
|
}
|
|
2199
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
|
+
};
|
|
3118
|
+
}
|
|
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 }
|
|
3160
|
+
};
|
|
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 });
|
|
3180
|
+
}
|
|
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 };
|
|
3198
|
+
}
|
|
3199
|
+
|
|
2200
3200
|
// src/runtime/boundedFetch.ts
|
|
2201
|
-
import { Buffer as
|
|
3201
|
+
import { Buffer as Buffer3 } from "node:buffer";
|
|
2202
3202
|
|
|
2203
3203
|
// src/runtime/abort.ts
|
|
2204
3204
|
import { clearTimeout, setTimeout } from "node:timers";
|
|
@@ -2240,13 +3240,13 @@ function createBoundedPublisherFetch(options = {}) {
|
|
|
2240
3240
|
...init?.signal == null ? [] : [init.signal],
|
|
2241
3241
|
...input instanceof Request ? [input.signal] : []
|
|
2242
3242
|
];
|
|
2243
|
-
const
|
|
3243
|
+
const scope2 = publisherAbortScope(signals, timeout);
|
|
2244
3244
|
try {
|
|
2245
|
-
|
|
3245
|
+
scope2.signal.throwIfAborted();
|
|
2246
3246
|
const response = await nativeFetch(input, {
|
|
2247
3247
|
...init,
|
|
2248
3248
|
redirect: "error",
|
|
2249
|
-
signal:
|
|
3249
|
+
signal: scope2.signal
|
|
2250
3250
|
});
|
|
2251
3251
|
if (response.body === null)
|
|
2252
3252
|
return response;
|
|
@@ -2274,13 +3274,13 @@ function createBoundedPublisherFetch(options = {}) {
|
|
|
2274
3274
|
headers.delete("content-encoding");
|
|
2275
3275
|
headers.delete("transfer-encoding");
|
|
2276
3276
|
headers.set("content-length", String(size));
|
|
2277
|
-
return new Response(
|
|
3277
|
+
return new Response(Buffer3.concat(chunks), {
|
|
2278
3278
|
status: response.status,
|
|
2279
3279
|
statusText: response.statusText,
|
|
2280
3280
|
headers
|
|
2281
3281
|
});
|
|
2282
3282
|
} finally {
|
|
2283
|
-
|
|
3283
|
+
scope2.dispose();
|
|
2284
3284
|
}
|
|
2285
3285
|
})();
|
|
2286
3286
|
active.add(operation);
|
|
@@ -2305,7 +3305,7 @@ function createBoundedPublisherEthersTransport(options = {}) {
|
|
|
2305
3305
|
cancellation?.addListener(() => controller.abort());
|
|
2306
3306
|
cancellation?.checkSignal();
|
|
2307
3307
|
const response = await bounded.fetchFn(request2.url, {
|
|
2308
|
-
body: request2.body === null ? void 0 :
|
|
3308
|
+
body: request2.body === null ? void 0 : Buffer3.from(request2.body),
|
|
2309
3309
|
headers: request2.headers,
|
|
2310
3310
|
method: request2.method,
|
|
2311
3311
|
signal: controller.signal
|
|
@@ -2525,12 +3525,12 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2525
3525
|
},
|
|
2526
3526
|
async acceptPending(signal) {
|
|
2527
3527
|
if (accepting === void 0) {
|
|
2528
|
-
const
|
|
3528
|
+
const scope2 = publisherAbortScope([owner.signal, signal]);
|
|
2529
3529
|
accepting = (async () => {
|
|
2530
3530
|
try {
|
|
2531
|
-
await accept(
|
|
3531
|
+
await accept(scope2.signal);
|
|
2532
3532
|
} finally {
|
|
2533
|
-
|
|
3533
|
+
scope2.dispose();
|
|
2534
3534
|
accepting = void 0;
|
|
2535
3535
|
}
|
|
2536
3536
|
})();
|
|
@@ -2545,25 +3545,25 @@ function createPublisherHandshakeDiscovery(options) {
|
|
|
2545
3545
|
}
|
|
2546
3546
|
|
|
2547
3547
|
// src/runtime/httpsTransport.ts
|
|
2548
|
-
import { Buffer as
|
|
3548
|
+
import { Buffer as Buffer4 } from "node:buffer";
|
|
2549
3549
|
import { lookup } from "node:dns/promises";
|
|
2550
3550
|
import { request } from "node:https";
|
|
2551
3551
|
import { BlockList, isIP } from "node:net";
|
|
2552
|
-
import { z as
|
|
3552
|
+
import { z as z4 } from "zod";
|
|
2553
3553
|
|
|
2554
3554
|
// src/runtime/sources.ts
|
|
2555
|
-
import { createHash as
|
|
2556
|
-
import { canonicalJson as
|
|
2557
|
-
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";
|
|
2558
3558
|
import { PayloadBuilder } from "@xyo-network/sdk";
|
|
2559
3559
|
import { asXL1BlockNumber } from "@xyo-network/xl1-sdk";
|
|
2560
3560
|
import { FetchRequest, JsonRpcProvider } from "ethers";
|
|
2561
3561
|
function publisherSourceKey(source) {
|
|
2562
|
-
return
|
|
3562
|
+
return createHash4("sha256").update(canonicalJson3(WakeSourceZod2.parse(source))).digest("hex");
|
|
2563
3563
|
}
|
|
2564
3564
|
async function boundedPublisherOperation(operation, signal, timeoutMs = 1e4) {
|
|
2565
|
-
const
|
|
2566
|
-
const combined =
|
|
3565
|
+
const scope2 = publisherAbortScope([signal], timeoutMs);
|
|
3566
|
+
const combined = scope2.signal;
|
|
2567
3567
|
let rejectAbort;
|
|
2568
3568
|
const aborted = new Promise((_resolve, reject) => {
|
|
2569
3569
|
rejectAbort = () => reject(new Error("Publisher operation aborted or timed out"));
|
|
@@ -2575,11 +3575,11 @@ async function boundedPublisherOperation(operation, signal, timeoutMs = 1e4) {
|
|
|
2575
3575
|
} finally {
|
|
2576
3576
|
if (rejectAbort !== void 0)
|
|
2577
3577
|
combined.removeEventListener("abort", rejectAbort);
|
|
2578
|
-
|
|
3578
|
+
scope2.dispose();
|
|
2579
3579
|
}
|
|
2580
3580
|
}
|
|
2581
3581
|
function createXl1PublisherSource(options) {
|
|
2582
|
-
const source =
|
|
3582
|
+
const source = WakeSourceZod2.parse(options.source);
|
|
2583
3583
|
if (source.kind !== "xl1-finalized")
|
|
2584
3584
|
throw new Error("Expected an XL1 finalized source");
|
|
2585
3585
|
return {
|
|
@@ -2603,7 +3603,7 @@ function createXl1PublisherSource(options) {
|
|
|
2603
3603
|
};
|
|
2604
3604
|
}
|
|
2605
3605
|
function createEthereumFinalizedPublisherSource(options) {
|
|
2606
|
-
const source =
|
|
3606
|
+
const source = WakeSourceZod2.parse(options.source);
|
|
2607
3607
|
if (source.kind !== "ethereum-finalized")
|
|
2608
3608
|
throw new Error("Expected an Ethereum finalized source");
|
|
2609
3609
|
const { chainId, genesisHash } = source;
|
|
@@ -2669,7 +3669,7 @@ function createEthereumFinalizedPublisherSource(options) {
|
|
|
2669
3669
|
};
|
|
2670
3670
|
}
|
|
2671
3671
|
function createUtcIntervalPublisherSource(options) {
|
|
2672
|
-
const source =
|
|
3672
|
+
const source = WakeSourceZod2.parse(options.source);
|
|
2673
3673
|
if (source.kind !== "utc-interval")
|
|
2674
3674
|
throw new Error("Expected a UTC interval source");
|
|
2675
3675
|
const nowSeconds = options.nowSeconds ?? (() => Math.floor(Date.now() / 1e3));
|
|
@@ -2686,14 +3686,14 @@ function createUtcIntervalPublisherSource(options) {
|
|
|
2686
3686
|
}
|
|
2687
3687
|
|
|
2688
3688
|
// src/runtime/httpsTransport.ts
|
|
2689
|
-
var OptionsZod =
|
|
2690
|
-
allowedEndpoints:
|
|
2691
|
-
endpointHeaders:
|
|
2692
|
-
endpoint:
|
|
2693
|
-
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) })
|
|
2694
3694
|
})).max(64).default([]),
|
|
2695
|
-
maxResponseBytes:
|
|
2696
|
-
requestTimeoutMs:
|
|
3695
|
+
maxResponseBytes: z4.number().int().min(1).max(1048576).default(65536),
|
|
3696
|
+
requestTimeoutMs: z4.number().int().min(100).max(6e4).default(1e4)
|
|
2697
3697
|
});
|
|
2698
3698
|
var NON_PUBLIC_IPV4_SUBNETS = [
|
|
2699
3699
|
["0.0.0.0", 8],
|
|
@@ -2773,8 +3773,8 @@ function createPublisherHttpsTransport(input) {
|
|
|
2773
3773
|
assertAllowed(endpoint);
|
|
2774
3774
|
signal.throwIfAborted();
|
|
2775
3775
|
const url = new URL(endpoint);
|
|
2776
|
-
const
|
|
2777
|
-
const combined =
|
|
3776
|
+
const scope2 = publisherAbortScope([signal], options.requestTimeoutMs);
|
|
3777
|
+
const combined = scope2.signal;
|
|
2778
3778
|
try {
|
|
2779
3779
|
const addresses = await boundedPublisherOperation(lookup(url.hostname.replaceAll(/^\[|\]$/gu, ""), { all: true, verbatim: true }), combined, options.requestTimeoutMs);
|
|
2780
3780
|
combined.throwIfAborted();
|
|
@@ -2783,7 +3783,7 @@ function createPublisherHttpsTransport(input) {
|
|
|
2783
3783
|
const selected = addresses[0];
|
|
2784
3784
|
if (selected === void 0)
|
|
2785
3785
|
throw new Error("Publisher DNS lookup was empty");
|
|
2786
|
-
const encoded =
|
|
3786
|
+
const encoded = Buffer4.from(JSON.stringify(body));
|
|
2787
3787
|
if (encoded.byteLength > 65536)
|
|
2788
3788
|
throw new Error("Publisher request body is too large");
|
|
2789
3789
|
return await new Promise((resolve, reject) => {
|
|
@@ -2824,7 +3824,7 @@ function createPublisherHttpsTransport(input) {
|
|
|
2824
3824
|
response.on("error", () => reject(new Error("Publisher HTTPS response failed")));
|
|
2825
3825
|
response.on("end", () => {
|
|
2826
3826
|
try {
|
|
2827
|
-
const responseBody = size === 0 ? null : JSON.parse(
|
|
3827
|
+
const responseBody = size === 0 ? null : JSON.parse(Buffer4.concat(chunks).toString("utf8"));
|
|
2828
3828
|
const retry = Number(response.headers["retry-after"]);
|
|
2829
3829
|
resolve({
|
|
2830
3830
|
status,
|
|
@@ -2840,7 +3840,7 @@ function createPublisherHttpsTransport(input) {
|
|
|
2840
3840
|
call.end(encoded);
|
|
2841
3841
|
});
|
|
2842
3842
|
} finally {
|
|
2843
|
-
|
|
3843
|
+
scope2.dispose();
|
|
2844
3844
|
}
|
|
2845
3845
|
}
|
|
2846
3846
|
};
|
|
@@ -2852,25 +3852,25 @@ import { createMemoryLocator, PeriodicActor } from "@ariestools/actor";
|
|
|
2852
3852
|
import { creatable } from "@ariestools/sdk";
|
|
2853
3853
|
import { BestEffortAdmissionAcknowledgmentZod } from "@xyo-network/event-kit-protocol";
|
|
2854
3854
|
import { BestEffortDeliveryPolicyZod, WakeDeliveryModeZod } from "@xyo-network/event-kit-schemas";
|
|
2855
|
-
import { z as
|
|
3855
|
+
import { z as z5 } from "zod";
|
|
2856
3856
|
|
|
2857
3857
|
// src/runtime/publisherDeliveryState.ts
|
|
2858
|
-
import { randomBytes as
|
|
3858
|
+
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
2859
3859
|
import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
|
|
2860
|
-
import { WakeEnvelopeSchema, WakeEnvelopeZod } from "@xyo-network/event-kit-schemas";
|
|
3860
|
+
import { WakeEnvelopeSchema as WakeEnvelopeSchema2, WakeEnvelopeZod as WakeEnvelopeZod2 } from "@xyo-network/event-kit-schemas";
|
|
2861
3861
|
function assertDeliveryState(record, subscription) {
|
|
2862
3862
|
if (!isDeepStrictEqual3(record.subscription, subscription))
|
|
2863
3863
|
throw new Error("Subscription revision changed without a new identity");
|
|
2864
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))
|
|
2865
3865
|
throw new Error("Persisted publisher delivery state is invalid");
|
|
2866
3866
|
if (record.pending === void 0) return;
|
|
2867
|
-
const pending =
|
|
3867
|
+
const pending = WakeEnvelopeZod2.parse(record.pending);
|
|
2868
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))
|
|
2869
3869
|
throw new Error("Persisted publisher outbox binding is invalid");
|
|
2870
3870
|
}
|
|
2871
|
-
function makeEvent(subscription, fromPosition, toPosition, now, wakeId =
|
|
2872
|
-
return
|
|
2873
|
-
schema:
|
|
3871
|
+
function makeEvent(subscription, fromPosition, toPosition, now, wakeId = randomBytes6(16).toString("hex")) {
|
|
3872
|
+
return WakeEnvelopeZod2.parse({
|
|
3873
|
+
schema: WakeEnvelopeSchema2,
|
|
2874
3874
|
authorizationChainId: subscription.authorizationChainId,
|
|
2875
3875
|
deliveryMode: subscription.deliveryMode,
|
|
2876
3876
|
deploymentId: subscription.deploymentId,
|
|
@@ -2965,27 +3965,27 @@ function failedDelivery(record, current, mode, error, now) {
|
|
|
2965
3965
|
}
|
|
2966
3966
|
|
|
2967
3967
|
// src/runtime/sourceSigner.ts
|
|
2968
|
-
import { randomBytes as
|
|
3968
|
+
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
2969
3969
|
import {
|
|
2970
|
-
NormalizedAddressZod as
|
|
2971
|
-
WakeEnvelopeZod as
|
|
3970
|
+
NormalizedAddressZod as NormalizedAddressZod7,
|
|
3971
|
+
WakeEnvelopeZod as WakeEnvelopeZod3,
|
|
2972
3972
|
WakeJwtSchema
|
|
2973
3973
|
} from "@xyo-network/event-kit-schemas";
|
|
2974
3974
|
import { PayloadBuilder as PayloadBuilder2 } from "@xyo-network/sdk";
|
|
2975
3975
|
function createAccountSourceWakeSigner(options) {
|
|
2976
|
-
const publisher =
|
|
3976
|
+
const publisher = NormalizedAddressZod7.parse(options.publisher);
|
|
2977
3977
|
if (String(options.account.address).replace(/^0x/u, "").toLowerCase() !== publisher)
|
|
2978
3978
|
throw new Error("Publisher signing account mismatch");
|
|
2979
3979
|
return {
|
|
2980
3980
|
async sign(input, audience) {
|
|
2981
|
-
const event =
|
|
3981
|
+
const event = WakeEnvelopeZod3.parse(input);
|
|
2982
3982
|
const eventHash = await PayloadBuilder2.dataHash(event);
|
|
2983
3983
|
const now = options.nowSeconds?.() ?? Math.floor(Date.now() / 1e3);
|
|
2984
3984
|
if (!Number.isSafeInteger(now) || now < 0)
|
|
2985
3985
|
throw new Error("Publisher signing clock is invalid");
|
|
2986
3986
|
const { token: jwt } = await options.account.signJwt({
|
|
2987
3987
|
audience,
|
|
2988
|
-
claims: { eventHash, jti:
|
|
3988
|
+
claims: { eventHash, jti: randomBytes7(16).toString("hex") },
|
|
2989
3989
|
now,
|
|
2990
3990
|
schema: WakeJwtSchema,
|
|
2991
3991
|
ttl: 60
|
|
@@ -3012,11 +4012,11 @@ var PublisherRuntimeActor = class extends PeriodicActor {
|
|
|
3012
4012
|
PublisherRuntimeActor = __decorateClass([
|
|
3013
4013
|
creatable()
|
|
3014
4014
|
], PublisherRuntimeActor);
|
|
3015
|
-
var TimingZod =
|
|
3016
|
-
concurrency:
|
|
3017
|
-
intervalMs:
|
|
3018
|
-
maxSubscriptions:
|
|
3019
|
-
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)
|
|
3020
4020
|
});
|
|
3021
4021
|
function runtimeSettings(options) {
|
|
3022
4022
|
const deliveryMode = WakeDeliveryModeZod.parse(options.deliveryMode);
|
|
@@ -3176,7 +4176,7 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3176
4176
|
acknowledgeBestEffort(record, response.body, signed.eventHash, signal, now);
|
|
3177
4177
|
return;
|
|
3178
4178
|
}
|
|
3179
|
-
const receipt =
|
|
4179
|
+
const receipt = z5.object({ receiptId: z5.string().min(1), wakeId: z5.literal(pending.wakeId) }).safeParse(response.body);
|
|
3180
4180
|
if (!receipt.success) throw new Error("Publisher admission did not return a matching durable receipt");
|
|
3181
4181
|
signal.throwIfAborted();
|
|
3182
4182
|
state.transaction(() => {
|
|
@@ -3258,8 +4258,8 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3258
4258
|
return available;
|
|
3259
4259
|
}
|
|
3260
4260
|
async function tick(actorSignal) {
|
|
3261
|
-
const
|
|
3262
|
-
const signal =
|
|
4261
|
+
const scope2 = publisherAbortScope([abort.signal, actorSignal]);
|
|
4262
|
+
const signal = scope2.signal;
|
|
3263
4263
|
try {
|
|
3264
4264
|
signal.throwIfAborted();
|
|
3265
4265
|
const now = clock();
|
|
@@ -3290,7 +4290,7 @@ async function createPersistentEventPublisherRuntime(options) {
|
|
|
3290
4290
|
if (!signal.aborted)
|
|
3291
4291
|
throw error;
|
|
3292
4292
|
} finally {
|
|
3293
|
-
|
|
4293
|
+
scope2.dispose();
|
|
3294
4294
|
}
|
|
3295
4295
|
}
|
|
3296
4296
|
const actor = await PublisherRuntimeActor.create({
|
|
@@ -3464,15 +4464,15 @@ import {
|
|
|
3464
4464
|
Base64Url32BytesZod as Base64Url32BytesZod3,
|
|
3465
4465
|
EndpointChallengeZod as EndpointChallengeZod3,
|
|
3466
4466
|
HandshakeIdZod as HandshakeIdZod4,
|
|
3467
|
-
HashZod as
|
|
3468
|
-
NormalizedAddressZod as
|
|
4467
|
+
HashZod as HashZod9,
|
|
4468
|
+
NormalizedAddressZod as NormalizedAddressZod8
|
|
3469
4469
|
} from "@xyo-network/event-kit-schemas";
|
|
3470
|
-
import { z as
|
|
3471
|
-
var KeyZod =
|
|
3472
|
-
encryptionKey:
|
|
4470
|
+
import { z as z6 } from "zod";
|
|
4471
|
+
var KeyZod = z6.strictObject({
|
|
4472
|
+
encryptionKey: HashZod9,
|
|
3473
4473
|
keyId: HandshakeIdZod4,
|
|
3474
4474
|
privateKey: Base64Url32BytesZod3,
|
|
3475
|
-
publisher:
|
|
4475
|
+
publisher: NormalizedAddressZod8
|
|
3476
4476
|
});
|
|
3477
4477
|
var phases = ["signed", "content-written", "content-verified", "broadcast", "included", "finalized"];
|
|
3478
4478
|
function requireTime2(value) {
|
|
@@ -3482,13 +4482,13 @@ function samePublicationIntent(left, right) {
|
|
|
3482
4482
|
return left.chainId === right.chainId && left.source === right.source && left.operationId === right.operationId && isDeepStrictEqual5(left.eventHashes, right.eventHashes) && isDeepStrictEqual5(left.attachmentHashes, right.attachmentHashes);
|
|
3483
4483
|
}
|
|
3484
4484
|
function publicationKey(intent) {
|
|
3485
|
-
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)) {
|
|
3486
4486
|
throw new Error("Invalid publication intent");
|
|
3487
4487
|
}
|
|
3488
4488
|
return JSON.stringify([intent.chainId, intent.source, intent.operationId]);
|
|
3489
4489
|
}
|
|
3490
4490
|
function createSqlitePublisherHandshakeStores(state, options = {}) {
|
|
3491
|
-
const replayKey = options.replayNamespace === void 0 ? "checkpoint" : `checkpoint:${
|
|
4491
|
+
const replayKey = options.replayNamespace === void 0 ? "checkpoint" : `checkpoint:${z6.string().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/u).parse(options.replayNamespace)}`;
|
|
3492
4492
|
const replay = {
|
|
3493
4493
|
load: async () => state.get("replay", replayKey),
|
|
3494
4494
|
async save(checkpoint) {
|
|
@@ -3516,11 +4516,11 @@ function createSqlitePublisherHandshakeStores(state, options = {}) {
|
|
|
3516
4516
|
};
|
|
3517
4517
|
const keys = {
|
|
3518
4518
|
async getPrivateKey(input) {
|
|
3519
|
-
const key = `${
|
|
4519
|
+
const key = `${NormalizedAddressZod8.parse(input.publisher)}:${HandshakeIdZod4.parse(input.keyId)}`;
|
|
3520
4520
|
const raw = state.get("encryption-keys", key);
|
|
3521
4521
|
if (raw === void 0) return;
|
|
3522
4522
|
const record = KeyZod.parse(raw);
|
|
3523
|
-
if (record.encryptionKey !==
|
|
4523
|
+
if (record.encryptionKey !== HashZod9.parse(input.encryptionKey)) throw new Error("Encryption key identity mismatch");
|
|
3524
4524
|
const { publisher: _publisher, ...privateKey } = record;
|
|
3525
4525
|
return privateKey;
|
|
3526
4526
|
},
|
|
@@ -3538,13 +4538,13 @@ function createSqlitePublisherHandshakeStores(state, options = {}) {
|
|
|
3538
4538
|
});
|
|
3539
4539
|
}
|
|
3540
4540
|
};
|
|
3541
|
-
const current = (hash) => state.get("challenges",
|
|
4541
|
+
const current = (hash) => state.get("challenges", HashZod9.parse(hash));
|
|
3542
4542
|
const live = (record, now) => record.challenge.issuedAt <= now && now < record.challenge.expiresAt;
|
|
3543
4543
|
const challenges = {
|
|
3544
4544
|
get: async (hash) => current(hash),
|
|
3545
4545
|
async issue(input) {
|
|
3546
4546
|
const challenge = EndpointChallengeZod3.parse(input.challenge);
|
|
3547
|
-
const hash =
|
|
4547
|
+
const hash = HashZod9.parse(input.challengeHash);
|
|
3548
4548
|
if (await endpointChallengeHash2(challenge) !== hash) return { kind: "collision" };
|
|
3549
4549
|
return state.transaction(() => {
|
|
3550
4550
|
const priorNonce = state.get("challenge-nonces", challenge.nonce);
|
|
@@ -3576,7 +4576,7 @@ function createSqlitePublisherHandshakeStores(state, options = {}) {
|
|
|
3576
4576
|
},
|
|
3577
4577
|
async completeVerified(input) {
|
|
3578
4578
|
requireTime2(input.nowSeconds);
|
|
3579
|
-
const responseHash =
|
|
4579
|
+
const responseHash = HashZod9.parse(input.responseHash);
|
|
3580
4580
|
return state.transaction(() => {
|
|
3581
4581
|
const record = current(input.challengeHash);
|
|
3582
4582
|
if (record === void 0) return { kind: "missing" };
|
|
@@ -3612,7 +4612,7 @@ function createSqlitePublisherHandshakeStores(state, options = {}) {
|
|
|
3612
4612
|
},
|
|
3613
4613
|
async save(record) {
|
|
3614
4614
|
const key = publicationKey(record);
|
|
3615
|
-
|
|
4615
|
+
HashZod9.parse(record.evidence.transactionHash);
|
|
3616
4616
|
if (!phases.includes(record.phase)) throw new Error("Unknown publication phase");
|
|
3617
4617
|
state.transaction(() => {
|
|
3618
4618
|
const signed = state.get("publication:signed", key);
|
|
@@ -3637,15 +4637,15 @@ function createSqlitePublisherHandshakeStores(state, options = {}) {
|
|
|
3637
4637
|
}
|
|
3638
4638
|
|
|
3639
4639
|
// src/runtime/sqlitePublisherState.ts
|
|
3640
|
-
import { randomBytes as
|
|
3641
|
-
import { constants } from "node:fs";
|
|
4640
|
+
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
4641
|
+
import { constants as constants3 } from "node:fs";
|
|
3642
4642
|
import {
|
|
3643
|
-
lstat,
|
|
3644
|
-
mkdir as
|
|
3645
|
-
open as
|
|
4643
|
+
lstat as lstat5,
|
|
4644
|
+
mkdir as mkdir4,
|
|
4645
|
+
open as open6
|
|
3646
4646
|
} from "node:fs/promises";
|
|
3647
4647
|
import { hostname } from "node:os";
|
|
3648
|
-
import
|
|
4648
|
+
import PATH16 from "node:path";
|
|
3649
4649
|
function decode(value) {
|
|
3650
4650
|
return JSON.parse(value, (_key, candidate) => {
|
|
3651
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;
|
|
@@ -3666,13 +4666,13 @@ function processExists(pid) {
|
|
|
3666
4666
|
async function openSqlitePublisherState(options) {
|
|
3667
4667
|
if (options.path === ":memory:" || options.identity.length === 0)
|
|
3668
4668
|
throw new Error("Persistent publisher needs a file and an identity");
|
|
3669
|
-
const path =
|
|
3670
|
-
const directory =
|
|
3671
|
-
await
|
|
3672
|
-
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);
|
|
3673
4673
|
if (!metadata.isDirectory() || (metadata.mode & 63) !== 0)
|
|
3674
4674
|
throw new Error("Publisher state directory must be a real directory with 0700 permissions");
|
|
3675
|
-
const file = await
|
|
4675
|
+
const file = await open6(path, constants3.O_CREAT | constants3.O_RDWR | constants3.O_NOFOLLOW, 384);
|
|
3676
4676
|
try {
|
|
3677
4677
|
const metadata2 = await file.stat();
|
|
3678
4678
|
if (!metadata2.isFile() || (metadata2.mode & 63) !== 0)
|
|
@@ -3681,9 +4681,9 @@ async function openSqlitePublisherState(options) {
|
|
|
3681
4681
|
} finally {
|
|
3682
4682
|
await file.close();
|
|
3683
4683
|
}
|
|
3684
|
-
const { DatabaseSync:
|
|
3685
|
-
const database = new
|
|
3686
|
-
const token =
|
|
4684
|
+
const { DatabaseSync: DatabaseSync3 } = await import("node:sqlite");
|
|
4685
|
+
const database = new DatabaseSync3(path);
|
|
4686
|
+
const token = randomBytes8(16).toString("hex");
|
|
3687
4687
|
let closed = false;
|
|
3688
4688
|
let inTransaction = false;
|
|
3689
4689
|
const transaction = (work) => {
|
|
@@ -3774,67 +4774,362 @@ async function openSqlitePublisherState(options) {
|
|
|
3774
4774
|
};
|
|
3775
4775
|
}
|
|
3776
4776
|
|
|
3777
|
-
// src/
|
|
3778
|
-
import {
|
|
3779
|
-
import
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
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) {
|
|
3813
4857
|
return {
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
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;
|
|
3822
5079
|
return {
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
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
|
+
});
|
|
3826
5103
|
},
|
|
3827
5104
|
async release() {
|
|
3828
|
-
|
|
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
|
+
});
|
|
3829
5110
|
}
|
|
3830
5111
|
};
|
|
3831
|
-
}
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
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)
|
|
3838
5133
|
};
|
|
3839
5134
|
}
|
|
3840
5135
|
export {
|
|
@@ -3845,6 +5140,7 @@ export {
|
|
|
3845
5140
|
assertSameIntentBinding,
|
|
3846
5141
|
boundedPublisherOperation,
|
|
3847
5142
|
createAccountSourceWakeSigner,
|
|
5143
|
+
createBestEffortSnapshotWorker,
|
|
3848
5144
|
createBoundedPublisherEthersTransport,
|
|
3849
5145
|
createBoundedPublisherFetch,
|
|
3850
5146
|
createEthereumFinalizedPublisherSource,
|
|
@@ -3863,8 +5159,10 @@ export {
|
|
|
3863
5159
|
createPublisherHandshakeClient,
|
|
3864
5160
|
createPublisherHandshakeDiscovery,
|
|
3865
5161
|
createPublisherHttpsTransport,
|
|
5162
|
+
createSqliteBestEffortInbox,
|
|
3866
5163
|
createSqlitePublisherHandshakeStores,
|
|
3867
5164
|
createSqliteWakeQueue,
|
|
5165
|
+
createSqliteWakeReceiver,
|
|
3868
5166
|
createSubscriberHandshakeClient,
|
|
3869
5167
|
createSynchronizedEventKitHandshakeReader,
|
|
3870
5168
|
createUtcIntervalPublisherSource,
|
|
@@ -3872,6 +5170,8 @@ export {
|
|
|
3872
5170
|
handshakeMutationRequestFingerprint,
|
|
3873
5171
|
intentLookupKey,
|
|
3874
5172
|
isPublicPublisherAddress,
|
|
5173
|
+
migrateIdOnlyReducerCheckpoint,
|
|
5174
|
+
migrateReceiverStore,
|
|
3875
5175
|
openPublisherDataLake,
|
|
3876
5176
|
openPublisherRestGateway,
|
|
3877
5177
|
openSqlitePublisherState,
|