@xyo-network/event-kit-node 0.1.1 → 0.1.2
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 +23 -3
- package/dist/node/handshake.mjs +122 -249
- package/dist/node/handshake.mjs.map +2 -2
- package/dist/node/handshakeClients.d.ts +7 -4
- package/dist/node/handshakeClients.d.ts.map +1 -1
- package/dist/node/handshakeMutationIntent.d.ts +118 -34
- package/dist/node/handshakeMutationIntent.d.ts.map +1 -1
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.mjs +1582 -291
- package/dist/node/index.mjs.map +4 -4
- package/dist/node/runtime/abort.d.ts +6 -0
- package/dist/node/runtime/abort.d.ts.map +1 -0
- package/dist/node/runtime/boundedFetch.d.ts +20 -0
- package/dist/node/runtime/boundedFetch.d.ts.map +1 -0
- package/dist/node/runtime/handshakeDiscovery.d.ts +16 -0
- package/dist/node/runtime/handshakeDiscovery.d.ts.map +1 -0
- package/dist/node/runtime/httpsTransport.d.ts +24 -0
- package/dist/node/runtime/httpsTransport.d.ts.map +1 -0
- package/dist/node/runtime/publisherRuntime.d.ts +84 -0
- package/dist/node/runtime/publisherRuntime.d.ts.map +1 -0
- package/dist/node/runtime/restGateway.d.ts +36 -0
- package/dist/node/runtime/restGateway.d.ts.map +1 -0
- package/dist/node/runtime/sourceSigner.d.ts +41 -0
- package/dist/node/runtime/sourceSigner.d.ts.map +1 -0
- package/dist/node/runtime/sources.d.ts +35 -0
- package/dist/node/runtime/sources.d.ts.map +1 -0
- package/dist/node/runtime/sqliteHandshakeStores.d.ts +15 -0
- package/dist/node/runtime/sqliteHandshakeStores.d.ts.map +1 -0
- package/dist/node/runtime/sqlitePublisherState.d.ts +21 -0
- package/dist/node/runtime/sqlitePublisherState.d.ts.map +1 -0
- package/dist/node/runtime.d.ts +10 -0
- package/dist/node/runtime.d.ts.map +1 -0
- package/dist/node/runtime.mjs +1640 -0
- package/dist/node/runtime.mjs.map +7 -0
- package/package.json +16 -11
package/dist/node/index.mjs
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
4
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
5
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
6
|
+
if (decorator = decorators[i])
|
|
7
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
8
|
+
if (kind && result) __defProp(target, key, result);
|
|
9
|
+
return result;
|
|
10
|
+
};
|
|
11
|
+
|
|
1
12
|
// src/filesystemCursorStore.ts
|
|
2
13
|
import {
|
|
3
14
|
mkdir,
|
|
@@ -765,8 +776,8 @@ function createMemoryHandshakeMutationIntentStore() {
|
|
|
765
776
|
const intents = /* @__PURE__ */ new Map();
|
|
766
777
|
return {
|
|
767
778
|
async get(rawLookup) {
|
|
768
|
-
const
|
|
769
|
-
return intents.get(intentLookupKey(
|
|
779
|
+
const lookup2 = parseHandshakeMutationIntentLookup(rawLookup);
|
|
780
|
+
return intents.get(intentLookupKey(lookup2));
|
|
770
781
|
},
|
|
771
782
|
async getOrCreate(rawIntent) {
|
|
772
783
|
const proposed = await parseHandshakeMutationIntent(rawIntent);
|
|
@@ -781,8 +792,8 @@ function createMemoryHandshakeMutationIntentStore() {
|
|
|
781
792
|
}
|
|
782
793
|
};
|
|
783
794
|
}
|
|
784
|
-
function intentLookupKey(
|
|
785
|
-
const parsed = parseHandshakeMutationIntentLookup(
|
|
795
|
+
function intentLookupKey(lookup2) {
|
|
796
|
+
const parsed = parseHandshakeMutationIntentLookup(lookup2);
|
|
786
797
|
return [parsed.chainId, parsed.publisher, parsed.source, parsed.kind, parsed.operationId].join("/");
|
|
787
798
|
}
|
|
788
799
|
function assertSameIntentBinding(left, right) {
|
|
@@ -853,22 +864,22 @@ async function assertRevocationIntent(intent) {
|
|
|
853
864
|
function createFilesystemHandshakeMutationIntentStore(options) {
|
|
854
865
|
const root = PATH9.resolve(options.path);
|
|
855
866
|
function pathFor(rawLookup) {
|
|
856
|
-
const
|
|
867
|
+
const lookup2 = parseHandshakeMutationIntentLookup(rawLookup);
|
|
857
868
|
return PATH9.join(
|
|
858
869
|
root,
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
`${
|
|
870
|
+
lookup2.chainId,
|
|
871
|
+
lookup2.publisher,
|
|
872
|
+
lookup2.source,
|
|
873
|
+
lookup2.kind,
|
|
874
|
+
`${lookup2.operationId}.json`
|
|
864
875
|
);
|
|
865
876
|
}
|
|
866
|
-
async function readIntent(
|
|
867
|
-
const path = pathFor(
|
|
877
|
+
async function readIntent(lookup2) {
|
|
878
|
+
const path = pathFor(lookup2);
|
|
868
879
|
const raw = await readJsonFileStrict(path);
|
|
869
880
|
if (raw === void 0) return void 0;
|
|
870
881
|
const intent = await parseHandshakeMutationIntent(raw);
|
|
871
|
-
const parsedLookup = parseHandshakeMutationIntentLookup(
|
|
882
|
+
const parsedLookup = parseHandshakeMutationIntentLookup(lookup2);
|
|
872
883
|
if (intent.chainId !== parsedLookup.chainId || intent.kind !== parsedLookup.kind || intent.operationId !== parsedLookup.operationId || intent.publisher !== parsedLookup.publisher || intent.source !== parsedLookup.source) {
|
|
873
884
|
throw new Error("handshake mutation intent does not match its immutable path");
|
|
874
885
|
}
|
|
@@ -877,8 +888,8 @@ function createFilesystemHandshakeMutationIntentStore(options) {
|
|
|
877
888
|
return intent;
|
|
878
889
|
}
|
|
879
890
|
return {
|
|
880
|
-
async get(
|
|
881
|
-
return await readIntent(parseHandshakeMutationIntentLookup(
|
|
891
|
+
async get(lookup2) {
|
|
892
|
+
return await readIntent(parseHandshakeMutationIntentLookup(lookup2));
|
|
882
893
|
},
|
|
883
894
|
async getOrCreate(rawIntent) {
|
|
884
895
|
const proposed = await parseHandshakeMutationIntent(rawIntent);
|
|
@@ -906,35 +917,35 @@ import {
|
|
|
906
917
|
} from "@xyo-network/event-kit-schemas";
|
|
907
918
|
function createFilesystemPublisherEncryptionKeyStore(options) {
|
|
908
919
|
const root = PATH10.resolve(options.path);
|
|
909
|
-
function parseLookup(
|
|
920
|
+
function parseLookup(lookup2) {
|
|
910
921
|
return {
|
|
911
|
-
encryptionKey: HashZod2.parse(
|
|
912
|
-
keyId: HandshakeIdZod2.parse(
|
|
913
|
-
publisher: NormalizedAddressZod2.parse(
|
|
922
|
+
encryptionKey: HashZod2.parse(lookup2.encryptionKey),
|
|
923
|
+
keyId: HandshakeIdZod2.parse(lookup2.keyId),
|
|
924
|
+
publisher: NormalizedAddressZod2.parse(lookup2.publisher)
|
|
914
925
|
};
|
|
915
926
|
}
|
|
916
|
-
function pathFor(
|
|
917
|
-
return PATH10.join(root,
|
|
927
|
+
function pathFor(lookup2) {
|
|
928
|
+
return PATH10.join(root, lookup2.publisher, `${lookup2.encryptionKey}-${lookup2.keyId}.json`);
|
|
918
929
|
}
|
|
919
|
-
function keyIdPath(
|
|
920
|
-
return PATH10.join(root,
|
|
930
|
+
function keyIdPath(lookup2) {
|
|
931
|
+
return PATH10.join(root, lookup2.publisher, "key-ids", `${lookup2.keyId}.json`);
|
|
921
932
|
}
|
|
922
|
-
async function readRecord2(
|
|
923
|
-
const path = pathFor(
|
|
933
|
+
async function readRecord2(lookup2) {
|
|
934
|
+
const path = pathFor(lookup2);
|
|
924
935
|
const raw = await readJsonFileStrict(path);
|
|
925
936
|
if (raw === void 0) return void 0;
|
|
926
937
|
const parsed = parsePrivateKeyRecord(raw);
|
|
927
|
-
if (parsed.publisher !==
|
|
928
|
-
const keyIdRecord = parseKeyIdRecord(await readJsonFileStrict(keyIdPath(
|
|
929
|
-
if (keyIdRecord.publisher !==
|
|
938
|
+
if (parsed.publisher !== lookup2.publisher || parsed.encryptionKey !== lookup2.encryptionKey || parsed.keyId !== lookup2.keyId) throw new Error("publisher encryption-key record does not match its immutable path");
|
|
939
|
+
const keyIdRecord = parseKeyIdRecord(await readJsonFileStrict(keyIdPath(lookup2)));
|
|
940
|
+
if (keyIdRecord.publisher !== lookup2.publisher || keyIdRecord.encryptionKey !== lookup2.encryptionKey || keyIdRecord.keyId !== lookup2.keyId) throw new Error("publisher encryption-key keyId index does not match its immutable path");
|
|
930
941
|
const metadata = await stat2(path);
|
|
931
942
|
if ((metadata.mode & 63) !== 0) throw new Error("publisher encryption-key record permissions are too broad");
|
|
932
943
|
return parsed;
|
|
933
944
|
}
|
|
934
945
|
return {
|
|
935
946
|
async getPrivateKey(rawLookup) {
|
|
936
|
-
const
|
|
937
|
-
const record = await readRecord2(
|
|
947
|
+
const lookup2 = parseLookup(rawLookup);
|
|
948
|
+
const record = await readRecord2(lookup2);
|
|
938
949
|
if (record === void 0) return void 0;
|
|
939
950
|
return Object.freeze({
|
|
940
951
|
encryptionKey: record.encryptionKey,
|
|
@@ -944,25 +955,25 @@ function createFilesystemPublisherEncryptionKeyStore(options) {
|
|
|
944
955
|
},
|
|
945
956
|
async putPrivateKey(input) {
|
|
946
957
|
const record = parsePrivateKeyRecord({ ...input.key, publisher: input.publisher });
|
|
947
|
-
const
|
|
948
|
-
const path = pathFor(
|
|
958
|
+
const lookup2 = parseLookup(record);
|
|
959
|
+
const path = pathFor(lookup2);
|
|
949
960
|
const keyIdRecord = {
|
|
950
961
|
encryptionKey: record.encryptionKey,
|
|
951
962
|
keyId: record.keyId,
|
|
952
963
|
publisher: record.publisher
|
|
953
964
|
};
|
|
954
965
|
if (!await publishJsonFile(
|
|
955
|
-
keyIdPath(
|
|
966
|
+
keyIdPath(lookup2),
|
|
956
967
|
keyIdRecord,
|
|
957
968
|
{ directoryMode: 448, mode: 384 }
|
|
958
969
|
)) {
|
|
959
|
-
const existingKeyId = parseKeyIdRecord(await readJsonFileStrict(keyIdPath(
|
|
970
|
+
const existingKeyId = parseKeyIdRecord(await readJsonFileStrict(keyIdPath(lookup2)));
|
|
960
971
|
if (stringifyJson(existingKeyId) !== stringifyJson(keyIdRecord)) {
|
|
961
972
|
throw new Error(`publisher encryption-key keyId collision for ${record.keyId}`);
|
|
962
973
|
}
|
|
963
974
|
}
|
|
964
975
|
if (await publishJsonFile(path, record, { directoryMode: 448, mode: 384 })) return "stored";
|
|
965
|
-
const existing = await readRecord2(
|
|
976
|
+
const existing = await readRecord2(lookup2);
|
|
966
977
|
if (stringifyJson(existing) !== stringifyJson(record)) {
|
|
967
978
|
throw new Error(`publisher encryption-key collision for ${record.encryptionKey}/${record.keyId}`);
|
|
968
979
|
}
|
|
@@ -1143,6 +1154,7 @@ function requireTime(value) {
|
|
|
1143
1154
|
// src/handshakeClients.ts
|
|
1144
1155
|
import {
|
|
1145
1156
|
beginPublisherEndpointChallengeAttempt,
|
|
1157
|
+
canonicalJson,
|
|
1146
1158
|
completePublisherEndpointChallenge,
|
|
1147
1159
|
createEncryptedWakeSubscription,
|
|
1148
1160
|
createPublisherEncryptionKeyRevocation,
|
|
@@ -1191,10 +1203,7 @@ function createSynchronizedEventKitHandshakeReader(options) {
|
|
|
1191
1203
|
function assertIdentity() {
|
|
1192
1204
|
const checkpoint = options.replay.snapshot();
|
|
1193
1205
|
if (checkpoint.chainId !== chainId || checkpoint.network !== options.session.network || checkpoint.publisher !== publisher) {
|
|
1194
|
-
throw new EventKitHandshakeClientError(
|
|
1195
|
-
"identity-mismatch",
|
|
1196
|
-
"Finalized replay identity does not match the pinned Event Kit network and publisher"
|
|
1197
|
-
);
|
|
1206
|
+
throw new EventKitHandshakeClientError("identity-mismatch", "Finalized replay identity does not match the pinned Event Kit network and publisher");
|
|
1198
1207
|
}
|
|
1199
1208
|
}
|
|
1200
1209
|
return Object.freeze({
|
|
@@ -1247,10 +1256,7 @@ function createEventKitHandshakePublicationPort(transport) {
|
|
|
1247
1256
|
const raw = String(await transport.signer.address());
|
|
1248
1257
|
const normalized = NormalizedAddressZod3.parse(normalizeAddress(raw));
|
|
1249
1258
|
if (raw !== normalized) {
|
|
1250
|
-
throw new EventKitHandshakeClientError(
|
|
1251
|
-
"identity-mismatch",
|
|
1252
|
-
"XL1 publication signer address is not canonical lowercase hexadecimal without 0x"
|
|
1253
|
-
);
|
|
1259
|
+
throw new EventKitHandshakeClientError("identity-mismatch", "XL1 publication signer address is not canonical lowercase hexadecimal without 0x");
|
|
1254
1260
|
}
|
|
1255
1261
|
return normalized;
|
|
1256
1262
|
}
|
|
@@ -1266,10 +1272,7 @@ function nextBlock(head) {
|
|
|
1266
1272
|
function samplePublisherClock(nowSeconds) {
|
|
1267
1273
|
const sampled = nowSeconds();
|
|
1268
1274
|
if (!Number.isSafeInteger(sampled) || sampled < 0) {
|
|
1269
|
-
throw new EventKitHandshakeClientError(
|
|
1270
|
-
"challenge-rejected",
|
|
1271
|
-
"Publisher clock must return a nonnegative JSON-safe Unix second"
|
|
1272
|
-
);
|
|
1275
|
+
throw new EventKitHandshakeClientError("challenge-rejected", "Publisher clock must return a nonnegative JSON-safe Unix second");
|
|
1273
1276
|
}
|
|
1274
1277
|
return sampled;
|
|
1275
1278
|
}
|
|
@@ -1292,37 +1295,25 @@ function serviceQuery(reader, snapshot, serviceId) {
|
|
|
1292
1295
|
}
|
|
1293
1296
|
function requireService(discovery) {
|
|
1294
1297
|
if (!discovery.ok) {
|
|
1295
|
-
throw new EventKitHandshakeClientError(
|
|
1296
|
-
"service-unavailable",
|
|
1297
|
-
`Publisher service is unavailable: ${discovery.reason}`
|
|
1298
|
-
);
|
|
1298
|
+
throw new EventKitHandshakeClientError("service-unavailable", `Publisher service is unavailable: ${discovery.reason}`);
|
|
1299
1299
|
}
|
|
1300
1300
|
return discovery;
|
|
1301
1301
|
}
|
|
1302
1302
|
function requireExpectedService(discovery, expected) {
|
|
1303
1303
|
const expectedHash = HashZod4.parse(expected);
|
|
1304
1304
|
if (discovery.service.objectHash !== expectedHash) {
|
|
1305
|
-
throw new EventKitHandshakeClientError(
|
|
1306
|
-
"service-mismatch",
|
|
1307
|
-
"Currently selected publisher service does not match the expected object hash"
|
|
1308
|
-
);
|
|
1305
|
+
throw new EventKitHandshakeClientError("service-mismatch", "Currently selected publisher service does not match the expected object hash");
|
|
1309
1306
|
}
|
|
1310
1307
|
return expectedHash;
|
|
1311
1308
|
}
|
|
1312
1309
|
function assertPublicationIdentity(publication, reader) {
|
|
1313
1310
|
if (publication.chainId !== reader.chainId) {
|
|
1314
|
-
throw new EventKitHandshakeClientError(
|
|
1315
|
-
"identity-mismatch",
|
|
1316
|
-
"Publication transport chain ID does not match the synchronized handshake reader"
|
|
1317
|
-
);
|
|
1311
|
+
throw new EventKitHandshakeClientError("identity-mismatch", "Publication transport chain ID does not match the synchronized handshake reader");
|
|
1318
1312
|
}
|
|
1319
1313
|
}
|
|
1320
1314
|
async function assertPublicationSource(publication, expected) {
|
|
1321
1315
|
if (await publication.source() !== expected) {
|
|
1322
|
-
throw new EventKitHandshakeClientError(
|
|
1323
|
-
"identity-mismatch",
|
|
1324
|
-
"Publication signer does not match the required handshake Claim source"
|
|
1325
|
-
);
|
|
1316
|
+
throw new EventKitHandshakeClientError("identity-mismatch", "Publication signer does not match the required handshake Claim source");
|
|
1326
1317
|
}
|
|
1327
1318
|
}
|
|
1328
1319
|
function intentLookup(reader, kind, operationId, source) {
|
|
@@ -1335,15 +1326,13 @@ function intentLookup(reader, kind, operationId, source) {
|
|
|
1335
1326
|
};
|
|
1336
1327
|
}
|
|
1337
1328
|
function mutationIntentError(error) {
|
|
1338
|
-
return error instanceof HandshakeMutationIntentConflictError ? new EventKitHandshakeClientError("mutation-intent-conflict", error.message) : new EventKitHandshakeClientError(
|
|
1339
|
-
"mutation-intent-unavailable",
|
|
1340
|
-
"Handshake mutation intent storage is unavailable or invalid"
|
|
1341
|
-
);
|
|
1329
|
+
return error instanceof HandshakeMutationIntentConflictError ? new EventKitHandshakeClientError("mutation-intent-conflict", error.message) : new EventKitHandshakeClientError("mutation-intent-unavailable", "Handshake mutation intent storage is unavailable or invalid");
|
|
1342
1330
|
}
|
|
1343
1331
|
async function loadMutationIntent(store, reader, kind, operationId, source, requestFingerprint) {
|
|
1344
1332
|
try {
|
|
1345
1333
|
const existing = await store.get(intentLookup(reader, kind, operationId, source));
|
|
1346
|
-
if (existing === void 0)
|
|
1334
|
+
if (existing === void 0)
|
|
1335
|
+
return void 0;
|
|
1347
1336
|
if (existing.kind !== kind || existing.requestFingerprint !== requestFingerprint) {
|
|
1348
1337
|
throw new HandshakeMutationIntentConflictError();
|
|
1349
1338
|
}
|
|
@@ -1362,7 +1351,8 @@ async function persistMutationIntent(store, intent) {
|
|
|
1362
1351
|
async function publisherServiceRequestFingerprint(input) {
|
|
1363
1352
|
return await handshakeMutationRequestFingerprint({
|
|
1364
1353
|
expiresAtBlock: input.expiresAtBlock ?? null,
|
|
1365
|
-
|
|
1354
|
+
maxPositionsPerWake: input.maxPositionsPerWake,
|
|
1355
|
+
sources: input.sources,
|
|
1366
1356
|
maxSubscriptionLifetimeBlocks: input.maxSubscriptionLifetimeBlocks,
|
|
1367
1357
|
validFromBlock: input.validFromBlock ?? null
|
|
1368
1358
|
});
|
|
@@ -1371,7 +1361,7 @@ async function publisherRotationRequestFingerprint(input) {
|
|
|
1371
1361
|
return await handshakeMutationRequestFingerprint({
|
|
1372
1362
|
expiresAtBlock: input.expiresAtBlock ?? null,
|
|
1373
1363
|
keyExpiresAtBlock: input.keyExpiresAtBlock ?? null,
|
|
1374
|
-
|
|
1364
|
+
maxPositionsPerWake: input.maxPositionsPerWake ?? null,
|
|
1375
1365
|
maxSubscriptionLifetimeBlocks: input.maxSubscriptionLifetimeBlocks ?? null,
|
|
1376
1366
|
service: HashZod4.parse(input.service),
|
|
1377
1367
|
validFromBlock: input.validFromBlock ?? null
|
|
@@ -1385,7 +1375,9 @@ async function subscriberSubscriptionRequestFingerprint(input) {
|
|
|
1385
1375
|
endpoint: input.endpoint,
|
|
1386
1376
|
expiresAtBlock: input.expiresAtBlock,
|
|
1387
1377
|
grantId: input.grantId ?? null,
|
|
1388
|
-
|
|
1378
|
+
source: input.source,
|
|
1379
|
+
fromPosition: input.fromPosition,
|
|
1380
|
+
maxPositionsPerWake: input.maxPositionsPerWake ?? null,
|
|
1389
1381
|
queueId: input.queueId ?? null,
|
|
1390
1382
|
revision: input.revision ?? null,
|
|
1391
1383
|
subscriptionId: input.subscriptionId ?? null,
|
|
@@ -1428,10 +1420,7 @@ function protocolCandidate(reader, discovery, candidate, keys) {
|
|
|
1428
1420
|
};
|
|
1429
1421
|
}
|
|
1430
1422
|
function validationError(reason) {
|
|
1431
|
-
return new EventKitHandshakeClientError(
|
|
1432
|
-
"subscription-invalid",
|
|
1433
|
-
`Publisher subscription candidate failed validation: ${reason}`
|
|
1434
|
-
);
|
|
1423
|
+
return new EventKitHandshakeClientError("subscription-invalid", `Publisher subscription candidate failed validation: ${reason}`);
|
|
1435
1424
|
}
|
|
1436
1425
|
function challengeError(result) {
|
|
1437
1426
|
const reason = result.ok ? "unknown" : result.reason;
|
|
@@ -1456,7 +1445,8 @@ function createPublisherHandshakeClient(options) {
|
|
|
1456
1445
|
}
|
|
1457
1446
|
async function resolveCandidate(selector) {
|
|
1458
1447
|
const found = await candidateDiscovery(selector);
|
|
1459
|
-
if (!found.ok)
|
|
1448
|
+
if (!found.ok)
|
|
1449
|
+
throw new EventKitHandshakeClientError("service-unavailable", `Publisher service is unavailable: ${found.reason}`);
|
|
1460
1450
|
const subscriber = NormalizedAddressZod3.parse(selector.subscriber);
|
|
1461
1451
|
const subscription = selector.subscription === void 0 ? void 0 : HashZod4.parse(selector.subscription);
|
|
1462
1452
|
const candidates = found.candidates.filter((candidate2) => candidate2.subscriber === subscriber && (subscription === void 0 || candidate2.tuple.subscription.objectHash === subscription));
|
|
@@ -1467,10 +1457,9 @@ function createPublisherHandshakeClient(options) {
|
|
|
1467
1457
|
throw new EventKitHandshakeClientError("candidate-ambiguous", "More than one finalized acceptance candidate matched");
|
|
1468
1458
|
}
|
|
1469
1459
|
const candidate = candidates[0];
|
|
1470
|
-
const validated = await validateAndDecryptPublisherSubscription(
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
if (!validated.ok) throw validationError(validated.reason);
|
|
1460
|
+
const validated = await validateAndDecryptPublisherSubscription(protocolCandidate(options.reader, found, candidate, options.keys));
|
|
1461
|
+
if (!validated.ok)
|
|
1462
|
+
throw validationError(validated.reason);
|
|
1474
1463
|
return { candidate, validated };
|
|
1475
1464
|
}
|
|
1476
1465
|
async function candidateForChallenge(challenge) {
|
|
@@ -1479,10 +1468,7 @@ function createPublisherHandshakeClient(options) {
|
|
|
1479
1468
|
subscription: challenge.subscription
|
|
1480
1469
|
});
|
|
1481
1470
|
if (!endpointChallengeMatchesPublisherContext(challenge, resolved.validated.context)) {
|
|
1482
|
-
throw new EventKitHandshakeClientError(
|
|
1483
|
-
"challenge-context-invalid",
|
|
1484
|
-
"Endpoint challenge does not match the current finalized subscription candidate"
|
|
1485
|
-
);
|
|
1471
|
+
throw new EventKitHandshakeClientError("challenge-context-invalid", "Endpoint challenge does not match the current finalized subscription candidate");
|
|
1486
1472
|
}
|
|
1487
1473
|
return resolved;
|
|
1488
1474
|
}
|
|
@@ -1572,12 +1558,7 @@ function createPublisherHandshakeClient(options) {
|
|
|
1572
1558
|
operationId: intent.operationId,
|
|
1573
1559
|
response: intent.response
|
|
1574
1560
|
});
|
|
1575
|
-
const reconciliation = await reconcileAcceptance(
|
|
1576
|
-
acceptanceHash,
|
|
1577
|
-
publication,
|
|
1578
|
-
intent.challenge.subscriber,
|
|
1579
|
-
intent.challenge.subscription
|
|
1580
|
-
);
|
|
1561
|
+
const reconciliation = await reconcileAcceptance(acceptanceHash, publication, intent.challenge.subscriber, intent.challenge.subscription);
|
|
1581
1562
|
return Object.freeze({
|
|
1582
1563
|
acceptance: Object.freeze({ body: intent.acceptance, objectHash: acceptanceHash }),
|
|
1583
1564
|
challenge: intent.operationId,
|
|
@@ -1598,10 +1579,7 @@ function createPublisherHandshakeClient(options) {
|
|
|
1598
1579
|
publisher: options.reader.publisher
|
|
1599
1580
|
});
|
|
1600
1581
|
} catch {
|
|
1601
|
-
throw new EventKitHandshakeClientError(
|
|
1602
|
-
"key-custody-failed",
|
|
1603
|
-
"Publisher encryption private key was not durably retained; publication was not attempted"
|
|
1604
|
-
);
|
|
1582
|
+
throw new EventKitHandshakeClientError("key-custody-failed", "Publisher encryption private key was not durably retained; publication was not attempted");
|
|
1605
1583
|
}
|
|
1606
1584
|
const [keyHash, serviceHash] = await Promise.all([
|
|
1607
1585
|
handshakeObjectHash2(intent.encryptionKey),
|
|
@@ -1624,19 +1602,19 @@ function createPublisherHandshakeClient(options) {
|
|
|
1624
1602
|
const client = {
|
|
1625
1603
|
async acceptanceCandidates(selector = {}) {
|
|
1626
1604
|
const found = await candidateDiscovery(selector);
|
|
1627
|
-
if (!found.ok || selector.subscription === void 0)
|
|
1605
|
+
if (!found.ok || selector.subscription === void 0)
|
|
1606
|
+
return found;
|
|
1628
1607
|
const subscription = HashZod4.parse(selector.subscription);
|
|
1629
1608
|
return Object.freeze({
|
|
1630
1609
|
...found,
|
|
1631
|
-
candidates: found.candidates.filter(
|
|
1632
|
-
(candidate) => candidate.tuple.subscription.objectHash === subscription
|
|
1633
|
-
)
|
|
1610
|
+
candidates: found.candidates.filter((candidate) => candidate.tuple.subscription.objectHash === subscription)
|
|
1634
1611
|
});
|
|
1635
1612
|
},
|
|
1636
1613
|
async beginChallenge(input) {
|
|
1637
1614
|
const challengeHash = HashZod4.parse(input.challengeHash);
|
|
1638
1615
|
const record = await options.challenges.get(challengeHash);
|
|
1639
|
-
if (record === void 0)
|
|
1616
|
+
if (record === void 0)
|
|
1617
|
+
return { kind: "missing" };
|
|
1640
1618
|
const resolved = await candidateForChallenge(record.challenge);
|
|
1641
1619
|
const begun = await beginPublisherEndpointChallengeAttempt({
|
|
1642
1620
|
challengeHash,
|
|
@@ -1671,10 +1649,7 @@ function createPublisherHandshakeClient(options) {
|
|
|
1671
1649
|
throw new EventKitHandshakeClientError("challenge-not-found", "Endpoint challenge was not found");
|
|
1672
1650
|
}
|
|
1673
1651
|
if (record.challenge.subscriber !== subscriber || record.challenge.subscription !== subscription) {
|
|
1674
|
-
throw new EventKitHandshakeClientError(
|
|
1675
|
-
"challenge-context-invalid",
|
|
1676
|
-
"Stored endpoint challenge does not match the caller expected subscriber and subscription"
|
|
1677
|
-
);
|
|
1652
|
+
throw new EventKitHandshakeClientError("challenge-context-invalid", "Stored endpoint challenge does not match the caller expected subscriber and subscription");
|
|
1678
1653
|
}
|
|
1679
1654
|
const responseHash = await handshakeObjectHash2(input.response);
|
|
1680
1655
|
const requestFingerprint = await exactMutationRequestFingerprint({
|
|
@@ -1683,20 +1658,10 @@ function createPublisherHandshakeClient(options) {
|
|
|
1683
1658
|
subscriber,
|
|
1684
1659
|
subscription
|
|
1685
1660
|
});
|
|
1686
|
-
const existing = await loadMutationIntent(
|
|
1687
|
-
options.intents,
|
|
1688
|
-
options.reader,
|
|
1689
|
-
"publisher-subscription-accept",
|
|
1690
|
-
challengeHash,
|
|
1691
|
-
options.reader.publisher,
|
|
1692
|
-
requestFingerprint
|
|
1693
|
-
);
|
|
1661
|
+
const existing = await loadMutationIntent(options.intents, options.reader, "publisher-subscription-accept", challengeHash, options.reader.publisher, requestFingerprint);
|
|
1694
1662
|
if (existing !== void 0) {
|
|
1695
1663
|
if (record.state !== "verified" || record.responseHash !== responseHash) {
|
|
1696
|
-
throw new EventKitHandshakeClientError(
|
|
1697
|
-
"mutation-intent-conflict",
|
|
1698
|
-
"Acceptance intent does not match the durable verified challenge decision"
|
|
1699
|
-
);
|
|
1664
|
+
throw new EventKitHandshakeClientError("mutation-intent-conflict", "Acceptance intent does not match the durable verified challenge decision");
|
|
1700
1665
|
}
|
|
1701
1666
|
return await publishAcceptanceIntent(existing, "resumed");
|
|
1702
1667
|
}
|
|
@@ -1708,36 +1673,22 @@ function createPublisherHandshakeClient(options) {
|
|
|
1708
1673
|
store: options.challenges,
|
|
1709
1674
|
subscription: resolved.validated
|
|
1710
1675
|
});
|
|
1711
|
-
if (!completed.ok)
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
"
|
|
1716
|
-
challengeHash,
|
|
1717
|
-
options.reader.publisher,
|
|
1718
|
-
requestFingerprint
|
|
1719
|
-
);
|
|
1720
|
-
if (concurrentlyPersisted !== void 0) return await publishAcceptanceIntent(concurrentlyPersisted, "resumed");
|
|
1676
|
+
if (!completed.ok)
|
|
1677
|
+
throw challengeError(completed);
|
|
1678
|
+
const concurrentlyPersisted = await loadMutationIntent(options.intents, options.reader, "publisher-subscription-accept", challengeHash, options.reader.publisher, requestFingerprint);
|
|
1679
|
+
if (concurrentlyPersisted !== void 0)
|
|
1680
|
+
return await publishAcceptanceIntent(concurrentlyPersisted, "resumed");
|
|
1721
1681
|
let current;
|
|
1722
1682
|
try {
|
|
1723
1683
|
current = await candidateForChallenge(record.challenge);
|
|
1724
1684
|
} catch (error) {
|
|
1725
|
-
const recovered = await loadMutationIntent(
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
"publisher-subscription-accept",
|
|
1729
|
-
challengeHash,
|
|
1730
|
-
options.reader.publisher,
|
|
1731
|
-
requestFingerprint
|
|
1732
|
-
);
|
|
1733
|
-
if (recovered !== void 0) return await publishAcceptanceIntent(recovered, "resumed");
|
|
1685
|
+
const recovered = await loadMutationIntent(options.intents, options.reader, "publisher-subscription-accept", challengeHash, options.reader.publisher, requestFingerprint);
|
|
1686
|
+
if (recovered !== void 0)
|
|
1687
|
+
return await publishAcceptanceIntent(recovered, "resumed");
|
|
1734
1688
|
throw error;
|
|
1735
1689
|
}
|
|
1736
1690
|
if (!endpointChallengeMatchesPublisherContext(record.challenge, current.validated.context)) {
|
|
1737
|
-
throw new EventKitHandshakeClientError(
|
|
1738
|
-
"challenge-context-invalid",
|
|
1739
|
-
"Endpoint challenge candidate changed before acceptance publication"
|
|
1740
|
-
);
|
|
1691
|
+
throw new EventKitHandshakeClientError("challenge-context-invalid", "Endpoint challenge candidate changed before acceptance publication");
|
|
1741
1692
|
}
|
|
1742
1693
|
const resolvedIntent = await persistMutationIntent(options.intents, {
|
|
1743
1694
|
acceptance: completed.acceptance,
|
|
@@ -1779,42 +1730,35 @@ function createPublisherHandshakeClient(options) {
|
|
|
1779
1730
|
async publishService(input) {
|
|
1780
1731
|
await assertPublicationSource(options.publication, options.reader.publisher);
|
|
1781
1732
|
if (serviceId !== void 0) {
|
|
1782
|
-
throw new EventKitHandshakeClientError(
|
|
1783
|
-
"service-root-requires-fresh-id",
|
|
1784
|
-
"Root service publication requires an unconfigured client so a fresh serviceId is generated"
|
|
1785
|
-
);
|
|
1733
|
+
throw new EventKitHandshakeClientError("service-root-requires-fresh-id", "Root service publication requires an unconfigured client so a fresh serviceId is generated");
|
|
1786
1734
|
}
|
|
1787
1735
|
const operationId = HandshakeIdZod3.parse(input.operationId);
|
|
1788
1736
|
const requestFingerprint = await publisherServiceRequestFingerprint(input);
|
|
1789
|
-
const priorIntent = await loadMutationIntent(
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
"publisher-service-publish",
|
|
1793
|
-
operationId,
|
|
1794
|
-
options.reader.publisher,
|
|
1795
|
-
requestFingerprint
|
|
1796
|
-
);
|
|
1797
|
-
if (priorIntent !== void 0) return await publishPublisherServiceIntent(priorIntent, "resumed");
|
|
1737
|
+
const priorIntent = await loadMutationIntent(options.intents, options.reader, "publisher-service-publish", operationId, options.reader.publisher, requestFingerprint);
|
|
1738
|
+
if (priorIntent !== void 0)
|
|
1739
|
+
return await publishPublisherServiceIntent(priorIntent, "resumed");
|
|
1798
1740
|
const current = await snapshot();
|
|
1799
1741
|
const existing = await discovery(current);
|
|
1800
1742
|
if (existing.ok) {
|
|
1801
|
-
throw new EventKitHandshakeClientError(
|
|
1802
|
-
"service-already-exists",
|
|
1803
|
-
"An active publisher service already exists; rotate its key to publish a successor"
|
|
1804
|
-
);
|
|
1743
|
+
throw new EventKitHandshakeClientError("service-already-exists", "An active publisher service already exists; rotate its key to publish a successor");
|
|
1805
1744
|
}
|
|
1806
1745
|
if (existing.reason !== "service-not-found") {
|
|
1807
1746
|
throw new EventKitHandshakeClientError("service-unavailable", `Cannot declare service: ${existing.reason}`);
|
|
1808
1747
|
}
|
|
1809
1748
|
const validFromBlock = input.validFromBlock ?? nextBlock(current.finalizedHead);
|
|
1810
1749
|
const generated = await generatePublisherEncryptionKey({ validFromBlock });
|
|
1811
|
-
const
|
|
1750
|
+
const serviceInput = {
|
|
1812
1751
|
encryptionKeys: [generated.descriptorHash],
|
|
1813
1752
|
...input.expiresAtBlock === void 0 ? {} : { expiresAtBlock: input.expiresAtBlock },
|
|
1814
|
-
maxBlocksPerWake: input.maxBlocksPerWake,
|
|
1815
1753
|
maxSubscriptionLifetimeBlocks: input.maxSubscriptionLifetimeBlocks,
|
|
1816
1754
|
preferredEncryptionKey: generated.descriptorHash,
|
|
1817
1755
|
validFromBlock
|
|
1756
|
+
};
|
|
1757
|
+
const service = createPublisherService({
|
|
1758
|
+
...serviceInput,
|
|
1759
|
+
authorizationChainId: options.reader.chainId,
|
|
1760
|
+
sources: input.sources,
|
|
1761
|
+
maxPositionsPerWake: input.maxPositionsPerWake
|
|
1818
1762
|
});
|
|
1819
1763
|
const resolvedIntent = await persistMutationIntent(options.intents, {
|
|
1820
1764
|
chainId: options.reader.chainId,
|
|
@@ -1834,23 +1778,13 @@ function createPublisherHandshakeClient(options) {
|
|
|
1834
1778
|
await assertPublicationSource(options.publication, options.reader.publisher);
|
|
1835
1779
|
const keyHash = HashZod4.parse(key);
|
|
1836
1780
|
const requestFingerprint = await exactMutationRequestFingerprint({ key: keyHash });
|
|
1837
|
-
let resolvedIntent = await loadMutationIntent(
|
|
1838
|
-
options.intents,
|
|
1839
|
-
options.reader,
|
|
1840
|
-
"publisher-encryption-key-retire",
|
|
1841
|
-
keyHash,
|
|
1842
|
-
options.reader.publisher,
|
|
1843
|
-
requestFingerprint
|
|
1844
|
-
);
|
|
1781
|
+
let resolvedIntent = await loadMutationIntent(options.intents, options.reader, "publisher-encryption-key-retire", keyHash, options.reader.publisher, requestFingerprint);
|
|
1845
1782
|
let intentDisposition = "resumed";
|
|
1846
1783
|
if (resolvedIntent === void 0) {
|
|
1847
1784
|
const current = await snapshot();
|
|
1848
1785
|
const selected = requireService(await discovery(current));
|
|
1849
1786
|
if (selected.key.objectHash !== keyHash || !selected.service.body.encryptionKeys.includes(keyHash) || selected.service.body.preferredEncryptionKey !== keyHash) {
|
|
1850
|
-
throw new EventKitHandshakeClientError(
|
|
1851
|
-
"key-mismatch",
|
|
1852
|
-
"Encryption key does not match the exact current preferred descriptor evidence"
|
|
1853
|
-
);
|
|
1787
|
+
throw new EventKitHandshakeClientError("key-mismatch", "Encryption key does not match the exact current preferred descriptor evidence");
|
|
1854
1788
|
}
|
|
1855
1789
|
const persisted = await persistMutationIntent(options.intents, {
|
|
1856
1790
|
chainId: options.reader.chainId,
|
|
@@ -1882,14 +1816,7 @@ function createPublisherHandshakeClient(options) {
|
|
|
1882
1816
|
await assertPublicationSource(options.publication, options.reader.publisher);
|
|
1883
1817
|
const service = HashZod4.parse(expectedService);
|
|
1884
1818
|
const requestFingerprint = await exactMutationRequestFingerprint({ service });
|
|
1885
|
-
let resolvedIntent = await loadMutationIntent(
|
|
1886
|
-
options.intents,
|
|
1887
|
-
options.reader,
|
|
1888
|
-
"publisher-service-retire",
|
|
1889
|
-
service,
|
|
1890
|
-
options.reader.publisher,
|
|
1891
|
-
requestFingerprint
|
|
1892
|
-
);
|
|
1819
|
+
let resolvedIntent = await loadMutationIntent(options.intents, options.reader, "publisher-service-retire", service, options.reader.publisher, requestFingerprint);
|
|
1893
1820
|
let intentDisposition = "resumed";
|
|
1894
1821
|
if (resolvedIntent === void 0) {
|
|
1895
1822
|
const current = await snapshot();
|
|
@@ -1925,15 +1852,9 @@ function createPublisherHandshakeClient(options) {
|
|
|
1925
1852
|
await assertPublicationSource(options.publication, options.reader.publisher);
|
|
1926
1853
|
const operationId = HandshakeIdZod3.parse(input.operationId);
|
|
1927
1854
|
const requestFingerprint = await publisherRotationRequestFingerprint(input);
|
|
1928
|
-
const priorIntent = await loadMutationIntent(
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
"publisher-service-rotate-key",
|
|
1932
|
-
operationId,
|
|
1933
|
-
options.reader.publisher,
|
|
1934
|
-
requestFingerprint
|
|
1935
|
-
);
|
|
1936
|
-
if (priorIntent !== void 0) return await publishPublisherServiceIntent(priorIntent, "resumed");
|
|
1855
|
+
const priorIntent = await loadMutationIntent(options.intents, options.reader, "publisher-service-rotate-key", operationId, options.reader.publisher, requestFingerprint);
|
|
1856
|
+
if (priorIntent !== void 0)
|
|
1857
|
+
return await publishPublisherServiceIntent(priorIntent, "resumed");
|
|
1937
1858
|
const current = await snapshot();
|
|
1938
1859
|
const selected = requireService(await discovery(current));
|
|
1939
1860
|
requireExpectedService(selected, input.service);
|
|
@@ -1942,16 +1863,21 @@ function createPublisherHandshakeClient(options) {
|
|
|
1942
1863
|
...input.keyExpiresAtBlock === void 0 ? {} : { expiresAtBlock: input.keyExpiresAtBlock },
|
|
1943
1864
|
validFromBlock
|
|
1944
1865
|
});
|
|
1945
|
-
const
|
|
1866
|
+
const serviceInput = {
|
|
1946
1867
|
encryptionKeys: [generated.descriptorHash],
|
|
1947
1868
|
...input.expiresAtBlock === void 0 ? selected.service.body.expiresAtBlock === void 0 ? {} : { expiresAtBlock: selected.service.body.expiresAtBlock } : { expiresAtBlock: input.expiresAtBlock },
|
|
1948
|
-
maxBlocksPerWake: input.maxBlocksPerWake ?? selected.service.body.limits.maxBlocksPerWake,
|
|
1949
1869
|
maxSubscriptionLifetimeBlocks: input.maxSubscriptionLifetimeBlocks ?? selected.service.body.limits.maxSubscriptionLifetimeBlocks,
|
|
1950
1870
|
preferredEncryptionKey: generated.descriptorHash,
|
|
1951
1871
|
revision: selected.service.body.revision + 1,
|
|
1952
1872
|
serviceId: selected.service.body.serviceId,
|
|
1953
1873
|
supersedes: HashZod4.parse(selected.service.objectHash),
|
|
1954
1874
|
validFromBlock
|
|
1875
|
+
};
|
|
1876
|
+
const service = createPublisherService({
|
|
1877
|
+
...serviceInput,
|
|
1878
|
+
authorizationChainId: options.reader.chainId,
|
|
1879
|
+
sources: selected.service.body.events.map((event) => event.source),
|
|
1880
|
+
maxPositionsPerWake: input.maxPositionsPerWake ?? selected.service.body.limits.maxPositionsPerWake
|
|
1955
1881
|
});
|
|
1956
1882
|
const resolvedIntent = await persistMutationIntent(options.intents, {
|
|
1957
1883
|
chainId: options.reader.chainId,
|
|
@@ -1995,11 +1921,9 @@ function createSubscriberHandshakeClient(options) {
|
|
|
1995
1921
|
return readySnapshot(await options.reader.sync());
|
|
1996
1922
|
}
|
|
1997
1923
|
async function serviceAt(current) {
|
|
1998
|
-
return requireService(
|
|
1999
|
-
await current.readModel.discoverPublisherService(serviceQuery(options.reader, current, serviceId))
|
|
2000
|
-
);
|
|
1924
|
+
return requireService(await current.readModel.discoverPublisherService(serviceQuery(options.reader, current, serviceId)));
|
|
2001
1925
|
}
|
|
2002
|
-
async function
|
|
1926
|
+
async function lookup2(subscriptionId) {
|
|
2003
1927
|
const current = await options.reader.sync();
|
|
2004
1928
|
return await lookupAt(current, subscriptionId);
|
|
2005
1929
|
}
|
|
@@ -2014,29 +1938,21 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2014
1938
|
const revision = input.revision ?? 1;
|
|
2015
1939
|
if (revision === 1) {
|
|
2016
1940
|
if (input.supersedes !== void 0) {
|
|
2017
|
-
throw new EventKitHandshakeClientError(
|
|
2018
|
-
"subscription-invalid",
|
|
2019
|
-
"Subscription revision 1 must not declare supersedes"
|
|
2020
|
-
);
|
|
1941
|
+
throw new EventKitHandshakeClientError("subscription-invalid", "Subscription revision 1 must not declare supersedes");
|
|
2021
1942
|
}
|
|
2022
|
-
if (input.subscriptionId === void 0)
|
|
1943
|
+
if (input.subscriptionId === void 0)
|
|
1944
|
+
return;
|
|
2023
1945
|
const view2 = await lookupAt(current, input.subscriptionId);
|
|
2024
1946
|
if (!view2.ok) {
|
|
2025
1947
|
throw new EventKitHandshakeClientError("service-unavailable", `Cannot validate subscription identity: ${view2.reason}`);
|
|
2026
1948
|
}
|
|
2027
1949
|
if (view2.subscriptions.some((item) => item.subscriptionId === input.subscriptionId)) {
|
|
2028
|
-
throw new EventKitHandshakeClientError(
|
|
2029
|
-
"subscription-invalid",
|
|
2030
|
-
"Subscription revision 1 would duplicate an existing finalized stable identity"
|
|
2031
|
-
);
|
|
1950
|
+
throw new EventKitHandshakeClientError("subscription-invalid", "Subscription revision 1 would duplicate an existing finalized stable identity");
|
|
2032
1951
|
}
|
|
2033
1952
|
return;
|
|
2034
1953
|
}
|
|
2035
1954
|
if (input.subscriptionId === void 0 || input.supersedes === void 0) {
|
|
2036
|
-
throw new EventKitHandshakeClientError(
|
|
2037
|
-
"subscription-invalid",
|
|
2038
|
-
"A replacement subscription requires subscriptionId and supersedes"
|
|
2039
|
-
);
|
|
1955
|
+
throw new EventKitHandshakeClientError("subscription-invalid", "A replacement subscription requires subscriptionId and supersedes");
|
|
2040
1956
|
}
|
|
2041
1957
|
const view = await lookupAt(current, input.subscriptionId);
|
|
2042
1958
|
if (!view.ok) {
|
|
@@ -2044,24 +1960,15 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2044
1960
|
}
|
|
2045
1961
|
const lifecycles = view.subscriptions.filter((item) => item.subscriptionId === input.subscriptionId);
|
|
2046
1962
|
if (lifecycles.length !== 1) {
|
|
2047
|
-
throw new EventKitHandshakeClientError(
|
|
2048
|
-
"subscription-invalid",
|
|
2049
|
-
"Replacement subscription does not resolve one finalized stable identity"
|
|
2050
|
-
);
|
|
1963
|
+
throw new EventKitHandshakeClientError("subscription-invalid", "Replacement subscription does not resolve one finalized stable identity");
|
|
2051
1964
|
}
|
|
2052
1965
|
if (lifecycles[0].state === "rejected") {
|
|
2053
|
-
throw new EventKitHandshakeClientError(
|
|
2054
|
-
"subscription-invalid",
|
|
2055
|
-
"Replacement subscription cannot extend a rejected finalized revision chain"
|
|
2056
|
-
);
|
|
1966
|
+
throw new EventKitHandshakeClientError("subscription-invalid", "Replacement subscription cannot extend a rejected finalized revision chain");
|
|
2057
1967
|
}
|
|
2058
1968
|
const highestRevision = Math.max(...lifecycles[0].tuples.map((tuple) => tuple.subscription.body.revision));
|
|
2059
1969
|
const latest = lifecycles[0].tuples.filter((tuple) => tuple.subscription.body.revision === highestRevision);
|
|
2060
1970
|
if (latest.length !== 1 || revision !== highestRevision + 1 || latest[0].subscription.objectHash !== input.supersedes) {
|
|
2061
|
-
throw new EventKitHandshakeClientError(
|
|
2062
|
-
"subscription-invalid",
|
|
2063
|
-
"Replacement subscription revision and supersedes must extend the exact finalized chain tip"
|
|
2064
|
-
);
|
|
1971
|
+
throw new EventKitHandshakeClientError("subscription-invalid", "Replacement subscription revision and supersedes must extend the exact finalized chain tip");
|
|
2065
1972
|
}
|
|
2066
1973
|
}
|
|
2067
1974
|
async function publishSubscriberSubscriptionIntent(intent, intentDisposition) {
|
|
@@ -2090,23 +1997,13 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2090
1997
|
await assertPublicationSource(options.publication, subscriber);
|
|
2091
1998
|
const grantHash = HashZod4.parse(grant);
|
|
2092
1999
|
const requestFingerprint = await exactMutationRequestFingerprint({ grant: grantHash });
|
|
2093
|
-
let resolvedIntent = await loadMutationIntent(
|
|
2094
|
-
options.intents,
|
|
2095
|
-
options.reader,
|
|
2096
|
-
"subscriber-grant-revoke",
|
|
2097
|
-
grantHash,
|
|
2098
|
-
subscriber,
|
|
2099
|
-
requestFingerprint
|
|
2100
|
-
);
|
|
2000
|
+
let resolvedIntent = await loadMutationIntent(options.intents, options.reader, "subscriber-grant-revoke", grantHash, subscriber, requestFingerprint);
|
|
2101
2001
|
let intentDisposition = "resumed";
|
|
2102
2002
|
if (resolvedIntent === void 0) {
|
|
2103
2003
|
const current = await snapshot();
|
|
2104
2004
|
const view = await current.readModel.subscriber({ ...serviceQuery(options.reader, current, serviceId), subscriber });
|
|
2105
2005
|
if (!view.ok || !tupleHashes(view).grants.has(grantHash)) {
|
|
2106
|
-
throw new EventKitHandshakeClientError(
|
|
2107
|
-
"subscription-not-found",
|
|
2108
|
-
"Grant is not present in this subscriber finalization view"
|
|
2109
|
-
);
|
|
2006
|
+
throw new EventKitHandshakeClientError("subscription-not-found", "Grant is not present in this subscriber finalization view");
|
|
2110
2007
|
}
|
|
2111
2008
|
const persisted = await persistMutationIntent(options.intents, {
|
|
2112
2009
|
chainId: options.reader.chainId,
|
|
@@ -2139,23 +2036,13 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2139
2036
|
await assertPublicationSource(options.publication, subscriber);
|
|
2140
2037
|
const subscriptionHash = HashZod4.parse(subscription);
|
|
2141
2038
|
const requestFingerprint = await exactMutationRequestFingerprint({ subscription: subscriptionHash });
|
|
2142
|
-
let resolvedIntent = await loadMutationIntent(
|
|
2143
|
-
options.intents,
|
|
2144
|
-
options.reader,
|
|
2145
|
-
"subscriber-subscription-revoke",
|
|
2146
|
-
subscriptionHash,
|
|
2147
|
-
subscriber,
|
|
2148
|
-
requestFingerprint
|
|
2149
|
-
);
|
|
2039
|
+
let resolvedIntent = await loadMutationIntent(options.intents, options.reader, "subscriber-subscription-revoke", subscriptionHash, subscriber, requestFingerprint);
|
|
2150
2040
|
let intentDisposition = "resumed";
|
|
2151
2041
|
if (resolvedIntent === void 0) {
|
|
2152
2042
|
const current = await snapshot();
|
|
2153
2043
|
const view = await current.readModel.subscriber({ ...serviceQuery(options.reader, current, serviceId), subscriber });
|
|
2154
2044
|
if (!view.ok || !tupleHashes(view).subscriptions.has(subscriptionHash)) {
|
|
2155
|
-
throw new EventKitHandshakeClientError(
|
|
2156
|
-
"subscription-not-found",
|
|
2157
|
-
"Subscription is not present in this subscriber finalization view"
|
|
2158
|
-
);
|
|
2045
|
+
throw new EventKitHandshakeClientError("subscription-not-found", "Subscription is not present in this subscriber finalization view");
|
|
2159
2046
|
}
|
|
2160
2047
|
const persisted = await persistMutationIntent(options.intents, {
|
|
2161
2048
|
chainId: options.reader.chainId,
|
|
@@ -2192,19 +2079,18 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2192
2079
|
const subscriptionHash = HashZod4.parse(subscription);
|
|
2193
2080
|
const current = await options.reader.sync();
|
|
2194
2081
|
const view = await lookupAt(current);
|
|
2195
|
-
if (!view.ok)
|
|
2082
|
+
if (!view.ok)
|
|
2083
|
+
return view;
|
|
2196
2084
|
const matches = view.subscriptions.flatMap((lifecycle) => lifecycle.tuples.filter((tuple) => tuple.subscription.objectHash === subscriptionHash).map((tuple) => ({ lifecycle, tuple })));
|
|
2197
2085
|
if (matches.length > 1) {
|
|
2198
|
-
throw new EventKitHandshakeClientError(
|
|
2199
|
-
"subscription-invalid",
|
|
2200
|
-
"Exact subscription hash appears more than once in the subscriber view"
|
|
2201
|
-
);
|
|
2086
|
+
throw new EventKitHandshakeClientError("subscription-invalid", "Exact subscription hash appears more than once in the subscriber view");
|
|
2202
2087
|
}
|
|
2203
|
-
if (matches.length === 0)
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2088
|
+
if (matches.length === 0)
|
|
2089
|
+
return Object.freeze({
|
|
2090
|
+
found: false,
|
|
2091
|
+
lookup: view,
|
|
2092
|
+
ok: true
|
|
2093
|
+
});
|
|
2208
2094
|
return Object.freeze({
|
|
2209
2095
|
found: true,
|
|
2210
2096
|
lifecycle: matches[0].lifecycle,
|
|
@@ -2217,36 +2103,22 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2217
2103
|
await assertPublicationSource(options.publication, subscriber);
|
|
2218
2104
|
const operationId = HandshakeIdZod3.parse(input.operationId);
|
|
2219
2105
|
const requestFingerprint = await subscriberSubscriptionRequestFingerprint(input);
|
|
2220
|
-
const priorIntent = await loadMutationIntent(
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
"subscriber-subscription-subscribe",
|
|
2224
|
-
operationId,
|
|
2225
|
-
subscriber,
|
|
2226
|
-
requestFingerprint
|
|
2227
|
-
);
|
|
2228
|
-
if (priorIntent !== void 0) return await publishSubscriberSubscriptionIntent(priorIntent, "resumed");
|
|
2106
|
+
const priorIntent = await loadMutationIntent(options.intents, options.reader, "subscriber-subscription-subscribe", operationId, subscriber, requestFingerprint);
|
|
2107
|
+
if (priorIntent !== void 0)
|
|
2108
|
+
return await publishSubscriberSubscriptionIntent(priorIntent, "resumed");
|
|
2229
2109
|
const current = await snapshot();
|
|
2230
2110
|
const selected = await serviceAt(current);
|
|
2231
2111
|
if (input.expiresAtBlock - current.finalizedHead <= 3) {
|
|
2232
|
-
throw new EventKitHandshakeClientError(
|
|
2233
|
-
"subscription-invalid",
|
|
2234
|
-
"Requested subscription expiry must leave finalized blocks for tuple publication, acceptance, and activation"
|
|
2235
|
-
);
|
|
2112
|
+
throw new EventKitHandshakeClientError("subscription-invalid", "Requested subscription expiry must leave finalized blocks for tuple publication, acceptance, and activation");
|
|
2236
2113
|
}
|
|
2237
2114
|
await validateRevision(current, input);
|
|
2238
|
-
const
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
"Requested maxBlocksPerWake exceeds the selected publisher service limit"
|
|
2243
|
-
);
|
|
2115
|
+
const maximum = selected.service.body.limits.maxPositionsPerWake;
|
|
2116
|
+
const maxPositionsPerWake = input.maxPositionsPerWake ?? maximum;
|
|
2117
|
+
if (maxPositionsPerWake > maximum) {
|
|
2118
|
+
throw new EventKitHandshakeClientError("subscription-invalid", "Requested maxPositionsPerWake exceeds the selected publisher service limit");
|
|
2244
2119
|
}
|
|
2245
2120
|
if (input.expiresAtBlock > current.finalizedHead + selected.service.body.limits.maxSubscriptionLifetimeBlocks) {
|
|
2246
|
-
throw new EventKitHandshakeClientError(
|
|
2247
|
-
"subscription-invalid",
|
|
2248
|
-
"Requested subscription expiry exceeds the selected publisher service lifetime limit"
|
|
2249
|
-
);
|
|
2121
|
+
throw new EventKitHandshakeClientError("subscription-invalid", "Requested subscription expiry exceeds the selected publisher service lifetime limit");
|
|
2250
2122
|
}
|
|
2251
2123
|
const grant = createWakeGrant({
|
|
2252
2124
|
deploymentId: input.deploymentId,
|
|
@@ -2255,19 +2127,11 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2255
2127
|
...input.queueId === void 0 || input.queueId === null ? {} : { queueId: input.queueId }
|
|
2256
2128
|
});
|
|
2257
2129
|
const grantHash = await handshakeObjectHash2(grant);
|
|
2258
|
-
const plaintext =
|
|
2259
|
-
|
|
2260
|
-
challengeSecret: input.challengeSecret,
|
|
2261
|
-
deploymentId: input.deploymentId,
|
|
2262
|
-
endpoint: input.endpoint,
|
|
2263
|
-
maxBlocksPerWake,
|
|
2264
|
-
queueId: input.queueId
|
|
2265
|
-
});
|
|
2266
|
-
const subscription = await createEncryptedWakeSubscription({
|
|
2130
|
+
const plaintext = subscriberPlaintext(selected.service.body, input, maxPositionsPerWake);
|
|
2131
|
+
const subscriptionInput = {
|
|
2267
2132
|
chainId: options.reader.chainId,
|
|
2268
2133
|
expiresAtBlock: input.expiresAtBlock,
|
|
2269
2134
|
grant: grantHash,
|
|
2270
|
-
plaintext,
|
|
2271
2135
|
publisher: options.reader.publisher,
|
|
2272
2136
|
publisherKey: {
|
|
2273
2137
|
encryptionKey: HashZod4.parse(selected.key.objectHash),
|
|
@@ -2279,7 +2143,8 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2279
2143
|
subscriber,
|
|
2280
2144
|
...input.subscriptionId === void 0 ? {} : { subscriptionId: input.subscriptionId },
|
|
2281
2145
|
...input.supersedes === void 0 ? {} : { supersedes: input.supersedes }
|
|
2282
|
-
}
|
|
2146
|
+
};
|
|
2147
|
+
const subscription = await createEncryptedWakeSubscription({ ...subscriptionInput, plaintext });
|
|
2283
2148
|
const resolvedIntent = await persistMutationIntent(options.intents, {
|
|
2284
2149
|
chainId: options.reader.chainId,
|
|
2285
2150
|
encryptionKey: selected.key.body,
|
|
@@ -2298,20 +2163,1430 @@ function createSubscriberHandshakeClient(options) {
|
|
|
2298
2163
|
return await publishSubscriberSubscriptionIntent(resolvedIntent.intent, resolvedIntent.disposition);
|
|
2299
2164
|
},
|
|
2300
2165
|
async subscriptions(subscriptionId) {
|
|
2301
|
-
return await
|
|
2166
|
+
return await lookup2(subscriptionId);
|
|
2302
2167
|
}
|
|
2303
2168
|
};
|
|
2304
2169
|
return Object.freeze(client);
|
|
2305
2170
|
}
|
|
2171
|
+
function subscriberPlaintext(service, input, maxPositionsPerWake) {
|
|
2172
|
+
const plaintextInput = {
|
|
2173
|
+
audience: input.audience,
|
|
2174
|
+
challengeSecret: input.challengeSecret,
|
|
2175
|
+
deploymentId: input.deploymentId,
|
|
2176
|
+
endpoint: input.endpoint,
|
|
2177
|
+
queueId: input.queueId
|
|
2178
|
+
};
|
|
2179
|
+
const source = input.source;
|
|
2180
|
+
if (!service.events.some((event) => canonicalJson(event.source) === canonicalJson(source))) {
|
|
2181
|
+
throw new Error("Requested source is not advertised by the selected service");
|
|
2182
|
+
}
|
|
2183
|
+
return createWakeSubscriptionPlaintext({
|
|
2184
|
+
...plaintextInput,
|
|
2185
|
+
source,
|
|
2186
|
+
fromPosition: input.fromPosition,
|
|
2187
|
+
maxPositionsPerWake
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2306
2190
|
|
|
2307
|
-
// src/
|
|
2308
|
-
import {
|
|
2191
|
+
// src/runtime/boundedFetch.ts
|
|
2192
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
2193
|
+
|
|
2194
|
+
// src/runtime/abort.ts
|
|
2195
|
+
import { clearTimeout, setTimeout } from "node:timers";
|
|
2196
|
+
function publisherAbortScope(signals, timeoutMs) {
|
|
2197
|
+
const controller = new AbortController();
|
|
2198
|
+
const abort = () => controller.abort();
|
|
2199
|
+
const timer = timeoutMs === void 0 ? void 0 : setTimeout(abort, timeoutMs);
|
|
2200
|
+
timer?.unref();
|
|
2201
|
+
for (const signal of signals) {
|
|
2202
|
+
if (signal.aborted) controller.abort();
|
|
2203
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
2204
|
+
}
|
|
2205
|
+
return {
|
|
2206
|
+
signal: controller.signal,
|
|
2207
|
+
dispose() {
|
|
2208
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2209
|
+
for (const signal of signals) signal.removeEventListener("abort", abort);
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// src/runtime/boundedFetch.ts
|
|
2215
|
+
function createBoundedPublisherFetch(options = {}) {
|
|
2216
|
+
const owner = new AbortController();
|
|
2217
|
+
const timeout = options.requestTimeoutMs ?? 1e4;
|
|
2218
|
+
const maximum = options.maxResponseBytes ?? 16 * 1024 * 1024;
|
|
2219
|
+
const concurrency = options.maxConcurrency ?? 16;
|
|
2220
|
+
if (!Number.isSafeInteger(timeout) || timeout < 100 || timeout > 6e4 || !Number.isSafeInteger(maximum) || maximum < 1 || maximum > 64 * 1024 * 1024 || !Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 64)
|
|
2221
|
+
throw new Error("Invalid publisher fetch limits");
|
|
2222
|
+
const active = /* @__PURE__ */ new Set();
|
|
2223
|
+
const nativeFetch = globalThis.fetch.bind(globalThis);
|
|
2224
|
+
const fetchFn = (input, init) => {
|
|
2225
|
+
if (active.size >= concurrency)
|
|
2226
|
+
return Promise.reject(new Error("Publisher HTTP concurrency limit exceeded"));
|
|
2227
|
+
const operation = (async () => {
|
|
2228
|
+
const signals = [
|
|
2229
|
+
owner.signal,
|
|
2230
|
+
...options.signal === void 0 ? [] : [options.signal],
|
|
2231
|
+
...init?.signal == null ? [] : [init.signal],
|
|
2232
|
+
...input instanceof Request ? [input.signal] : []
|
|
2233
|
+
];
|
|
2234
|
+
const scope = publisherAbortScope(signals, timeout);
|
|
2235
|
+
try {
|
|
2236
|
+
scope.signal.throwIfAborted();
|
|
2237
|
+
const response = await nativeFetch(input, {
|
|
2238
|
+
...init,
|
|
2239
|
+
redirect: "error",
|
|
2240
|
+
signal: scope.signal
|
|
2241
|
+
});
|
|
2242
|
+
if (response.body === null)
|
|
2243
|
+
return response;
|
|
2244
|
+
const reader = response.body.getReader();
|
|
2245
|
+
const chunks = [];
|
|
2246
|
+
let size = 0;
|
|
2247
|
+
try {
|
|
2248
|
+
for (; ; ) {
|
|
2249
|
+
const chunk = await reader.read();
|
|
2250
|
+
if (chunk.done)
|
|
2251
|
+
break;
|
|
2252
|
+
const bytes = chunk.value;
|
|
2253
|
+
if (!(bytes instanceof Uint8Array)) throw new Error("Publisher HTTP body returned a non-byte chunk");
|
|
2254
|
+
size += bytes.byteLength;
|
|
2255
|
+
if (size > maximum) {
|
|
2256
|
+
await reader.cancel();
|
|
2257
|
+
throw new Error("Publisher HTTP body exceeds its bound");
|
|
2258
|
+
}
|
|
2259
|
+
chunks.push(bytes);
|
|
2260
|
+
}
|
|
2261
|
+
} finally {
|
|
2262
|
+
reader.releaseLock();
|
|
2263
|
+
}
|
|
2264
|
+
const headers = new Headers(response.headers);
|
|
2265
|
+
headers.delete("content-encoding");
|
|
2266
|
+
headers.delete("transfer-encoding");
|
|
2267
|
+
headers.set("content-length", String(size));
|
|
2268
|
+
return new Response(Buffer2.concat(chunks), {
|
|
2269
|
+
status: response.status,
|
|
2270
|
+
statusText: response.statusText,
|
|
2271
|
+
headers
|
|
2272
|
+
});
|
|
2273
|
+
} finally {
|
|
2274
|
+
scope.dispose();
|
|
2275
|
+
}
|
|
2276
|
+
})();
|
|
2277
|
+
active.add(operation);
|
|
2278
|
+
void operation.then(() => active.delete(operation), () => active.delete(operation));
|
|
2279
|
+
return operation;
|
|
2280
|
+
};
|
|
2281
|
+
return {
|
|
2282
|
+
fetchFn,
|
|
2283
|
+
async close() {
|
|
2284
|
+
owner.abort();
|
|
2285
|
+
await Promise.allSettled(active);
|
|
2286
|
+
},
|
|
2287
|
+
get activeRequests() {
|
|
2288
|
+
return active.size;
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
function createBoundedPublisherEthersTransport(options = {}) {
|
|
2293
|
+
const bounded = createBoundedPublisherFetch(options);
|
|
2294
|
+
const getUrl = async (request2, cancellation) => {
|
|
2295
|
+
const controller = new AbortController();
|
|
2296
|
+
cancellation?.addListener(() => controller.abort());
|
|
2297
|
+
cancellation?.checkSignal();
|
|
2298
|
+
const response = await bounded.fetchFn(request2.url, {
|
|
2299
|
+
body: request2.body === null ? void 0 : Buffer2.from(request2.body),
|
|
2300
|
+
headers: request2.headers,
|
|
2301
|
+
method: request2.method,
|
|
2302
|
+
signal: controller.signal
|
|
2303
|
+
});
|
|
2304
|
+
return {
|
|
2305
|
+
body: new Uint8Array(await response.arrayBuffer()),
|
|
2306
|
+
headers: Object.fromEntries(response.headers),
|
|
2307
|
+
statusCode: response.status,
|
|
2308
|
+
statusMessage: response.statusText
|
|
2309
|
+
};
|
|
2310
|
+
};
|
|
2311
|
+
return {
|
|
2312
|
+
getUrl,
|
|
2313
|
+
async close() {
|
|
2314
|
+
await bounded.close();
|
|
2315
|
+
},
|
|
2316
|
+
get activeRequests() {
|
|
2317
|
+
return bounded.activeRequests;
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
// src/runtime/handshakeDiscovery.ts
|
|
2323
|
+
import { decryptWakeSubscription, sourceWakeFilterHash } from "@xyo-network/event-kit-protocol";
|
|
2324
|
+
import { EndpointChallengeResponseZod as EndpointChallengeResponseZod2 } from "@xyo-network/event-kit-schemas";
|
|
2325
|
+
function createPublisherHandshakeDiscovery(options) {
|
|
2326
|
+
const clock = options.nowSeconds ?? (() => Math.floor(Date.now() / 1e3));
|
|
2327
|
+
const owner = new AbortController();
|
|
2328
|
+
let accepting;
|
|
2329
|
+
let viewing;
|
|
2330
|
+
let status = {
|
|
2331
|
+
authorizationChainId: options.authorizationChainId,
|
|
2332
|
+
keyLoaded: false,
|
|
2333
|
+
activeKeyIds: [],
|
|
2334
|
+
pendingSubscriptions: 0,
|
|
2335
|
+
rejectedSubscriptions: 0,
|
|
2336
|
+
challengeFailures: 0
|
|
2337
|
+
};
|
|
2338
|
+
async function activeView(signal) {
|
|
2339
|
+
signal.throwIfAborted();
|
|
2340
|
+
viewing ??= options.client.subscriptions().finally(() => {
|
|
2341
|
+
viewing = void 0;
|
|
2342
|
+
});
|
|
2343
|
+
const view = await viewing;
|
|
2344
|
+
signal.throwIfAborted();
|
|
2345
|
+
if (!view.ok || view.indexedThrough !== view.finalizedHead)
|
|
2346
|
+
throw new Error("Publisher subscription view is unavailable or stale");
|
|
2347
|
+
status = {
|
|
2348
|
+
...status,
|
|
2349
|
+
finalizedHead: view.finalizedHead,
|
|
2350
|
+
indexedThrough: view.indexedThrough,
|
|
2351
|
+
pendingSubscriptions: view.subscriptions.filter((item) => item.state === "pending").length,
|
|
2352
|
+
rejectedSubscriptions: view.rejections.length
|
|
2353
|
+
};
|
|
2354
|
+
return view;
|
|
2355
|
+
}
|
|
2356
|
+
async function acceptCandidate(subscriber, subscription, signal) {
|
|
2357
|
+
try {
|
|
2358
|
+
let pending = options.state.get("outbound-challenges", subscription);
|
|
2359
|
+
if (pending !== void 0 && pending.response === void 0 && clock() >= pending.expiresAt)
|
|
2360
|
+
pending = void 0;
|
|
2361
|
+
if (pending === void 0) {
|
|
2362
|
+
const issued = await options.client.issueChallenge({
|
|
2363
|
+
issuedAt: clock(),
|
|
2364
|
+
lifetimeSeconds: 300,
|
|
2365
|
+
subscriber,
|
|
2366
|
+
subscription
|
|
2367
|
+
});
|
|
2368
|
+
pending = {
|
|
2369
|
+
challengeHash: issued.challengeHash,
|
|
2370
|
+
subscriber,
|
|
2371
|
+
subscription,
|
|
2372
|
+
expiresAt: issued.challenge.expiresAt
|
|
2373
|
+
};
|
|
2374
|
+
options.state.set("outbound-challenges", subscription, pending);
|
|
2375
|
+
}
|
|
2376
|
+
if (pending.response === void 0) {
|
|
2377
|
+
const begun = await options.client.beginChallenge({ challengeHash: pending.challengeHash, nowSeconds: clock });
|
|
2378
|
+
if (begun.kind !== "started")
|
|
2379
|
+
return;
|
|
2380
|
+
signal.throwIfAborted();
|
|
2381
|
+
const response2 = await options.transport.post(begun.target.endpoint, begun.challenge, signal);
|
|
2382
|
+
if (response2.status !== 200)
|
|
2383
|
+
throw new Error("Endpoint challenge was rejected");
|
|
2384
|
+
pending = { ...pending, response: EndpointChallengeResponseZod2.parse(response2.body) };
|
|
2385
|
+
options.state.set("outbound-challenges", subscription, pending);
|
|
2386
|
+
}
|
|
2387
|
+
signal.throwIfAborted();
|
|
2388
|
+
const response = pending.response;
|
|
2389
|
+
if (response === void 0)
|
|
2390
|
+
throw new Error("Endpoint proof is unavailable");
|
|
2391
|
+
await options.client.completeChallengeAndAccept({
|
|
2392
|
+
...pending,
|
|
2393
|
+
response,
|
|
2394
|
+
nowSeconds: clock
|
|
2395
|
+
});
|
|
2396
|
+
} catch {
|
|
2397
|
+
if (signal.aborted)
|
|
2398
|
+
return;
|
|
2399
|
+
const failures = options.state.get("challenge-failures", subscription) ?? 0;
|
|
2400
|
+
options.state.set("challenge-failures", subscription, failures + 1);
|
|
2401
|
+
const pending = options.state.get("outbound-challenges", subscription);
|
|
2402
|
+
if (pending !== void 0 && clock() >= pending.expiresAt)
|
|
2403
|
+
options.state.remove("outbound-challenges", subscription);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
async function accept(signal) {
|
|
2407
|
+
signal.throwIfAborted();
|
|
2408
|
+
const now = clock();
|
|
2409
|
+
if (now < (options.state.get("challenge-rate", "next-pass") ?? 0))
|
|
2410
|
+
return;
|
|
2411
|
+
options.state.set("challenge-rate", "next-pass", now + 10);
|
|
2412
|
+
const found = await options.client.acceptanceCandidates();
|
|
2413
|
+
if (!found.ok)
|
|
2414
|
+
throw new Error("Publisher acceptance candidates are unavailable");
|
|
2415
|
+
const offset = options.state.get("challenge-rate", "offset") ?? 0;
|
|
2416
|
+
const candidates = found.candidates;
|
|
2417
|
+
let attempted = 0;
|
|
2418
|
+
for (let index = 0; index < Math.min(candidates.length, 128) && attempted < 4; index += 1) {
|
|
2419
|
+
const candidate = candidates[(offset + index) % candidates.length];
|
|
2420
|
+
if (candidate === void 0)
|
|
2421
|
+
break;
|
|
2422
|
+
options.state.set("challenge-rate", "offset", (offset + index + 1) % candidates.length);
|
|
2423
|
+
const next = options.state.get("challenge-controller-rate", candidate.subscriber) ?? 0;
|
|
2424
|
+
if (now < next)
|
|
2425
|
+
continue;
|
|
2426
|
+
options.state.set("challenge-controller-rate", candidate.subscriber, now + 30);
|
|
2427
|
+
attempted += 1;
|
|
2428
|
+
signal.throwIfAborted();
|
|
2429
|
+
await acceptCandidate(candidate.subscriber, candidate.tuple.subscription.objectHash, signal);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
return {
|
|
2433
|
+
async discover(signal) {
|
|
2434
|
+
const view = await activeView(signal);
|
|
2435
|
+
const key = view.hasCurrentAdvertisement ? view.key : void 0;
|
|
2436
|
+
const loaded = key === void 0 ? void 0 : await options.keys.getPrivateKey({
|
|
2437
|
+
encryptionKey: key.objectHash,
|
|
2438
|
+
keyId: key.body.keyId,
|
|
2439
|
+
publisher: options.publisher
|
|
2440
|
+
});
|
|
2441
|
+
status = {
|
|
2442
|
+
...status,
|
|
2443
|
+
keyLoaded: loaded !== void 0,
|
|
2444
|
+
activeKeyIds: key === void 0 ? [] : [key.body.keyId]
|
|
2445
|
+
};
|
|
2446
|
+
const subscriptions = [];
|
|
2447
|
+
const active = view.subscriptions.filter((item) => item.state === "active");
|
|
2448
|
+
if (active.length > (options.maxSubscriptions ?? 64))
|
|
2449
|
+
throw new Error("Publisher active subscription limit exceeded");
|
|
2450
|
+
for (const lifecycle of active) {
|
|
2451
|
+
if (lifecycle.state !== "active" || lifecycle.activeFromBlock === void 0)
|
|
2452
|
+
continue;
|
|
2453
|
+
try {
|
|
2454
|
+
const tuple = lifecycle.tuples.find((item) => item.subscription.objectHash === lifecycle.selectedSubscription);
|
|
2455
|
+
if (tuple === void 0)
|
|
2456
|
+
throw new Error("Active publisher subscription has no selected tuple");
|
|
2457
|
+
const encrypted = tuple.subscription.body;
|
|
2458
|
+
if (encrypted.chainId !== options.authorizationChainId)
|
|
2459
|
+
throw new Error("Subscription authorization chain mismatch");
|
|
2460
|
+
const publisherKey = await options.keys.getPrivateKey({
|
|
2461
|
+
encryptionKey: encrypted.encryptionKey,
|
|
2462
|
+
keyId: encrypted.keyId,
|
|
2463
|
+
publisher: options.publisher
|
|
2464
|
+
});
|
|
2465
|
+
if (publisherKey === void 0)
|
|
2466
|
+
throw new Error("Subscription encryption key is unavailable");
|
|
2467
|
+
const plaintext = await decryptWakeSubscription({
|
|
2468
|
+
publisherKey,
|
|
2469
|
+
subscriber: tuple.subscriber,
|
|
2470
|
+
subscription: encrypted
|
|
2471
|
+
});
|
|
2472
|
+
options.transport.assertAllowed(plaintext.endpoint);
|
|
2473
|
+
subscriptions.push({
|
|
2474
|
+
subscriptionHash: tuple.subscription.objectHash,
|
|
2475
|
+
authorizationChainId: options.authorizationChainId,
|
|
2476
|
+
controller: tuple.subscriber,
|
|
2477
|
+
audience: plaintext.audience,
|
|
2478
|
+
endpoint: plaintext.endpoint,
|
|
2479
|
+
deploymentId: plaintext.deploymentId,
|
|
2480
|
+
...plaintext.queueId === null ? {} : { queueId: plaintext.queueId },
|
|
2481
|
+
source: plaintext.filter.source,
|
|
2482
|
+
fromPosition: plaintext.filter.fromPosition,
|
|
2483
|
+
maxPositionsPerWake: plaintext.filter.maxPositionsPerWake,
|
|
2484
|
+
filterHash: await sourceWakeFilterHash(plaintext.filter)
|
|
2485
|
+
});
|
|
2486
|
+
} catch {
|
|
2487
|
+
status = { ...status, rejectedSubscriptions: status.rejectedSubscriptions + 1 };
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
return subscriptions;
|
|
2491
|
+
},
|
|
2492
|
+
async isActive(subscription, signal) {
|
|
2493
|
+
const view = await activeView(signal);
|
|
2494
|
+
return view.subscriptions.some((item) => item.state === "active" && item.selectedSubscription === subscription.subscriptionHash && item.tuples.some((tuple) => tuple.subscription.objectHash === subscription.subscriptionHash && tuple.subscriber === subscription.controller));
|
|
2495
|
+
},
|
|
2496
|
+
async acceptPending(signal) {
|
|
2497
|
+
if (accepting === void 0) {
|
|
2498
|
+
const scope = publisherAbortScope([owner.signal, signal]);
|
|
2499
|
+
accepting = accept(scope.signal).finally(() => {
|
|
2500
|
+
scope.dispose();
|
|
2501
|
+
accepting = void 0;
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
await accepting;
|
|
2505
|
+
},
|
|
2506
|
+
async close() {
|
|
2507
|
+
await Promise.allSettled([accepting, viewing].filter((item) => item !== void 0));
|
|
2508
|
+
},
|
|
2509
|
+
status() {
|
|
2510
|
+
return { ...status, challengeFailures: options.state.entries("challenge-failures").reduce((sum, item) => sum + item.value, 0) };
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// src/runtime/httpsTransport.ts
|
|
2516
|
+
import { Buffer as Buffer3 } from "node:buffer";
|
|
2517
|
+
import { lookup } from "node:dns/promises";
|
|
2518
|
+
import { request } from "node:https";
|
|
2519
|
+
import { BlockList, isIP } from "node:net";
|
|
2520
|
+
import { z as z2 } from "zod";
|
|
2521
|
+
|
|
2522
|
+
// src/runtime/sources.ts
|
|
2523
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2524
|
+
import { canonicalJson as canonicalJson2 } from "@xyo-network/event-kit-protocol";
|
|
2525
|
+
import { completedUtcIntervalPosition, WakeSourceZod } from "@xyo-network/event-kit-schemas";
|
|
2526
|
+
import { PayloadBuilder } from "@xyo-network/sdk";
|
|
2527
|
+
import { asXL1BlockNumber } from "@xyo-network/xl1-sdk";
|
|
2528
|
+
import { FetchRequest, JsonRpcProvider } from "ethers";
|
|
2529
|
+
function publisherSourceKey(source) {
|
|
2530
|
+
return createHash3("sha256").update(canonicalJson2(WakeSourceZod.parse(source))).digest("hex");
|
|
2531
|
+
}
|
|
2532
|
+
async function boundedPublisherOperation(operation, signal, timeoutMs = 1e4) {
|
|
2533
|
+
const scope = publisherAbortScope([signal], timeoutMs);
|
|
2534
|
+
const combined = scope.signal;
|
|
2535
|
+
let rejectAbort;
|
|
2536
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
2537
|
+
rejectAbort = () => reject(new Error("Publisher operation aborted or timed out"));
|
|
2538
|
+
combined.addEventListener("abort", rejectAbort, { once: true });
|
|
2539
|
+
});
|
|
2540
|
+
try {
|
|
2541
|
+
combined.throwIfAborted();
|
|
2542
|
+
return await Promise.race([operation, aborted]);
|
|
2543
|
+
} finally {
|
|
2544
|
+
if (rejectAbort !== void 0)
|
|
2545
|
+
combined.removeEventListener("abort", rejectAbort);
|
|
2546
|
+
scope.dispose();
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
function createXl1PublisherSource(options) {
|
|
2550
|
+
const source = WakeSourceZod.parse(options.source);
|
|
2551
|
+
if (source.kind !== "xl1-finalized")
|
|
2552
|
+
throw new Error("Expected an XL1 finalized source");
|
|
2553
|
+
return {
|
|
2554
|
+
source,
|
|
2555
|
+
async head(signal) {
|
|
2556
|
+
const chainId = String(await boundedPublisherOperation(Promise.resolve(options.viewer.chainId()), signal));
|
|
2557
|
+
if (chainId !== source.chainId)
|
|
2558
|
+
throw new Error("XL1 source chain identity changed");
|
|
2559
|
+
const position = Number(await boundedPublisherOperation(Promise.resolve(options.viewer.finalization.headNumber()), signal));
|
|
2560
|
+
if (!Number.isSafeInteger(position) || position < 0)
|
|
2561
|
+
throw new Error("XL1 finalized head is invalid");
|
|
2562
|
+
const block = await boundedPublisherOperation(Promise.resolve(options.viewer.block.blockByNumber(asXL1BlockNumber(position, true))), signal);
|
|
2563
|
+
if (block?.[0].block !== position || !/^[0-9a-f]{64}$/u.test(block[0]._hash)) {
|
|
2564
|
+
throw new Error("XL1 finalized head block is unavailable or inconsistent");
|
|
2565
|
+
}
|
|
2566
|
+
if (String(block[0].chain) !== source.chainId || await PayloadBuilder.hash(block[0]) !== block[0]._hash) {
|
|
2567
|
+
throw new Error("XL1 finalized head block chain or hash is inconsistent");
|
|
2568
|
+
}
|
|
2569
|
+
return { position, hash: block[0]._hash };
|
|
2570
|
+
}
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
function createEthereumFinalizedPublisherSource(options) {
|
|
2574
|
+
const source = WakeSourceZod.parse(options.source);
|
|
2575
|
+
if (source.kind !== "ethereum-finalized")
|
|
2576
|
+
throw new Error("Expected an Ethereum finalized source");
|
|
2577
|
+
const { chainId, genesisHash } = source;
|
|
2578
|
+
const url = new URL(options.rpcUrl);
|
|
2579
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.hash !== "") {
|
|
2580
|
+
throw new Error("Ethereum source RPC must use HTTPS without URL credentials or fragments");
|
|
2581
|
+
}
|
|
2582
|
+
const connection = new FetchRequest(url.href);
|
|
2583
|
+
connection.timeout = options.requestTimeoutMs ?? 1e4;
|
|
2584
|
+
const transport = createBoundedPublisherEthersTransport({
|
|
2585
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
2586
|
+
maxResponseBytes: 2 * 1024 * 1024,
|
|
2587
|
+
maxConcurrency: 8
|
|
2588
|
+
});
|
|
2589
|
+
connection.getUrlFunc = transport.getUrl;
|
|
2590
|
+
connection.setThrottleParams({ maxAttempts: 1 });
|
|
2591
|
+
const provider = new JsonRpcProvider(connection, void 0, { batchMaxCount: 1, cacheTimeout: -1 });
|
|
2592
|
+
let reading;
|
|
2593
|
+
let closed = false;
|
|
2594
|
+
let closing;
|
|
2595
|
+
const close = () => closing ??= (async () => {
|
|
2596
|
+
closed = true;
|
|
2597
|
+
provider.destroy();
|
|
2598
|
+
await transport.close();
|
|
2599
|
+
})();
|
|
2600
|
+
const onAbort = () => {
|
|
2601
|
+
void close();
|
|
2602
|
+
};
|
|
2603
|
+
async function read() {
|
|
2604
|
+
const requests = [provider.getNetwork(), provider.getBlock(0), provider.getBlock("finalized")];
|
|
2605
|
+
const values = await Promise.all(requests).catch(async (error) => {
|
|
2606
|
+
await Promise.allSettled(requests);
|
|
2607
|
+
throw error;
|
|
2608
|
+
});
|
|
2609
|
+
const [network, genesis, block] = values;
|
|
2610
|
+
if (network.chainId.toString() !== chainId || genesis?.hash?.slice(2).toLowerCase() !== genesisHash) {
|
|
2611
|
+
throw new Error("Ethereum source chain identity mismatch");
|
|
2612
|
+
}
|
|
2613
|
+
if (block?.hash === null || block === null || !/^0x[0-9a-fA-F]{64}$/u.test(block.hash) || !Number.isSafeInteger(block.number) || block.number < 0) throw new Error("Ethereum finalized head is unavailable");
|
|
2614
|
+
return { position: block.number, hash: block.hash.slice(2).toLowerCase() };
|
|
2615
|
+
}
|
|
2616
|
+
return {
|
|
2617
|
+
source,
|
|
2618
|
+
close,
|
|
2619
|
+
async head(signal) {
|
|
2620
|
+
signal.throwIfAborted();
|
|
2621
|
+
if (closed) throw new Error("Ethereum source is closed");
|
|
2622
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2623
|
+
reading ??= read().finally(() => {
|
|
2624
|
+
reading = void 0;
|
|
2625
|
+
});
|
|
2626
|
+
try {
|
|
2627
|
+
return await boundedPublisherOperation(reading, signal, options.requestTimeoutMs ?? 1e4);
|
|
2628
|
+
} finally {
|
|
2629
|
+
signal.removeEventListener("abort", onAbort);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
function createUtcIntervalPublisherSource(options) {
|
|
2635
|
+
const source = WakeSourceZod.parse(options.source);
|
|
2636
|
+
if (source.kind !== "utc-interval")
|
|
2637
|
+
throw new Error("Expected a UTC interval source");
|
|
2638
|
+
const nowSeconds = options.nowSeconds ?? (() => Math.floor(Date.now() / 1e3));
|
|
2639
|
+
return {
|
|
2640
|
+
source,
|
|
2641
|
+
async head(signal) {
|
|
2642
|
+
signal.throwIfAborted();
|
|
2643
|
+
const now = nowSeconds();
|
|
2644
|
+
if (!Number.isSafeInteger(now) || now < 0)
|
|
2645
|
+
throw new Error("UTC interval clock is invalid");
|
|
2646
|
+
return { position: completedUtcIntervalPosition(source, now) ?? -1 };
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// src/runtime/httpsTransport.ts
|
|
2652
|
+
var OptionsZod = z2.strictObject({
|
|
2653
|
+
allowedEndpoints: z2.array(z2.url()).min(1).max(64),
|
|
2654
|
+
endpointHeaders: z2.array(z2.strictObject({
|
|
2655
|
+
endpoint: z2.url(),
|
|
2656
|
+
headers: z2.strictObject({ "x-vercel-protection-bypass": z2.string().min(1).max(4096).regex(/^[\x21-\x7E]+$/u) })
|
|
2657
|
+
})).max(64).default([]),
|
|
2658
|
+
maxResponseBytes: z2.number().int().min(1).max(1048576).default(65536),
|
|
2659
|
+
requestTimeoutMs: z2.number().int().min(100).max(6e4).default(1e4)
|
|
2660
|
+
});
|
|
2661
|
+
var nonPublicIpv4 = new BlockList();
|
|
2662
|
+
for (const [address, prefix] of [
|
|
2663
|
+
["0.0.0.0", 8],
|
|
2664
|
+
["10.0.0.0", 8],
|
|
2665
|
+
["100.64.0.0", 10],
|
|
2666
|
+
["127.0.0.0", 8],
|
|
2667
|
+
["169.254.0.0", 16],
|
|
2668
|
+
["172.16.0.0", 12],
|
|
2669
|
+
["192.0.0.0", 24],
|
|
2670
|
+
["192.0.2.0", 24],
|
|
2671
|
+
["192.88.99.0", 24],
|
|
2672
|
+
["192.168.0.0", 16],
|
|
2673
|
+
["198.18.0.0", 15],
|
|
2674
|
+
["198.51.100.0", 24],
|
|
2675
|
+
["203.0.113.0", 24],
|
|
2676
|
+
["224.0.0.0", 4],
|
|
2677
|
+
["240.0.0.0", 4]
|
|
2678
|
+
])
|
|
2679
|
+
nonPublicIpv4.addSubnet(address, prefix, "ipv4");
|
|
2680
|
+
var globalIpv6 = new BlockList();
|
|
2681
|
+
globalIpv6.addSubnet("2000::", 3, "ipv6");
|
|
2682
|
+
var nonPublicIpv6 = new BlockList();
|
|
2683
|
+
for (const [address, prefix] of [
|
|
2684
|
+
["2001::", 23],
|
|
2685
|
+
["2001:db8::", 32],
|
|
2686
|
+
["2002::", 16],
|
|
2687
|
+
["3fff::", 20]
|
|
2688
|
+
])
|
|
2689
|
+
nonPublicIpv6.addSubnet(address, prefix, "ipv6");
|
|
2690
|
+
function isPublicPublisherAddress(address) {
|
|
2691
|
+
if (isIP(address) === 4)
|
|
2692
|
+
return !nonPublicIpv4.check(address, "ipv4");
|
|
2693
|
+
if (isIP(address) !== 6)
|
|
2694
|
+
return false;
|
|
2695
|
+
return globalIpv6.check(address, "ipv6") && !nonPublicIpv6.check(address, "ipv6");
|
|
2696
|
+
}
|
|
2697
|
+
function createPublisherHttpsTransport(input) {
|
|
2698
|
+
const parsed = OptionsZod.safeParse(input);
|
|
2699
|
+
if (!parsed.success) throw new Error("Invalid publisher HTTPS transport configuration");
|
|
2700
|
+
const options = parsed.data;
|
|
2701
|
+
const allowed = new Set(options.allowedEndpoints.map((endpoint) => {
|
|
2702
|
+
const url = new URL(endpoint);
|
|
2703
|
+
if (url.href !== endpoint || url.protocol !== "https:" || url.port !== "" && url.port !== "443" || url.username !== "" || url.password !== "" || url.hash !== "")
|
|
2704
|
+
throw new Error("Publisher destinations must be canonical public HTTPS URLs on port 443");
|
|
2705
|
+
const literal = url.hostname.replaceAll(/^\[|\]$/gu, "");
|
|
2706
|
+
if (isIP(literal) !== 0 && !isPublicPublisherAddress(literal))
|
|
2707
|
+
throw new Error("Publisher destination is not public");
|
|
2708
|
+
if (url.hostname === "localhost" || url.hostname.endsWith(".localhost") || url.hostname.endsWith(".local"))
|
|
2709
|
+
throw new Error("Publisher destination is not public");
|
|
2710
|
+
return endpoint;
|
|
2711
|
+
}));
|
|
2712
|
+
const assertAllowed = (endpoint) => {
|
|
2713
|
+
if (!allowed.has(endpoint))
|
|
2714
|
+
throw new Error("Publisher destination is not explicitly allowed");
|
|
2715
|
+
};
|
|
2716
|
+
const endpointHeaders = /* @__PURE__ */ new Map();
|
|
2717
|
+
for (const entry of options.endpointHeaders) {
|
|
2718
|
+
assertAllowed(entry.endpoint);
|
|
2719
|
+
if (endpointHeaders.has(entry.endpoint)) throw new Error("Duplicate publisher endpoint header configuration");
|
|
2720
|
+
endpointHeaders.set(entry.endpoint, entry.headers);
|
|
2721
|
+
}
|
|
2722
|
+
return {
|
|
2723
|
+
assertAllowed,
|
|
2724
|
+
async post(endpoint, body, signal, jwt) {
|
|
2725
|
+
assertAllowed(endpoint);
|
|
2726
|
+
signal.throwIfAborted();
|
|
2727
|
+
const url = new URL(endpoint);
|
|
2728
|
+
const scope = publisherAbortScope([signal], options.requestTimeoutMs);
|
|
2729
|
+
const combined = scope.signal;
|
|
2730
|
+
try {
|
|
2731
|
+
const addresses = await boundedPublisherOperation(lookup(url.hostname.replaceAll(/^\[|\]$/gu, ""), { all: true, verbatim: true }), combined, options.requestTimeoutMs);
|
|
2732
|
+
combined.throwIfAborted();
|
|
2733
|
+
if (addresses.length === 0 || addresses.some((item) => !isPublicPublisherAddress(item.address)))
|
|
2734
|
+
throw new Error("Publisher DNS answer includes a non-public address");
|
|
2735
|
+
const selected = addresses[0];
|
|
2736
|
+
if (selected === void 0)
|
|
2737
|
+
throw new Error("Publisher DNS lookup was empty");
|
|
2738
|
+
const encoded = Buffer3.from(JSON.stringify(body));
|
|
2739
|
+
if (encoded.byteLength > 65536)
|
|
2740
|
+
throw new Error("Publisher request body is too large");
|
|
2741
|
+
return await new Promise((resolve, reject) => {
|
|
2742
|
+
const call = request(url, {
|
|
2743
|
+
agent: false,
|
|
2744
|
+
headers: {
|
|
2745
|
+
...endpointHeaders.get(endpoint),
|
|
2746
|
+
"content-length": encoded.byteLength,
|
|
2747
|
+
"content-type": "application/json",
|
|
2748
|
+
...jwt === void 0 ? {} : { authorization: `Bearer ${jwt}` }
|
|
2749
|
+
},
|
|
2750
|
+
lookup(_hostname, lookupOptions, callback) {
|
|
2751
|
+
if (lookupOptions.all)
|
|
2752
|
+
callback(null, [{ address: selected.address, family: selected.family }]);
|
|
2753
|
+
else
|
|
2754
|
+
callback(null, selected.address, selected.family);
|
|
2755
|
+
},
|
|
2756
|
+
method: "POST",
|
|
2757
|
+
rejectUnauthorized: true,
|
|
2758
|
+
signal: combined
|
|
2759
|
+
}, (response) => {
|
|
2760
|
+
const status = response.statusCode ?? 500;
|
|
2761
|
+
if (status >= 300 && status < 400) {
|
|
2762
|
+
response.destroy();
|
|
2763
|
+
reject(new Error("Publisher HTTPS redirects are forbidden"));
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
let size = 0;
|
|
2767
|
+
const chunks = [];
|
|
2768
|
+
response.on("data", (chunk) => {
|
|
2769
|
+
size += chunk.byteLength;
|
|
2770
|
+
if (size > options.maxResponseBytes) {
|
|
2771
|
+
response.destroy();
|
|
2772
|
+
reject(new Error("Publisher HTTPS response is too large"));
|
|
2773
|
+
} else
|
|
2774
|
+
chunks.push(chunk);
|
|
2775
|
+
});
|
|
2776
|
+
response.on("error", () => reject(new Error("Publisher HTTPS response failed")));
|
|
2777
|
+
response.on("end", () => {
|
|
2778
|
+
try {
|
|
2779
|
+
const responseBody = size === 0 ? null : JSON.parse(Buffer3.concat(chunks).toString("utf8"));
|
|
2780
|
+
const retry = Number(response.headers["retry-after"]);
|
|
2781
|
+
resolve({
|
|
2782
|
+
status,
|
|
2783
|
+
body: responseBody,
|
|
2784
|
+
...Number.isFinite(retry) && retry > 0 ? { retryAfterMs: Math.min(retry * 1e3, 36e5) } : {}
|
|
2785
|
+
});
|
|
2786
|
+
} catch {
|
|
2787
|
+
reject(new Error("Publisher HTTPS response is not JSON"));
|
|
2788
|
+
}
|
|
2789
|
+
});
|
|
2790
|
+
});
|
|
2791
|
+
call.on("error", () => reject(new Error("Publisher HTTPS request failed")));
|
|
2792
|
+
call.end(encoded);
|
|
2793
|
+
});
|
|
2794
|
+
} finally {
|
|
2795
|
+
scope.dispose();
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
// src/runtime/publisherRuntime.ts
|
|
2802
|
+
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
2803
|
+
import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
|
|
2804
|
+
import { createMemoryLocator, PeriodicActor } from "@ariestools/actor";
|
|
2805
|
+
import { creatable } from "@ariestools/sdk";
|
|
2806
|
+
import { WakeEnvelopeSchema, WakeEnvelopeZod as WakeEnvelopeZod2 } from "@xyo-network/event-kit-schemas";
|
|
2807
|
+
import { z as z3 } from "zod";
|
|
2808
|
+
|
|
2809
|
+
// src/runtime/sourceSigner.ts
|
|
2810
|
+
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
2811
|
+
import {
|
|
2812
|
+
NormalizedAddressZod as NormalizedAddressZod4,
|
|
2813
|
+
WakeEnvelopeZod,
|
|
2814
|
+
WakeJwtSchema
|
|
2815
|
+
} from "@xyo-network/event-kit-schemas";
|
|
2816
|
+
import { PayloadBuilder as PayloadBuilder2 } from "@xyo-network/sdk";
|
|
2817
|
+
function createAccountSourceWakeSigner(options) {
|
|
2818
|
+
const publisher = NormalizedAddressZod4.parse(options.publisher);
|
|
2819
|
+
if (String(options.account.address).replace(/^0x/u, "").toLowerCase() !== publisher)
|
|
2820
|
+
throw new Error("Publisher signing account mismatch");
|
|
2821
|
+
return {
|
|
2822
|
+
async sign(input, audience) {
|
|
2823
|
+
const event = WakeEnvelopeZod.parse(input);
|
|
2824
|
+
const eventHash = await PayloadBuilder2.dataHash(event);
|
|
2825
|
+
const now = options.nowSeconds?.() ?? Math.floor(Date.now() / 1e3);
|
|
2826
|
+
if (!Number.isSafeInteger(now) || now < 0)
|
|
2827
|
+
throw new Error("Publisher signing clock is invalid");
|
|
2828
|
+
const { token: jwt } = await options.account.signJwt({
|
|
2829
|
+
audience,
|
|
2830
|
+
claims: { eventHash, jti: randomBytes5(16).toString("hex") },
|
|
2831
|
+
now,
|
|
2832
|
+
schema: WakeJwtSchema,
|
|
2833
|
+
ttl: 60
|
|
2834
|
+
});
|
|
2835
|
+
return {
|
|
2836
|
+
event,
|
|
2837
|
+
eventHash,
|
|
2838
|
+
jwt,
|
|
2839
|
+
publisher
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
// src/runtime/publisherRuntime.ts
|
|
2846
|
+
function activationReady(status) {
|
|
2847
|
+
return (status?.keyLoaded ?? true) && (status?.pendingSubscriptions ?? 0) === 0;
|
|
2848
|
+
}
|
|
2849
|
+
function assertDeliveryState(record, subscription) {
|
|
2850
|
+
if (!isDeepStrictEqual2(record.subscription, subscription))
|
|
2851
|
+
throw new Error("Subscription revision changed without a new identity");
|
|
2852
|
+
if (!Number.isSafeInteger(record.cursor) || record.cursor < subscription.fromPosition - 1 || ![record.delivered, record.failures, record.nextAttemptMs].every((value) => Number.isSafeInteger(value) && value >= 0) || !["active", "blocked", "cancelled"].includes(record.state))
|
|
2853
|
+
throw new Error("Persisted publisher delivery state is invalid");
|
|
2854
|
+
if (record.pending !== void 0) {
|
|
2855
|
+
const pending = WakeEnvelopeZod2.parse(record.pending);
|
|
2856
|
+
if (pending.fromPosition !== record.cursor + 1 || pending.toPosition - pending.fromPosition + 1 > subscription.maxPositionsPerWake || pending.authorizationChainId !== subscription.authorizationChainId || pending.deploymentId !== subscription.deploymentId || pending.queueId !== subscription.queueId || pending.filterHash !== subscription.filterHash || !isDeepStrictEqual2(pending.source, subscription.source))
|
|
2857
|
+
throw new Error("Persisted publisher outbox binding is invalid");
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
function makeEvent(subscription, fromPosition, toPosition, now, wakeId = randomBytes6(16).toString("hex")) {
|
|
2861
|
+
return WakeEnvelopeZod2.parse({
|
|
2862
|
+
schema: WakeEnvelopeSchema,
|
|
2863
|
+
authorizationChainId: subscription.authorizationChainId,
|
|
2864
|
+
deploymentId: subscription.deploymentId,
|
|
2865
|
+
filterHash: subscription.filterHash,
|
|
2866
|
+
fromPosition,
|
|
2867
|
+
issuedAt: now,
|
|
2868
|
+
source: subscription.source,
|
|
2869
|
+
toPosition,
|
|
2870
|
+
wakeId,
|
|
2871
|
+
...subscription.queueId === void 0 ? {} : { queueId: subscription.queueId }
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
var PublisherRuntimeActor = class extends PeriodicActor {
|
|
2875
|
+
async runPass(context) {
|
|
2876
|
+
await this.params.tick(context.signal);
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
PublisherRuntimeActor = __decorateClass([
|
|
2880
|
+
creatable()
|
|
2881
|
+
], PublisherRuntimeActor);
|
|
2882
|
+
var TimingZod = z3.object({
|
|
2883
|
+
concurrency: z3.number().int().min(1).max(32).default(4),
|
|
2884
|
+
intervalMs: z3.number().int().min(100).max(6e4).default(1e3),
|
|
2885
|
+
maxSubscriptions: z3.number().int().min(1).max(1024).default(64),
|
|
2886
|
+
requestTimeoutMs: z3.number().int().min(100).max(6e4).default(1e4)
|
|
2887
|
+
});
|
|
2888
|
+
var DeliveryResponseError = class extends Error {
|
|
2889
|
+
retryAfterMs;
|
|
2890
|
+
status;
|
|
2891
|
+
constructor(status, retryAfterMs) {
|
|
2892
|
+
super("Publisher admission failed");
|
|
2893
|
+
this.status = status;
|
|
2894
|
+
this.retryAfterMs = retryAfterMs;
|
|
2895
|
+
}
|
|
2896
|
+
get retryable() {
|
|
2897
|
+
return this.status === 408 || this.status === 429 || this.status >= 500;
|
|
2898
|
+
}
|
|
2899
|
+
};
|
|
2900
|
+
async function createPersistentEventPublisherRuntime(options) {
|
|
2901
|
+
const timing = TimingZod.parse(options);
|
|
2902
|
+
if (options.sources.length < 1 || options.sources.length > 16)
|
|
2903
|
+
throw new Error("Publisher requires between one and sixteen bounded sources");
|
|
2904
|
+
const clock = options.nowSeconds ?? (() => Math.floor(Date.now() / 1e3));
|
|
2905
|
+
const signer = createAccountSourceWakeSigner({
|
|
2906
|
+
account: options.account,
|
|
2907
|
+
publisher: options.publisher,
|
|
2908
|
+
nowSeconds: clock
|
|
2909
|
+
});
|
|
2910
|
+
const sourceReaders = new Map(options.sources.map((reader) => [publisherSourceKey(reader.source), reader]));
|
|
2911
|
+
if (sourceReaders.size !== options.sources.length)
|
|
2912
|
+
throw new Error("Duplicate publisher source identity");
|
|
2913
|
+
const abort = new AbortController();
|
|
2914
|
+
let running = false;
|
|
2915
|
+
let ready = false;
|
|
2916
|
+
let clockHealthy = true;
|
|
2917
|
+
let passes = 0;
|
|
2918
|
+
let lastError;
|
|
2919
|
+
let sourcesHealthy = false;
|
|
2920
|
+
let stop;
|
|
2921
|
+
const state = options.state;
|
|
2922
|
+
const timed = (promise, signal) => boundedPublisherOperation(promise, signal, timing.requestTimeoutMs);
|
|
2923
|
+
function loadDelivery(subscription, now) {
|
|
2924
|
+
const key = subscription.subscriptionHash;
|
|
2925
|
+
let record = state.get("deliveries", key);
|
|
2926
|
+
if (record === void 0) {
|
|
2927
|
+
makeEvent(subscription, subscription.fromPosition, subscription.fromPosition, now, "0".repeat(32));
|
|
2928
|
+
if (!Number.isSafeInteger(subscription.maxPositionsPerWake) || subscription.maxPositionsPerWake < 1 || subscription.maxPositionsPerWake > 4096) {
|
|
2929
|
+
throw new Error("Invalid source range budget");
|
|
2930
|
+
}
|
|
2931
|
+
record = {
|
|
2932
|
+
subscription,
|
|
2933
|
+
cursor: subscription.fromPosition - 1,
|
|
2934
|
+
delivered: 0,
|
|
2935
|
+
failures: 0,
|
|
2936
|
+
nextAttemptMs: 0,
|
|
2937
|
+
state: "active"
|
|
2938
|
+
};
|
|
2939
|
+
state.set("deliveries", key, record);
|
|
2940
|
+
}
|
|
2941
|
+
assertDeliveryState(record, subscription);
|
|
2942
|
+
return record;
|
|
2943
|
+
}
|
|
2944
|
+
function prepareDelivery(record, now, heads) {
|
|
2945
|
+
if (record.pending !== void 0) return { ...record, pending: record.pending };
|
|
2946
|
+
const subscription = record.subscription;
|
|
2947
|
+
const head = heads.get(publisherSourceKey(subscription.source));
|
|
2948
|
+
if (head === void 0 || head instanceof Error) throw new Error("Configured source is unavailable");
|
|
2949
|
+
if (head.position <= record.cursor) return;
|
|
2950
|
+
const fromPosition = record.cursor + 1;
|
|
2951
|
+
const pending = makeEvent(subscription, fromPosition, Math.min(head.position, fromPosition + subscription.maxPositionsPerWake - 1), now);
|
|
2952
|
+
const prepared = { ...record, pending };
|
|
2953
|
+
state.transaction(() => {
|
|
2954
|
+
const logicalKey = JSON.stringify([subscription.subscriptionHash, pending.fromPosition, pending.toPosition]);
|
|
2955
|
+
if (!state.insert("delivery-events", logicalKey, pending)) throw new Error("Publisher logical range already exists outside its contiguous cursor");
|
|
2956
|
+
state.set("deliveries", subscription.subscriptionHash, prepared);
|
|
2957
|
+
});
|
|
2958
|
+
return prepared;
|
|
2959
|
+
}
|
|
2960
|
+
async function stillActive(record, signal) {
|
|
2961
|
+
if (await timed(options.discovery.isActive(record.subscription, signal), signal)) return true;
|
|
2962
|
+
state.set("deliveries", record.subscription.subscriptionHash, {
|
|
2963
|
+
...record,
|
|
2964
|
+
pending: void 0,
|
|
2965
|
+
state: "cancelled"
|
|
2966
|
+
});
|
|
2967
|
+
return false;
|
|
2968
|
+
}
|
|
2969
|
+
async function sendDelivery(record, signal, now) {
|
|
2970
|
+
const { pending, subscription } = record;
|
|
2971
|
+
signal.throwIfAborted();
|
|
2972
|
+
const signed = await signer.sign(pending, subscription.audience);
|
|
2973
|
+
signal.throwIfAborted();
|
|
2974
|
+
const attempt = state.get("delivery-attempts", pending.wakeId)?.count ?? 0;
|
|
2975
|
+
state.set("delivery-attempts", pending.wakeId, {
|
|
2976
|
+
count: attempt + 1,
|
|
2977
|
+
startedAt: now,
|
|
2978
|
+
status: "in-flight"
|
|
2979
|
+
});
|
|
2980
|
+
const response = await options.transport.post(subscription.endpoint, signed.event, signal, signed.jwt);
|
|
2981
|
+
state.set("delivery-attempts", pending.wakeId, {
|
|
2982
|
+
count: attempt + 1,
|
|
2983
|
+
startedAt: now,
|
|
2984
|
+
status: response.status
|
|
2985
|
+
});
|
|
2986
|
+
if (response.status !== 200 && response.status !== 202) throw new DeliveryResponseError(response.status, response.retryAfterMs);
|
|
2987
|
+
const receipt = z3.object({ receiptId: z3.string().min(1), wakeId: z3.literal(pending.wakeId) }).safeParse(response.body);
|
|
2988
|
+
if (!receipt.success) throw new Error("Publisher admission did not return a matching durable receipt");
|
|
2989
|
+
signal.throwIfAborted();
|
|
2990
|
+
state.transaction(() => {
|
|
2991
|
+
state.set("delivery-receipts", pending.wakeId, { receiptId: receipt.data.receiptId, receivedAt: now });
|
|
2992
|
+
state.set("deliveries", subscription.subscriptionHash, {
|
|
2993
|
+
subscription,
|
|
2994
|
+
cursor: pending.toPosition,
|
|
2995
|
+
delivered: record.delivered + 1,
|
|
2996
|
+
failures: 0,
|
|
2997
|
+
nextAttemptMs: 0,
|
|
2998
|
+
state: "active"
|
|
2999
|
+
});
|
|
3000
|
+
});
|
|
3001
|
+
}
|
|
3002
|
+
async function deliver(subscription, signal, now, heads) {
|
|
3003
|
+
let record = loadDelivery(subscription, now);
|
|
3004
|
+
if (record.state !== "active" || record.nextAttemptMs > now * 1e3) return;
|
|
3005
|
+
try {
|
|
3006
|
+
if (!await stillActive(record, signal)) return;
|
|
3007
|
+
options.transport.assertAllowed(subscription.endpoint);
|
|
3008
|
+
if (!sourceReaders.has(publisherSourceKey(subscription.source))) throw new Error("Subscription source is not configured");
|
|
3009
|
+
const prepared = prepareDelivery(record, now, heads);
|
|
3010
|
+
if (prepared === void 0) return;
|
|
3011
|
+
record = prepared;
|
|
3012
|
+
if (!await stillActive(record, signal)) return;
|
|
3013
|
+
await sendDelivery(prepared, signal, now);
|
|
3014
|
+
} catch (error) {
|
|
3015
|
+
if (signal.aborted) return;
|
|
3016
|
+
state.set("deliveries", subscription.subscriptionHash, {
|
|
3017
|
+
...record,
|
|
3018
|
+
failures: record.failures + 1,
|
|
3019
|
+
state: error instanceof DeliveryResponseError && !error.retryable ? "blocked" : "active",
|
|
3020
|
+
nextAttemptMs: now * 1e3 + Math.min(36e5, error instanceof DeliveryResponseError && error.retryAfterMs !== void 0 ? error.retryAfterMs : Math.min(3e5, 1e3 * 2 ** Math.min(record.failures, 8))),
|
|
3021
|
+
lastError: "delivery-failed"
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
async function readSources(signal) {
|
|
3026
|
+
const heads = /* @__PURE__ */ new Map();
|
|
3027
|
+
const results = await Promise.allSettled([...sourceReaders].map(async ([key, reader]) => {
|
|
3028
|
+
const head = await timed(reader.head(signal), signal);
|
|
3029
|
+
if (!Number.isSafeInteger(head.position) || head.position < -1)
|
|
3030
|
+
throw new Error("Source head is invalid");
|
|
3031
|
+
const previous = state.get("source-heads", key);
|
|
3032
|
+
if (previous !== void 0 && (head.position < previous.position || head.position === previous.position && head.hash !== previous.hash)) {
|
|
3033
|
+
throw new Error("Finalized source regressed or changed hash");
|
|
3034
|
+
}
|
|
3035
|
+
signal.throwIfAborted();
|
|
3036
|
+
state.set("source-heads", key, head);
|
|
3037
|
+
heads.set(key, head);
|
|
3038
|
+
}));
|
|
3039
|
+
sourcesHealthy = results.every((item) => item.status === "fulfilled");
|
|
3040
|
+
for (const [index, result] of results.entries())
|
|
3041
|
+
if (result.status === "rejected") {
|
|
3042
|
+
const key = [...sourceReaders.keys()][index];
|
|
3043
|
+
if (key !== void 0)
|
|
3044
|
+
heads.set(key, new Error("Source read failed"));
|
|
3045
|
+
}
|
|
3046
|
+
return heads;
|
|
3047
|
+
}
|
|
3048
|
+
async function reconcileDeliveries(subscriptions, signal) {
|
|
3049
|
+
if (subscriptions.length > timing.maxSubscriptions) throw new Error("Publisher subscription limit exceeded");
|
|
3050
|
+
const active = new Set(subscriptions.map((subscription) => subscription.subscriptionHash));
|
|
3051
|
+
if (active.size !== subscriptions.length) throw new Error("Duplicate subscription identity");
|
|
3052
|
+
const unavailable = state.entries("deliveries").filter((item) => !active.has(item.key) && item.value.state !== "cancelled");
|
|
3053
|
+
let available = true;
|
|
3054
|
+
for (let offset = 0; offset < unavailable.length; offset += timing.concurrency) {
|
|
3055
|
+
signal.throwIfAborted();
|
|
3056
|
+
const results = await Promise.allSettled(unavailable.slice(offset, offset + timing.concurrency).map(async ({ key, value }) => {
|
|
3057
|
+
const stillAuthorized = await timed(options.discovery.isActive(value.subscription, signal), signal);
|
|
3058
|
+
if (!stillAuthorized) state.set("deliveries", key, {
|
|
3059
|
+
...value,
|
|
3060
|
+
pending: void 0,
|
|
3061
|
+
state: "cancelled"
|
|
3062
|
+
});
|
|
3063
|
+
return stillAuthorized;
|
|
3064
|
+
}));
|
|
3065
|
+
available = available && results.every((result) => result.status === "fulfilled" && !result.value);
|
|
3066
|
+
}
|
|
3067
|
+
return available;
|
|
3068
|
+
}
|
|
3069
|
+
async function tick(actorSignal) {
|
|
3070
|
+
const scope = publisherAbortScope([abort.signal, actorSignal]);
|
|
3071
|
+
const signal = scope.signal;
|
|
3072
|
+
try {
|
|
3073
|
+
signal.throwIfAborted();
|
|
3074
|
+
const now = clock();
|
|
3075
|
+
const prior = state.get("runtime", "highest-wall-clock") ?? 0;
|
|
3076
|
+
clockHealthy = Number.isSafeInteger(now) && now >= prior;
|
|
3077
|
+
if (!clockHealthy)
|
|
3078
|
+
throw new Error("Publisher clock moved backwards");
|
|
3079
|
+
state.set("runtime", "highest-wall-clock", now);
|
|
3080
|
+
const subscriptions = await timed(options.discovery.discover(signal), signal);
|
|
3081
|
+
const subscriptionsAvailable = await reconcileDeliveries(subscriptions, signal);
|
|
3082
|
+
const heads = await readSources(signal);
|
|
3083
|
+
for (let offset = 0; offset < subscriptions.length; offset += timing.concurrency) {
|
|
3084
|
+
signal.throwIfAborted();
|
|
3085
|
+
const outcomes = await Promise.allSettled(subscriptions.slice(offset, offset + timing.concurrency).map((subscription) => deliver(subscription, signal, now, heads)));
|
|
3086
|
+
const failed = outcomes.find((result) => result.status === "rejected");
|
|
3087
|
+
if (failed?.status === "rejected")
|
|
3088
|
+
throw failed.reason;
|
|
3089
|
+
}
|
|
3090
|
+
if (options.discovery.acceptPending !== void 0)
|
|
3091
|
+
await timed(options.discovery.acceptPending(signal), signal);
|
|
3092
|
+
const discoveryStatus = options.discovery.status?.();
|
|
3093
|
+
ready = subscriptionsAvailable && sourcesHealthy && activationReady(discoveryStatus);
|
|
3094
|
+
lastError = subscriptionsAvailable ? void 0 : "publisher-subscription-unavailable";
|
|
3095
|
+
passes += 1;
|
|
3096
|
+
} catch (error) {
|
|
3097
|
+
ready = false;
|
|
3098
|
+
lastError = clockHealthy ? "publisher-pass-failed" : "publisher-clock-unhealthy";
|
|
3099
|
+
if (!signal.aborted)
|
|
3100
|
+
throw error;
|
|
3101
|
+
} finally {
|
|
3102
|
+
scope.dispose();
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
const actor = await PublisherRuntimeActor.create({
|
|
3106
|
+
locator: createMemoryLocator({}),
|
|
3107
|
+
intervalMs: timing.intervalMs,
|
|
3108
|
+
firstRun: "immediate",
|
|
3109
|
+
readiness: "immediate",
|
|
3110
|
+
tick
|
|
3111
|
+
});
|
|
3112
|
+
return {
|
|
3113
|
+
async start() {
|
|
3114
|
+
if (stop !== void 0)
|
|
3115
|
+
throw new Error("Publisher is stopped");
|
|
3116
|
+
if (!running) {
|
|
3117
|
+
await actor.start();
|
|
3118
|
+
running = true;
|
|
3119
|
+
}
|
|
3120
|
+
},
|
|
3121
|
+
async runOnce() {
|
|
3122
|
+
if (stop !== void 0)
|
|
3123
|
+
throw new Error("Publisher is stopped");
|
|
3124
|
+
await actor.runOnce();
|
|
3125
|
+
},
|
|
3126
|
+
stop() {
|
|
3127
|
+
return stop ??= (async () => {
|
|
3128
|
+
abort.abort();
|
|
3129
|
+
const failures = [];
|
|
3130
|
+
try {
|
|
3131
|
+
await actor.stop();
|
|
3132
|
+
} catch (error) {
|
|
3133
|
+
failures.push(error);
|
|
3134
|
+
}
|
|
3135
|
+
try {
|
|
3136
|
+
await options.discovery.close?.();
|
|
3137
|
+
} catch (error) {
|
|
3138
|
+
failures.push(error);
|
|
3139
|
+
}
|
|
3140
|
+
for (const source of options.sources)
|
|
3141
|
+
try {
|
|
3142
|
+
await source.close?.();
|
|
3143
|
+
} catch (error) {
|
|
3144
|
+
failures.push(error);
|
|
3145
|
+
}
|
|
3146
|
+
running = false;
|
|
3147
|
+
ready = false;
|
|
3148
|
+
if (failures.length > 0)
|
|
3149
|
+
throw new AggregateError(failures, "Publisher runtime shutdown failed");
|
|
3150
|
+
})();
|
|
3151
|
+
},
|
|
3152
|
+
status() {
|
|
3153
|
+
const deliveries = state.entries("deliveries").map((item) => item.value);
|
|
3154
|
+
return {
|
|
3155
|
+
running,
|
|
3156
|
+
ready: running && ready,
|
|
3157
|
+
clockHealthy,
|
|
3158
|
+
degraded: !ready || deliveries.some((item) => item.state !== "cancelled" && item.failures > 0),
|
|
3159
|
+
publisher: options.publisher,
|
|
3160
|
+
activeSubscriptions: deliveries.filter((item) => item.state === "active").length,
|
|
3161
|
+
pendingDeliveries: deliveries.filter((item) => item.pending !== void 0).length,
|
|
3162
|
+
delivered: deliveries.reduce((sum, item) => sum + item.delivered, 0),
|
|
3163
|
+
failures: deliveries.reduce((sum, item) => sum + item.failures, 0),
|
|
3164
|
+
passes,
|
|
3165
|
+
...lastError === void 0 ? {} : { lastError },
|
|
3166
|
+
...options.discovery.status === void 0 ? {} : { handshake: options.discovery.status() },
|
|
3167
|
+
sources: state.entries("source-heads").map((item) => ({ id: item.key, ...item.value })),
|
|
3168
|
+
subscriptions: state.entries("deliveries").map((item) => ({
|
|
3169
|
+
id: item.key,
|
|
3170
|
+
cursor: item.value.cursor,
|
|
3171
|
+
state: item.value.state,
|
|
3172
|
+
pending: item.value.pending !== void 0,
|
|
3173
|
+
failures: item.value.failures
|
|
3174
|
+
}))
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
// src/runtime/restGateway.ts
|
|
3181
|
+
import { FetchJsonClient } from "@ariestools/sdk";
|
|
3182
|
+
import {
|
|
3183
|
+
baseConfigFactoryLocator,
|
|
3184
|
+
basicRestRunnerLocator,
|
|
3185
|
+
basicRestViewerLocator,
|
|
3186
|
+
creatableProvider,
|
|
3187
|
+
releaseProviderFactoryLocator,
|
|
3188
|
+
RestDataLakeRunner,
|
|
3189
|
+
RestDataLakeViewer,
|
|
3190
|
+
XyoGatewayMoniker,
|
|
3191
|
+
XyoGatewayRunnerMoniker
|
|
3192
|
+
} from "@xyo-network/xl1-sdk";
|
|
3193
|
+
async function openPublisherRestGateway(options) {
|
|
3194
|
+
const rest = {
|
|
3195
|
+
finalized: { type: "rest", baseUrl: options.finalizedUrl },
|
|
3196
|
+
chainState: { type: "rest", baseUrl: options.chainStateUrl },
|
|
3197
|
+
index: { type: "rest", baseUrl: options.indexUrl ?? options.finalizedUrl }
|
|
3198
|
+
};
|
|
3199
|
+
const locator = options.account === void 0 ? await basicRestViewerLocator("event-kit-publisher-read", rest, void 0, { fetchFn: options.fetchFn, primeCaches: "off" }) : await basicRestRunnerLocator("event-kit-publisher-write", rest, void 0, {
|
|
3200
|
+
fetchFn: options.fetchFn,
|
|
3201
|
+
primeCaches: "off",
|
|
3202
|
+
rpcUrl: options.rpcUrl,
|
|
3203
|
+
signerAccount: options.account
|
|
3204
|
+
});
|
|
3205
|
+
try {
|
|
3206
|
+
const gateway = options.account === void 0 ? await locator.getInstance(XyoGatewayMoniker) : await locator.getInstance(XyoGatewayRunnerMoniker);
|
|
3207
|
+
let stopping;
|
|
3208
|
+
return Object.freeze({ gateway, stop: () => stopping ??= releaseProviderFactoryLocator(locator) });
|
|
3209
|
+
} catch (error) {
|
|
3210
|
+
await releaseProviderFactoryLocator(locator);
|
|
3211
|
+
throw error;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
var BoundedRestDataLakeViewer = class extends RestDataLakeViewer {
|
|
3215
|
+
get client() {
|
|
3216
|
+
if (this.context.fetchFn === void 0)
|
|
3217
|
+
throw new Error("Publisher datalake requires its bounded fetch transport");
|
|
3218
|
+
return new FetchJsonClient({ fetcher: this.context.fetchFn });
|
|
3219
|
+
}
|
|
3220
|
+
};
|
|
3221
|
+
BoundedRestDataLakeViewer = __decorateClass([
|
|
3222
|
+
creatableProvider()
|
|
3223
|
+
], BoundedRestDataLakeViewer);
|
|
3224
|
+
var BoundedRestDataLakeRunner = class extends RestDataLakeRunner {
|
|
3225
|
+
get client() {
|
|
3226
|
+
if (this.context.fetchFn === void 0)
|
|
3227
|
+
throw new Error("Publisher datalake requires its bounded fetch transport");
|
|
3228
|
+
return new FetchJsonClient({ fetcher: this.context.fetchFn });
|
|
3229
|
+
}
|
|
3230
|
+
};
|
|
3231
|
+
BoundedRestDataLakeRunner = __decorateClass([
|
|
3232
|
+
creatableProvider()
|
|
3233
|
+
], BoundedRestDataLakeRunner);
|
|
3234
|
+
async function openPublisherDataLake(options) {
|
|
3235
|
+
const locator = await baseConfigFactoryLocator({
|
|
3236
|
+
connections: { "event-kit-lake": { type: "rest", baseUrl: options.url } },
|
|
3237
|
+
providerBindings: { DataLakeViewer: { connection: "event-kit-lake" }, DataLakeRunner: { connection: "event-kit-lake" } }
|
|
3238
|
+
});
|
|
3239
|
+
locator.context.fetchFn = options.fetchFn;
|
|
3240
|
+
locator.register(BoundedRestDataLakeViewer.factory(BoundedRestDataLakeViewer.dependencies));
|
|
3241
|
+
if (options.writable)
|
|
3242
|
+
locator.register(BoundedRestDataLakeRunner.factory(BoundedRestDataLakeRunner.dependencies));
|
|
3243
|
+
locator.freeze();
|
|
3244
|
+
try {
|
|
3245
|
+
const reader = await locator.getInstance(BoundedRestDataLakeViewer.defaultMoniker);
|
|
3246
|
+
const writer = options.writable ? await locator.getInstance(BoundedRestDataLakeRunner.defaultMoniker) : void 0;
|
|
3247
|
+
return {
|
|
3248
|
+
reader,
|
|
3249
|
+
writer,
|
|
3250
|
+
async close() {
|
|
3251
|
+
await releaseProviderFactoryLocator(locator);
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
} catch (error) {
|
|
3255
|
+
await releaseProviderFactoryLocator(locator);
|
|
3256
|
+
throw error;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
// src/runtime/sqliteHandshakeStores.ts
|
|
3261
|
+
import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
|
|
3262
|
+
import { endpointChallengeHash as endpointChallengeHash2 } from "@xyo-network/event-kit-protocol";
|
|
3263
|
+
import {
|
|
3264
|
+
Base64Url32BytesZod as Base64Url32BytesZod3,
|
|
3265
|
+
EndpointChallengeZod as EndpointChallengeZod3,
|
|
3266
|
+
HandshakeIdZod as HandshakeIdZod4,
|
|
3267
|
+
HashZod as HashZod5,
|
|
3268
|
+
NormalizedAddressZod as NormalizedAddressZod5
|
|
3269
|
+
} from "@xyo-network/event-kit-schemas";
|
|
3270
|
+
import { z as z4 } from "zod";
|
|
3271
|
+
var KeyZod = z4.strictObject({
|
|
3272
|
+
encryptionKey: HashZod5,
|
|
3273
|
+
keyId: HandshakeIdZod4,
|
|
3274
|
+
privateKey: Base64Url32BytesZod3,
|
|
3275
|
+
publisher: NormalizedAddressZod5
|
|
3276
|
+
});
|
|
3277
|
+
var phases = ["signed", "content-written", "content-verified", "broadcast", "included", "finalized"];
|
|
3278
|
+
function requireTime2(value) {
|
|
3279
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error("Publisher clock is invalid");
|
|
3280
|
+
}
|
|
3281
|
+
function samePublicationIntent(left, right) {
|
|
3282
|
+
return left.chainId === right.chainId && left.source === right.source && left.operationId === right.operationId && isDeepStrictEqual3(left.eventHashes, right.eventHashes) && isDeepStrictEqual3(left.attachmentHashes, right.attachmentHashes);
|
|
3283
|
+
}
|
|
3284
|
+
function publicationKey(intent) {
|
|
3285
|
+
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].every((hash) => HashZod5.safeParse(hash).success)) {
|
|
3286
|
+
throw new Error("Invalid publication intent");
|
|
3287
|
+
}
|
|
3288
|
+
return JSON.stringify([intent.chainId, intent.source, intent.operationId]);
|
|
3289
|
+
}
|
|
3290
|
+
function createSqlitePublisherHandshakeStores(state) {
|
|
3291
|
+
const replay = {
|
|
3292
|
+
async load() {
|
|
3293
|
+
return state.get("replay", "checkpoint");
|
|
3294
|
+
},
|
|
3295
|
+
async save(checkpoint) {
|
|
3296
|
+
state.set("replay", "checkpoint", checkpoint);
|
|
3297
|
+
}
|
|
3298
|
+
};
|
|
3299
|
+
const intents = {
|
|
3300
|
+
async get(lookup2) {
|
|
3301
|
+
const value = state.get("intents", intentLookupKey(lookup2));
|
|
3302
|
+
return value === void 0 ? void 0 : await parseHandshakeMutationIntent(value);
|
|
3303
|
+
},
|
|
3304
|
+
async getOrCreate(raw) {
|
|
3305
|
+
const proposed = await parseHandshakeMutationIntent(raw);
|
|
3306
|
+
return state.transaction(() => {
|
|
3307
|
+
const key = intentLookupKey(proposed);
|
|
3308
|
+
const winner = state.get("intents", key);
|
|
3309
|
+
if (winner !== void 0) {
|
|
3310
|
+
assertSameIntentBinding(winner, proposed);
|
|
3311
|
+
return { disposition: "resumed", intent: winner };
|
|
3312
|
+
}
|
|
3313
|
+
state.set("intents", key, proposed);
|
|
3314
|
+
return { disposition: "created", intent: proposed };
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
};
|
|
3318
|
+
const keys = {
|
|
3319
|
+
async getPrivateKey(input) {
|
|
3320
|
+
const key = `${NormalizedAddressZod5.parse(input.publisher)}:${HandshakeIdZod4.parse(input.keyId)}`;
|
|
3321
|
+
const raw = state.get("encryption-keys", key);
|
|
3322
|
+
if (raw === void 0) return;
|
|
3323
|
+
const record = KeyZod.parse(raw);
|
|
3324
|
+
if (record.encryptionKey !== HashZod5.parse(input.encryptionKey)) throw new Error("Encryption key identity mismatch");
|
|
3325
|
+
const { publisher: _publisher, ...privateKey } = record;
|
|
3326
|
+
return privateKey;
|
|
3327
|
+
},
|
|
3328
|
+
async putPrivateKey(input) {
|
|
3329
|
+
const record = KeyZod.parse({ ...input.key, publisher: input.publisher });
|
|
3330
|
+
return state.transaction(() => {
|
|
3331
|
+
const key = `${record.publisher}:${record.keyId}`;
|
|
3332
|
+
const prior = state.get("encryption-keys", key);
|
|
3333
|
+
if (prior !== void 0) {
|
|
3334
|
+
if (!isDeepStrictEqual3(prior, record)) throw new Error("Encryption key collision");
|
|
3335
|
+
return "duplicate";
|
|
3336
|
+
}
|
|
3337
|
+
state.set("encryption-keys", key, record);
|
|
3338
|
+
return "stored";
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
};
|
|
3342
|
+
const current = (hash) => state.get("challenges", HashZod5.parse(hash));
|
|
3343
|
+
const live = (record, now) => record.challenge.issuedAt <= now && now < record.challenge.expiresAt;
|
|
3344
|
+
const challenges = {
|
|
3345
|
+
async get(hash) {
|
|
3346
|
+
return current(hash);
|
|
3347
|
+
},
|
|
3348
|
+
async issue(input) {
|
|
3349
|
+
const challenge = EndpointChallengeZod3.parse(input.challenge);
|
|
3350
|
+
const hash = HashZod5.parse(input.challengeHash);
|
|
3351
|
+
if (await endpointChallengeHash2(challenge) !== hash) return { kind: "collision" };
|
|
3352
|
+
return state.transaction(() => {
|
|
3353
|
+
const priorNonce = state.get("challenge-nonces", challenge.nonce);
|
|
3354
|
+
if (priorNonce !== void 0 && priorNonce !== hash) return { kind: "collision" };
|
|
3355
|
+
const prior = current(hash);
|
|
3356
|
+
if (prior !== void 0) return isDeepStrictEqual3(prior.challenge, challenge) ? { kind: "duplicate", record: prior } : { kind: "collision" };
|
|
3357
|
+
const record = {
|
|
3358
|
+
challenge,
|
|
3359
|
+
challengeHash: hash,
|
|
3360
|
+
state: "issued"
|
|
3361
|
+
};
|
|
3362
|
+
state.set("challenge-nonces", challenge.nonce, hash);
|
|
3363
|
+
state.set("challenges", hash, record);
|
|
3364
|
+
return { kind: "issued", record };
|
|
3365
|
+
});
|
|
3366
|
+
},
|
|
3367
|
+
async beginAttempt(input) {
|
|
3368
|
+
requireTime2(input.nowSeconds);
|
|
3369
|
+
return state.transaction(() => {
|
|
3370
|
+
const record = current(input.challengeHash);
|
|
3371
|
+
if (record === void 0) return { kind: "missing" };
|
|
3372
|
+
if (record.state === "verified") return { kind: "verified", record };
|
|
3373
|
+
if (record.state === "attempted") return { kind: "already-attempted", record };
|
|
3374
|
+
if (!live(record, input.nowSeconds)) return { kind: "expired", record };
|
|
3375
|
+
const attempted = { ...record, state: "attempted" };
|
|
3376
|
+
state.set("challenges", input.challengeHash, attempted);
|
|
3377
|
+
return { kind: "started", record: attempted };
|
|
3378
|
+
});
|
|
3379
|
+
},
|
|
3380
|
+
async completeVerified(input) {
|
|
3381
|
+
requireTime2(input.nowSeconds);
|
|
3382
|
+
const responseHash = HashZod5.parse(input.responseHash);
|
|
3383
|
+
return state.transaction(() => {
|
|
3384
|
+
const record = current(input.challengeHash);
|
|
3385
|
+
if (record === void 0) return { kind: "missing" };
|
|
3386
|
+
if (record.state === "verified") return { kind: record.responseHash === responseHash ? "duplicate" : "collision", record };
|
|
3387
|
+
if (record.state !== "attempted") return { kind: "not-attempted", record };
|
|
3388
|
+
if (!live(record, input.nowSeconds)) return { kind: "expired", record };
|
|
3389
|
+
const verified = {
|
|
3390
|
+
...record,
|
|
3391
|
+
responseHash,
|
|
3392
|
+
state: "verified"
|
|
3393
|
+
};
|
|
3394
|
+
state.set("challenges", input.challengeHash, verified);
|
|
3395
|
+
return { kind: "verified", record: verified };
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
};
|
|
3399
|
+
const journal = {
|
|
3400
|
+
async load(intent) {
|
|
3401
|
+
const key = publicationKey(intent);
|
|
3402
|
+
const signed = state.get("publication:signed", key);
|
|
3403
|
+
if (signed === void 0) return;
|
|
3404
|
+
if (!samePublicationIntent(signed, intent)) throw new Error("Publication intent collision");
|
|
3405
|
+
let latest = signed;
|
|
3406
|
+
for (const phase of phases) {
|
|
3407
|
+
const record = state.get(`publication:${phase}`, key);
|
|
3408
|
+
if (record === void 0) continue;
|
|
3409
|
+
if (record.phase !== phase || !samePublicationIntent(record, intent) || !isDeepStrictEqual3(record.evidence, signed.evidence)) {
|
|
3410
|
+
throw new Error("Publication evidence collision");
|
|
3411
|
+
}
|
|
3412
|
+
latest = record;
|
|
3413
|
+
}
|
|
3414
|
+
return latest;
|
|
3415
|
+
},
|
|
3416
|
+
async save(record) {
|
|
3417
|
+
const key = publicationKey(record);
|
|
3418
|
+
HashZod5.parse(record.evidence.transactionHash);
|
|
3419
|
+
if (!phases.includes(record.phase)) throw new Error("Unknown publication phase");
|
|
3420
|
+
state.transaction(() => {
|
|
3421
|
+
const signed = state.get("publication:signed", key);
|
|
3422
|
+
if (signed === void 0 && record.phase !== "signed") throw new Error("Publication must bind signed evidence first");
|
|
3423
|
+
if (signed !== void 0 && (!samePublicationIntent(signed, record) || !isDeepStrictEqual3(signed.evidence, record.evidence))) {
|
|
3424
|
+
throw new Error("Publication intent or evidence collision");
|
|
3425
|
+
}
|
|
3426
|
+
const namespace = `publication:${record.phase}`;
|
|
3427
|
+
const prior = state.get(namespace, key);
|
|
3428
|
+
if (prior !== void 0 && !isDeepStrictEqual3(prior, record)) throw new Error("Publication phase collision");
|
|
3429
|
+
if (prior === void 0) state.set(namespace, key, record);
|
|
3430
|
+
});
|
|
3431
|
+
}
|
|
3432
|
+
};
|
|
3433
|
+
return {
|
|
3434
|
+
challenges,
|
|
3435
|
+
intents,
|
|
3436
|
+
journal,
|
|
3437
|
+
keys,
|
|
3438
|
+
replay
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
// src/runtime/sqlitePublisherState.ts
|
|
3443
|
+
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
3444
|
+
import { constants } from "node:fs";
|
|
3445
|
+
import {
|
|
3446
|
+
lstat,
|
|
3447
|
+
mkdir as mkdir5,
|
|
3448
|
+
open as open5
|
|
3449
|
+
} from "node:fs/promises";
|
|
3450
|
+
import { hostname } from "node:os";
|
|
2309
3451
|
import PATH12 from "node:path";
|
|
3452
|
+
function decode(value) {
|
|
3453
|
+
return JSON.parse(value, (_key, candidate) => {
|
|
3454
|
+
if (typeof candidate === "object" && candidate !== null && Object.keys(candidate).length === 1 && "$eventKitBigInt" in candidate && typeof candidate.$eventKitBigInt === "string" && /^(?:0|[1-9][0-9]*)$/u.test(candidate.$eventKitBigInt))
|
|
3455
|
+
return BigInt(candidate.$eventKitBigInt);
|
|
3456
|
+
return candidate;
|
|
3457
|
+
});
|
|
3458
|
+
}
|
|
3459
|
+
function processExists(pid) {
|
|
3460
|
+
if (!Number.isSafeInteger(pid) || pid < 1)
|
|
3461
|
+
throw new Error("Invalid publisher owner record");
|
|
3462
|
+
try {
|
|
3463
|
+
process.kill(pid, 0);
|
|
3464
|
+
return true;
|
|
3465
|
+
} catch (error) {
|
|
3466
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH")
|
|
3467
|
+
return false;
|
|
3468
|
+
return true;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
async function openSqlitePublisherState(options) {
|
|
3472
|
+
if (options.path === ":memory:" || options.identity.length === 0)
|
|
3473
|
+
throw new Error("Persistent publisher needs a file and an identity");
|
|
3474
|
+
const path = PATH12.resolve(options.path);
|
|
3475
|
+
const directory = PATH12.dirname(path);
|
|
3476
|
+
await mkdir5(directory, { recursive: true, mode: 448 });
|
|
3477
|
+
const metadata = await lstat(directory);
|
|
3478
|
+
if (!metadata.isDirectory() || (metadata.mode & 63) !== 0)
|
|
3479
|
+
throw new Error("Publisher state directory must be a real directory with 0700 permissions");
|
|
3480
|
+
const file = await open5(path, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW, 384);
|
|
3481
|
+
try {
|
|
3482
|
+
const metadata2 = await file.stat();
|
|
3483
|
+
if (!metadata2.isFile() || (metadata2.mode & 63) !== 0)
|
|
3484
|
+
throw new Error("Publisher database permissions must be 0600");
|
|
3485
|
+
await file.sync();
|
|
3486
|
+
} finally {
|
|
3487
|
+
await file.close();
|
|
3488
|
+
}
|
|
3489
|
+
const { DatabaseSync: DatabaseSync2 } = await import("node:sqlite");
|
|
3490
|
+
const database = new DatabaseSync2(path);
|
|
3491
|
+
const token = randomBytes7(16).toString("hex");
|
|
3492
|
+
let closed = false;
|
|
3493
|
+
let inTransaction = false;
|
|
3494
|
+
const transaction = (work) => {
|
|
3495
|
+
if (closed)
|
|
3496
|
+
throw new Error("Publisher state is closed");
|
|
3497
|
+
if (inTransaction)
|
|
3498
|
+
return work();
|
|
3499
|
+
database.exec("BEGIN IMMEDIATE");
|
|
3500
|
+
inTransaction = true;
|
|
3501
|
+
try {
|
|
3502
|
+
const result = work();
|
|
3503
|
+
if (result instanceof Promise)
|
|
3504
|
+
throw new Error("Publisher state transactions must be synchronous");
|
|
3505
|
+
database.exec("COMMIT");
|
|
3506
|
+
return result;
|
|
3507
|
+
} catch (error) {
|
|
3508
|
+
database.exec("ROLLBACK");
|
|
3509
|
+
throw error;
|
|
3510
|
+
} finally {
|
|
3511
|
+
inTransaction = false;
|
|
3512
|
+
}
|
|
3513
|
+
};
|
|
3514
|
+
try {
|
|
3515
|
+
database.exec(`
|
|
3516
|
+
PRAGMA journal_mode = WAL;
|
|
3517
|
+
PRAGMA synchronous = FULL;
|
|
3518
|
+
PRAGMA busy_timeout = 5000;
|
|
3519
|
+
CREATE TABLE IF NOT EXISTS publisher_records (namespace TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY(namespace, key));
|
|
3520
|
+
CREATE TABLE IF NOT EXISTS publisher_owner (id INTEGER PRIMARY KEY CHECK(id = 1), host TEXT NOT NULL, pid INTEGER NOT NULL, token TEXT NOT NULL);
|
|
3521
|
+
`);
|
|
3522
|
+
transaction(() => {
|
|
3523
|
+
const prior = database.prepare("SELECT host, pid, token FROM publisher_owner WHERE id = 1").get();
|
|
3524
|
+
if (prior !== void 0 && (prior.host !== hostname() || processExists(prior.pid))) {
|
|
3525
|
+
throw new Error("Publisher database already has an active owner; a shared or multi-host database is not supported");
|
|
3526
|
+
}
|
|
3527
|
+
const binding = database.prepare("SELECT value FROM publisher_records WHERE namespace = ? AND key = ?").get("metadata", "identity");
|
|
3528
|
+
if (binding !== void 0 && decode(binding.value) !== options.identity)
|
|
3529
|
+
throw new Error("Publisher database identity mismatch");
|
|
3530
|
+
database.prepare("INSERT OR IGNORE INTO publisher_records(namespace,key,value) VALUES (?,?,?)").run("metadata", "identity", stringifyJson(options.identity));
|
|
3531
|
+
database.prepare("INSERT OR REPLACE INTO publisher_owner(id,host,pid,token) VALUES (1,?,?,?)").run(hostname(), process.pid, token);
|
|
3532
|
+
});
|
|
3533
|
+
} catch (error) {
|
|
3534
|
+
database.close();
|
|
3535
|
+
throw error;
|
|
3536
|
+
}
|
|
3537
|
+
const read = database.prepare("SELECT value FROM publisher_records WHERE namespace = ? AND key = ?");
|
|
3538
|
+
const list = database.prepare("SELECT key, value FROM publisher_records WHERE namespace = ? ORDER BY key");
|
|
3539
|
+
const write = database.prepare("INSERT INTO publisher_records(namespace,key,value) VALUES (?,?,?) ON CONFLICT(namespace,key) DO UPDATE SET value = excluded.value");
|
|
3540
|
+
const insert = database.prepare("INSERT OR IGNORE INTO publisher_records(namespace,key,value) VALUES (?,?,?)");
|
|
3541
|
+
const remove = database.prepare("DELETE FROM publisher_records WHERE namespace = ? AND key = ?");
|
|
3542
|
+
const assertOpen = () => {
|
|
3543
|
+
if (closed)
|
|
3544
|
+
throw new Error("Publisher state is closed");
|
|
3545
|
+
};
|
|
3546
|
+
return {
|
|
3547
|
+
path,
|
|
3548
|
+
get(namespace, key) {
|
|
3549
|
+
assertOpen();
|
|
3550
|
+
const row = read.get(namespace, key);
|
|
3551
|
+
return row === void 0 ? void 0 : decode(row.value);
|
|
3552
|
+
},
|
|
3553
|
+
entries(namespace) {
|
|
3554
|
+
assertOpen();
|
|
3555
|
+
return list.all(namespace).map((row) => ({ key: row.key, value: decode(row.value) }));
|
|
3556
|
+
},
|
|
3557
|
+
set(namespace, key, value) {
|
|
3558
|
+
assertOpen();
|
|
3559
|
+
write.run(namespace, key, stringifyJson(value));
|
|
3560
|
+
},
|
|
3561
|
+
insert(namespace, key, value) {
|
|
3562
|
+
assertOpen();
|
|
3563
|
+
return insert.run(namespace, key, stringifyJson(value)).changes === 1;
|
|
3564
|
+
},
|
|
3565
|
+
remove(namespace, key) {
|
|
3566
|
+
assertOpen();
|
|
3567
|
+
remove.run(namespace, key);
|
|
3568
|
+
},
|
|
3569
|
+
transaction,
|
|
3570
|
+
close() {
|
|
3571
|
+
if (closed)
|
|
3572
|
+
return;
|
|
3573
|
+
transaction(() => {
|
|
3574
|
+
database.prepare("DELETE FROM publisher_owner WHERE id = 1 AND token = ?").run(token);
|
|
3575
|
+
});
|
|
3576
|
+
database.close();
|
|
3577
|
+
closed = true;
|
|
3578
|
+
}
|
|
3579
|
+
};
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
// src/sqliteWakeQueue.ts
|
|
3583
|
+
import { mkdir as mkdir6 } from "node:fs/promises";
|
|
3584
|
+
import PATH13 from "node:path";
|
|
2310
3585
|
import { DatabaseSync } from "node:sqlite";
|
|
2311
3586
|
async function createSqliteWakeQueue(options) {
|
|
2312
3587
|
const inMemory = options.path === ":memory:";
|
|
2313
|
-
const file = inMemory ? options.path :
|
|
2314
|
-
if (!inMemory) await
|
|
3588
|
+
const file = inMemory ? options.path : PATH13.resolve(options.path);
|
|
3589
|
+
if (!inMemory) await mkdir6(PATH13.dirname(file), { recursive: true });
|
|
2315
3590
|
const database = new DatabaseSync(file);
|
|
2316
3591
|
database.exec(`
|
|
2317
3592
|
PRAGMA journal_mode = WAL;
|
|
@@ -2377,6 +3652,11 @@ export {
|
|
|
2377
3652
|
HandshakeMutationIntentKinds,
|
|
2378
3653
|
HandshakeMutationIntentZod,
|
|
2379
3654
|
assertSameIntentBinding,
|
|
3655
|
+
boundedPublisherOperation,
|
|
3656
|
+
createAccountSourceWakeSigner,
|
|
3657
|
+
createBoundedPublisherEthersTransport,
|
|
3658
|
+
createBoundedPublisherFetch,
|
|
3659
|
+
createEthereumFinalizedPublisherSource,
|
|
2380
3660
|
createEventKitHandshakePublicationPort,
|
|
2381
3661
|
createFilesystemCursorStore,
|
|
2382
3662
|
createFilesystemEventKitReplayCheckpointStore,
|
|
@@ -2388,13 +3668,24 @@ export {
|
|
|
2388
3668
|
createFilesystemWakeInbox,
|
|
2389
3669
|
createFilesystemWakeQueue,
|
|
2390
3670
|
createMemoryHandshakeMutationIntentStore,
|
|
3671
|
+
createPersistentEventPublisherRuntime,
|
|
2391
3672
|
createPublisherHandshakeClient,
|
|
3673
|
+
createPublisherHandshakeDiscovery,
|
|
3674
|
+
createPublisherHttpsTransport,
|
|
3675
|
+
createSqlitePublisherHandshakeStores,
|
|
2392
3676
|
createSqliteWakeQueue,
|
|
2393
3677
|
createSubscriberHandshakeClient,
|
|
2394
3678
|
createSynchronizedEventKitHandshakeReader,
|
|
3679
|
+
createUtcIntervalPublisherSource,
|
|
3680
|
+
createXl1PublisherSource,
|
|
2395
3681
|
handshakeMutationRequestFingerprint,
|
|
2396
3682
|
intentLookupKey,
|
|
3683
|
+
isPublicPublisherAddress,
|
|
3684
|
+
openPublisherDataLake,
|
|
3685
|
+
openPublisherRestGateway,
|
|
3686
|
+
openSqlitePublisherState,
|
|
2397
3687
|
parseHandshakeMutationIntent,
|
|
2398
|
-
parseHandshakeMutationIntentLookup
|
|
3688
|
+
parseHandshakeMutationIntentLookup,
|
|
3689
|
+
publisherSourceKey
|
|
2399
3690
|
};
|
|
2400
3691
|
//# sourceMappingURL=index.mjs.map
|