@xyo-network/event-kit-protocol 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,802 @@
1
+ // src/admissionTypes.ts
2
+ var EVENT_KIT_REPLAY_RETENTION_SKEW_SECONDS = 60;
3
+
4
+ // src/admitAndEnqueue.ts
5
+ async function admitAndEnqueue(input) {
6
+ const attemptResult = createAttempt(input.verified);
7
+ if (!attemptResult.ok) return attemptResult.failure;
8
+ const { attempt } = attemptResult;
9
+ const wake = toAdmittedWake(input.verified);
10
+ const prior = await resolvePriorAttempt(input.inbox, attempt, wake);
11
+ if (!prior.pending) return prior.result;
12
+ if (!isNonnegativeSafeInteger(input.expectedHead)) {
13
+ return reject("grant-view-invalid", ["expectedHead must be a nonnegative safe integer"]);
14
+ }
15
+ const authorization = await authorize(input);
16
+ if (!authorization.ok) return authorization.failure;
17
+ return await commitAuthorizedWake(input.inbox, attempt, authorization.grant, wake);
18
+ }
19
+ async function resolvePriorAttempt(inbox, attempt, wake) {
20
+ let resolution;
21
+ try {
22
+ resolution = await inbox.resolveAttempt(attempt);
23
+ } catch (error) {
24
+ return completed(reject("inbox-unavailable", [errorMessage(error)]));
25
+ }
26
+ if (!isRecord(resolution) || typeof resolution.kind !== "string") {
27
+ return completed(reject("inbox-unavailable", ["inbox returned an invalid attempt resolution"]));
28
+ }
29
+ if (resolution.kind === "jwt-id-collision" || resolution.kind === "wake-id-collision") {
30
+ return completed(reject(resolution.kind));
31
+ }
32
+ if (resolution.kind === "duplicate") {
33
+ const receiptFailure = validateReceipt(resolution.receipt, attempt);
34
+ return completed(receiptFailure ?? success("duplicate", snapshotReceipt(resolution.receipt), wake));
35
+ }
36
+ if (resolution.kind !== "new") {
37
+ return completed(reject("inbox-unavailable", ["inbox returned an unknown attempt resolution"]));
38
+ }
39
+ return Object.freeze({ pending: true });
40
+ }
41
+ async function commitAuthorizedWake(inbox, attempt, grant, wake) {
42
+ let committed;
43
+ try {
44
+ committed = await inbox.commitAuthorized({
45
+ attempt,
46
+ grant,
47
+ wake
48
+ });
49
+ } catch (error) {
50
+ return reject("inbox-unavailable", [errorMessage(error)]);
51
+ }
52
+ if (!isRecord(committed) || typeof committed.kind !== "string") {
53
+ return reject("inbox-unavailable", ["inbox returned an invalid commit result"]);
54
+ }
55
+ if (committed.kind === "jwt-id-collision" || committed.kind === "wake-id-collision") {
56
+ return reject(committed.kind);
57
+ }
58
+ if (committed.kind !== "duplicate" && committed.kind !== "enqueued") {
59
+ return reject("inbox-unavailable", ["inbox returned an unknown commit result"]);
60
+ }
61
+ const receiptFailure = validateReceipt(committed.receipt, attempt);
62
+ if (receiptFailure !== void 0) return receiptFailure;
63
+ return success(committed.kind, snapshotReceipt(committed.receipt), wake);
64
+ }
65
+ function completed(result) {
66
+ return Object.freeze({ pending: false, result });
67
+ }
68
+ function createAttempt(verified) {
69
+ if (verified.expiresAt > Number.MAX_SAFE_INTEGER - EVENT_KIT_REPLAY_RETENTION_SKEW_SECONDS) {
70
+ return {
71
+ failure: reject("inbox-unavailable", ["JWT expiry cannot be represented through the replay-retention window"]),
72
+ ok: false
73
+ };
74
+ }
75
+ return {
76
+ attempt: Object.freeze({
77
+ audience: verified.audience,
78
+ eventHash: verified.eventHash,
79
+ jwtId: verified.jwtId,
80
+ publisher: verified.publisher,
81
+ retainJwtUntil: verified.expiresAt + EVENT_KIT_REPLAY_RETENTION_SKEW_SECONDS,
82
+ wakeId: verified.wakeId
83
+ }),
84
+ ok: true
85
+ };
86
+ }
87
+ async function authorize(input) {
88
+ const { route } = input.verified;
89
+ let decision;
90
+ try {
91
+ decision = await input.grants.authorizePublisher({
92
+ controller: route.controller,
93
+ deploymentId: route.deploymentId,
94
+ expectedHead: input.expectedHead,
95
+ publisher: input.verified.publisher,
96
+ ...route.queueId === void 0 ? {} : { queueId: route.queueId }
97
+ });
98
+ } catch (error) {
99
+ return { failure: reject("grant-unavailable", [errorMessage(error)]), ok: false };
100
+ }
101
+ const viewFailure = validateGrantView(decision, input.expectedHead, route.maxGrantLagBlocks);
102
+ if (viewFailure !== void 0) return { failure: viewFailure, ok: false };
103
+ if (!decision.enabled) return { failure: reject("grant-missing"), ok: false };
104
+ const evidenceFailure = validateGrantEvidence(decision.grant, input.verified, decision.atBlock);
105
+ if (evidenceFailure !== void 0) return { failure: evidenceFailure, ok: false };
106
+ const grant = Object.freeze({
107
+ atBlock: decision.atBlock,
108
+ expectedHead: input.expectedHead,
109
+ grant: snapshotGrantEvidence(decision.grant),
110
+ lag: input.expectedHead - decision.atBlock
111
+ });
112
+ return { grant, ok: true };
113
+ }
114
+ function validateGrantView(decision, expectedHead, maxGrantLagBlocks) {
115
+ if (!isRecord(decision) || !Object.hasOwn(decision, "enabled") || typeof decision.enabled !== "boolean" || !Object.hasOwn(decision, "atBlock")) {
116
+ return reject("grant-view-invalid", ["grant authorizer returned an invalid decision"]);
117
+ }
118
+ if (!isNonnegativeSafeInteger(decision.atBlock)) {
119
+ return reject("grant-view-invalid", ["grant atBlock must be a nonnegative safe integer"]);
120
+ }
121
+ if (decision.atBlock > expectedHead) {
122
+ return reject("grant-view-invalid", ["grant view is ahead of the trusted finalized head"]);
123
+ }
124
+ const lag = expectedHead - decision.atBlock;
125
+ if (decision.expectedHead !== void 0 && decision.expectedHead !== expectedHead) {
126
+ return reject("grant-view-invalid", ["grant diagnostic expectedHead disagrees with the trusted head"]);
127
+ }
128
+ if (decision.lag !== void 0 && decision.lag !== lag) {
129
+ return reject("grant-view-invalid", ["grant diagnostic lag disagrees with the recomputed lag"]);
130
+ }
131
+ if (lag > maxGrantLagBlocks) {
132
+ return reject("grant-view-stale", [`grantLag=${lag} max=${maxGrantLagBlocks}`]);
133
+ }
134
+ return void 0;
135
+ }
136
+ function validateGrantEvidence(evidence, verified, atBlock) {
137
+ if (!isRecord(evidence)) return reject("grant-view-invalid", ["enabled grant decision omitted evidence"]);
138
+ const requiredFields = [
139
+ "assertedAtBlock",
140
+ "controller",
141
+ "deploymentId",
142
+ "id",
143
+ "target"
144
+ ];
145
+ if (requiredFields.some((field) => !Object.hasOwn(evidence, field))) {
146
+ return reject("grant-view-invalid", ["enabled grant evidence requires own canonical fields"]);
147
+ }
148
+ if (typeof evidence.id !== "string" || evidence.id.length === 0) {
149
+ return reject("grant-view-invalid", ["enabled grant evidence requires a non-empty id"]);
150
+ }
151
+ if (!isNonnegativeSafeInteger(evidence.assertedAtBlock) || evidence.assertedAtBlock > atBlock) {
152
+ return reject("grant-view-invalid", ["grant assertion block must be at or before the evaluated snapshot"]);
153
+ }
154
+ if (evidence.controller !== verified.route.controller) {
155
+ return reject("grant-view-invalid", ["grant controller does not match the configured controller"]);
156
+ }
157
+ if (evidence.target !== verified.publisher) {
158
+ return reject("grant-view-invalid", ["grant target does not match the verified publisher"]);
159
+ }
160
+ const evidenceHasQueue = Object.hasOwn(evidence, "queueId");
161
+ const routeHasQueue = Object.hasOwn(verified.route, "queueId");
162
+ if (evidence.deploymentId !== verified.route.deploymentId || evidenceHasQueue !== routeHasQueue || evidence.queueId !== verified.route.queueId) {
163
+ return reject("grant-view-invalid", ["grant scope does not exactly match the configured route"]);
164
+ }
165
+ return void 0;
166
+ }
167
+ function validateReceipt(receipt, attempt) {
168
+ const requiredFields = [
169
+ "audience",
170
+ "eventHash",
171
+ "id",
172
+ "publisher",
173
+ "wakeId"
174
+ ];
175
+ if (!isRecord(receipt) || requiredFields.some((field) => !Object.hasOwn(receipt, field)) || typeof receipt.id !== "string" || receipt.id.length === 0) {
176
+ return reject("inbox-unavailable", ["inbox returned an invalid receipt"]);
177
+ }
178
+ if (receipt.audience !== attempt.audience || receipt.publisher !== attempt.publisher || receipt.eventHash !== attempt.eventHash || receipt.wakeId !== attempt.wakeId) {
179
+ return reject("inbox-unavailable", ["inbox receipt is not bound to the verified attempt"]);
180
+ }
181
+ return void 0;
182
+ }
183
+ function snapshotGrantEvidence(evidence) {
184
+ return Object.freeze({
185
+ assertedAtBlock: evidence.assertedAtBlock,
186
+ controller: evidence.controller,
187
+ deploymentId: evidence.deploymentId,
188
+ id: evidence.id,
189
+ ...evidence.queueId === void 0 ? {} : { queueId: evidence.queueId },
190
+ target: evidence.target
191
+ });
192
+ }
193
+ function snapshotReceipt(receipt) {
194
+ return Object.freeze({
195
+ audience: receipt.audience,
196
+ eventHash: receipt.eventHash,
197
+ id: receipt.id,
198
+ publisher: receipt.publisher,
199
+ wakeId: receipt.wakeId
200
+ });
201
+ }
202
+ function toAdmittedWake(verified) {
203
+ return Object.freeze({
204
+ audience: verified.audience,
205
+ deploymentId: verified.deploymentId,
206
+ event: verified.event,
207
+ eventHash: verified.eventHash,
208
+ fromBlock: verified.fromBlock,
209
+ jti: verified.jwtId,
210
+ publisher: verified.publisher,
211
+ ...verified.queueId === void 0 ? {} : { queueId: verified.queueId },
212
+ toBlock: verified.toBlock,
213
+ wakeId: verified.wakeId
214
+ });
215
+ }
216
+ function success(disposition, receipt, wake) {
217
+ return Object.freeze({
218
+ disposition,
219
+ ok: true,
220
+ receipt,
221
+ wake
222
+ });
223
+ }
224
+ function reject(reason, details) {
225
+ if (details === void 0) return Object.freeze({ ok: false, reason });
226
+ return Object.freeze({
227
+ details: Object.freeze([...details]),
228
+ ok: false,
229
+ reason
230
+ });
231
+ }
232
+ function isNonnegativeSafeInteger(value) {
233
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
234
+ }
235
+ function isRecord(value) {
236
+ return typeof value === "object" && value !== null && !Array.isArray(value);
237
+ }
238
+ function errorMessage(error) {
239
+ return error instanceof Error ? error.message : String(error);
240
+ }
241
+
242
+ // src/memoryStatementGrantGraph.ts
243
+ import {
244
+ normalizeAddress,
245
+ WakeGrantObjectSchema,
246
+ WakeGrantRevocationSchema
247
+ } from "@xyo-network/event-kit-schemas";
248
+ function createMemoryStatementGrantGraph(initialHead = 0) {
249
+ let head = initialHead;
250
+ const claims = /* @__PURE__ */ new Map();
251
+ const objects = /* @__PURE__ */ new Map();
252
+ function claimKey(source, objectHash) {
253
+ return `${normalizeAddress(source)}|${objectHash}`;
254
+ }
255
+ function materialize(input) {
256
+ const source = normalizeAddress(input.source);
257
+ objects.set(input.objectHash, Object.freeze({
258
+ assurance: "resolved",
259
+ declaredSchema: input.declaredSchema,
260
+ objectHash: input.objectHash,
261
+ params: Object.freeze({ ...input.params })
262
+ }));
263
+ const key = claimKey(source, input.objectHash);
264
+ const existing = claims.get(key);
265
+ claims.set(key, Object.freeze(existing === void 0 ? {
266
+ declaredObjectSchema: input.declaredSchema,
267
+ firstBlock: input.block,
268
+ firstEventHash: input.eventHash,
269
+ lastBlock: input.block,
270
+ objectHash: input.objectHash,
271
+ source
272
+ } : { ...existing, lastBlock: Math.max(existing.lastBlock, input.block) }));
273
+ if (input.block > head) head = input.block;
274
+ }
275
+ return {
276
+ async head() {
277
+ return head;
278
+ },
279
+ setHead(block) {
280
+ head = block;
281
+ },
282
+ async claims(source, query) {
283
+ const normalizedSource = normalizeAddress(source);
284
+ const records = [];
285
+ for (const record of claims.values()) {
286
+ if (record.source !== normalizedSource) continue;
287
+ if (query?.objectHash !== void 0 && record.objectHash !== query.objectHash) continue;
288
+ if (query?.declaredObjectSchemaPrefix !== void 0 && !record.declaredObjectSchema.startsWith(query.declaredObjectSchemaPrefix)) {
289
+ continue;
290
+ }
291
+ records.push(record);
292
+ }
293
+ return records;
294
+ },
295
+ async object(objectHash) {
296
+ return objects.get(objectHash);
297
+ },
298
+ claim(input) {
299
+ if (input.grant.schema !== WakeGrantObjectSchema) {
300
+ throw new Error(`expected ${WakeGrantObjectSchema}`);
301
+ }
302
+ const { schema: _schema, ...params } = input.grant;
303
+ materialize({
304
+ block: input.block,
305
+ declaredSchema: WakeGrantObjectSchema,
306
+ eventHash: input.eventHash,
307
+ objectHash: input.objectHash,
308
+ params: {
309
+ ...params,
310
+ permissions: Object.freeze([...params.permissions])
311
+ },
312
+ source: input.source
313
+ });
314
+ },
315
+ revoke(input) {
316
+ const revocation = { grant: input.grantObjectHash };
317
+ materialize({
318
+ block: input.block,
319
+ declaredSchema: WakeGrantRevocationSchema,
320
+ eventHash: input.eventHash,
321
+ objectHash: input.objectHash,
322
+ params: revocation,
323
+ source: input.source
324
+ });
325
+ }
326
+ };
327
+ }
328
+
329
+ // src/memoryWakeInbox.ts
330
+ function createMemoryWakeInbox(options = {}) {
331
+ const tokenRecords = /* @__PURE__ */ new Map();
332
+ const wakeRecords = /* @__PURE__ */ new Map();
333
+ const queued = [];
334
+ let receiptSequence = 0;
335
+ let tail = Promise.resolve();
336
+ function exclusive(work) {
337
+ const run = tail.then(work, work);
338
+ tail = Promise.allSettled([run]);
339
+ return run;
340
+ }
341
+ function pruneExpiredTokens() {
342
+ const { nowSeconds } = options;
343
+ if (nowSeconds === void 0) return;
344
+ const now = nowSeconds();
345
+ for (const [key, binding] of tokenRecords) {
346
+ if (binding.retainJwtUntil <= now) tokenRecords.delete(key);
347
+ }
348
+ }
349
+ function matchRecorded(attempt) {
350
+ const tokenBinding = tokenRecords.get(tokenKey(attempt));
351
+ if (tokenBinding !== void 0) {
352
+ return tokenBinding.eventHash === attempt.eventHash && tokenBinding.wakeId === attempt.wakeId ? { kind: "duplicate", receipt: tokenBinding.receipt } : { kind: "jwt-id-collision" };
353
+ }
354
+ const wakeBinding = wakeRecords.get(wakeKey(attempt));
355
+ if (wakeBinding === void 0) return void 0;
356
+ if (wakeBinding.eventHash !== attempt.eventHash) return { kind: "wake-id-collision" };
357
+ tokenRecords.set(tokenKey(attempt), { ...wakeBinding, retainJwtUntil: attempt.retainJwtUntil });
358
+ return { kind: "duplicate", receipt: wakeBinding.receipt };
359
+ }
360
+ return {
361
+ get queued() {
362
+ return queued;
363
+ },
364
+ get tokenRecordCount() {
365
+ return tokenRecords.size;
366
+ },
367
+ get wakeRecordCount() {
368
+ return wakeRecords.size;
369
+ },
370
+ async resolveAttempt(attempt) {
371
+ return await exclusive(async () => {
372
+ pruneExpiredTokens();
373
+ return matchRecorded(attempt) ?? { kind: "new" };
374
+ });
375
+ },
376
+ async commitAuthorized({ attempt, wake }) {
377
+ return await exclusive(async () => {
378
+ pruneExpiredTokens();
379
+ const recorded = matchRecorded(attempt);
380
+ if (recorded !== void 0) return recorded;
381
+ const receipt = Object.freeze({
382
+ audience: attempt.audience,
383
+ eventHash: attempt.eventHash,
384
+ id: `receipt-${receiptSequence += 1}`,
385
+ publisher: attempt.publisher,
386
+ wakeId: attempt.wakeId
387
+ });
388
+ await options.enqueue?.(wake);
389
+ queued.push(wake);
390
+ const binding = {
391
+ eventHash: attempt.eventHash,
392
+ receipt,
393
+ wakeId: attempt.wakeId
394
+ };
395
+ tokenRecords.set(tokenKey(attempt), { ...binding, retainJwtUntil: attempt.retainJwtUntil });
396
+ wakeRecords.set(wakeKey(attempt), binding);
397
+ return { kind: "enqueued", receipt };
398
+ });
399
+ }
400
+ };
401
+ }
402
+ function tokenKey(attempt) {
403
+ return JSON.stringify([attempt.audience, attempt.publisher, attempt.jwtId]);
404
+ }
405
+ function wakeKey(attempt) {
406
+ return JSON.stringify([attempt.audience, attempt.publisher, attempt.wakeId]);
407
+ }
408
+
409
+ // src/memoryWakeQueue.ts
410
+ function createMemoryWakeQueue() {
411
+ const pending = [];
412
+ const inflight = /* @__PURE__ */ new Map();
413
+ let sequence = 0;
414
+ return {
415
+ async append(wake) {
416
+ pending.push({ sequence: sequence += 1, wake });
417
+ },
418
+ async claim() {
419
+ const entry = pending.shift();
420
+ if (entry === void 0) return void 0;
421
+ inflight.set(entry.sequence, entry);
422
+ return {
423
+ wake: entry.wake,
424
+ async ack() {
425
+ inflight.delete(entry.sequence);
426
+ },
427
+ async release() {
428
+ if (!inflight.delete(entry.sequence)) return;
429
+ pending.push(entry);
430
+ pending.sort((left, right) => left.sequence - right.sequence);
431
+ }
432
+ };
433
+ },
434
+ async inflightCount() {
435
+ return inflight.size;
436
+ },
437
+ async pendingCount() {
438
+ return pending.length;
439
+ },
440
+ async recoverInflight() {
441
+ const recovered = inflight.size;
442
+ for (const entry of inflight.values()) pending.push(entry);
443
+ inflight.clear();
444
+ pending.sort((left, right) => left.sequence - right.sequence);
445
+ return recovered;
446
+ }
447
+ };
448
+ }
449
+
450
+ // src/statementGraphWakeGrantAuthorizer.ts
451
+ import {
452
+ asWakeGrantObject,
453
+ asWakeGrantRevocation,
454
+ DeploymentIdZod,
455
+ HashZod,
456
+ NormalizedAddressZod,
457
+ WakeGrantObjectSchema as WakeGrantObjectSchema2,
458
+ WakeGrantRevocationSchema as WakeGrantRevocationSchema2
459
+ } from "@xyo-network/event-kit-schemas";
460
+ function createStatementGraphWakeGrantAuthorizer(options) {
461
+ const { viewer } = options;
462
+ return Object.freeze({
463
+ async authorizePublisher(request) {
464
+ assertValidRequest(request);
465
+ const atBlock = await readHead(viewer);
466
+ if (atBlock > request.expectedHead) {
467
+ throw new Error(`Statement Graph head ${atBlock} is ahead of expected head ${request.expectedHead}`);
468
+ }
469
+ const claims = await viewer.claims(request.controller, { declaredObjectSchemaPrefix: WakeGrantObjectSchema2 });
470
+ const revokedGrantHashes = await readRevocations(viewer, request.controller, atBlock);
471
+ let evidence;
472
+ for (const claim of claims) {
473
+ evidence = await resolveEvidence(viewer, claim, request, atBlock, revokedGrantHashes);
474
+ if (evidence !== void 0) break;
475
+ }
476
+ const completedAtBlock = await readHead(viewer);
477
+ if (completedAtBlock !== atBlock) {
478
+ throw new Error(`Statement Graph head changed during authorization [${atBlock} -> ${completedAtBlock}]`);
479
+ }
480
+ const lag = request.expectedHead - atBlock;
481
+ if (evidence === void 0) {
482
+ return Object.freeze({
483
+ atBlock,
484
+ enabled: false,
485
+ expectedHead: request.expectedHead,
486
+ lag
487
+ });
488
+ }
489
+ return Object.freeze({
490
+ atBlock,
491
+ enabled: true,
492
+ expectedHead: request.expectedHead,
493
+ grant: evidence,
494
+ lag
495
+ });
496
+ }
497
+ });
498
+ }
499
+ async function readRevocations(viewer, controller, atBlock) {
500
+ const revoked = /* @__PURE__ */ new Set();
501
+ const claims = await viewer.claims(controller, { declaredObjectSchemaPrefix: WakeGrantRevocationSchema2 });
502
+ for (const claim of claims) {
503
+ if (!isLiveClaim(claim, controller, WakeGrantRevocationSchema2, atBlock)) continue;
504
+ const object = await viewer.object(claim.objectHash);
505
+ if (object?.assurance !== "resolved") continue;
506
+ if (object.declaredSchema !== WakeGrantRevocationSchema2 || object.objectHash !== claim.objectHash) continue;
507
+ if (!isRecord2(object.params) || Object.hasOwn(object.params, "schema")) continue;
508
+ const revocation = asWakeGrantRevocation({ ...object.params, schema: object.declaredSchema });
509
+ if (revocation !== void 0) revoked.add(revocation.grant);
510
+ }
511
+ return revoked;
512
+ }
513
+ async function resolveEvidence(viewer, claim, request, atBlock, revokedGrantHashes) {
514
+ if (!isLiveClaim(claim, request.controller, WakeGrantObjectSchema2, atBlock)) return void 0;
515
+ if (revokedGrantHashes.has(claim.objectHash)) return void 0;
516
+ const object = await viewer.object(claim.objectHash);
517
+ const grant = parseExactGrantObject(object, claim.objectHash);
518
+ if (grant === void 0) return void 0;
519
+ if (grant.publisher !== request.publisher) return void 0;
520
+ if (grant.deploymentId !== request.deploymentId) return void 0;
521
+ if (grant.queueId !== request.queueId) return void 0;
522
+ if (!grant.permissions.includes("enqueue")) return void 0;
523
+ return Object.freeze({
524
+ assertedAtBlock: claim.firstBlock,
525
+ controller: request.controller,
526
+ deploymentId: grant.deploymentId,
527
+ id: claim.firstEventHash,
528
+ ...grant.queueId === void 0 ? {} : { queueId: grant.queueId },
529
+ target: grant.publisher
530
+ });
531
+ }
532
+ function isLiveClaim(claim, controller, declaredObjectSchema, atBlock) {
533
+ if (claim.source !== controller) return false;
534
+ if (claim.declaredObjectSchema !== declaredObjectSchema) return false;
535
+ if (!isNonnegativeSafeInteger2(claim.firstBlock) || claim.firstBlock > atBlock) return false;
536
+ if (!isNonnegativeSafeInteger2(claim.lastBlock) || claim.lastBlock < claim.firstBlock) return false;
537
+ if (!HashZod.safeParse(claim.firstEventHash).success) return false;
538
+ return HashZod.safeParse(claim.objectHash).success;
539
+ }
540
+ function parseExactGrantObject(object, expectedObjectHash) {
541
+ if (object?.assurance !== "resolved") return void 0;
542
+ if (object.declaredSchema !== WakeGrantObjectSchema2 || object.objectHash !== expectedObjectHash) return void 0;
543
+ if (!isRecord2(object.params) || Object.hasOwn(object.params, "schema")) return void 0;
544
+ return asWakeGrantObject({
545
+ ...object.params,
546
+ schema: object.declaredSchema
547
+ });
548
+ }
549
+ function assertValidRequest(request) {
550
+ if (!NormalizedAddressZod.safeParse(request.controller).success) {
551
+ throw new TypeError("Grant controller must be a normalized address");
552
+ }
553
+ if (!NormalizedAddressZod.safeParse(request.publisher).success) {
554
+ throw new TypeError("Grant publisher must be a normalized address");
555
+ }
556
+ if (!DeploymentIdZod.safeParse(request.deploymentId).success) {
557
+ throw new TypeError("Grant deploymentId must contain 1 to 255 characters");
558
+ }
559
+ if (request.queueId !== void 0 && (request.queueId.length === 0 || request.queueId.length > 255)) {
560
+ throw new TypeError("Grant queueId must contain 1 to 255 characters when configured");
561
+ }
562
+ if (!isNonnegativeSafeInteger2(request.expectedHead)) {
563
+ throw new RangeError("Grant expectedHead must be a nonnegative safe integer");
564
+ }
565
+ }
566
+ async function readHead(viewer) {
567
+ const head = await viewer.head();
568
+ if (!isNonnegativeSafeInteger2(head)) {
569
+ throw new RangeError("Statement Graph head must be a nonnegative safe integer");
570
+ }
571
+ return head;
572
+ }
573
+ function isNonnegativeSafeInteger2(value) {
574
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
575
+ }
576
+ function isRecord2(value) {
577
+ return typeof value === "object" && value !== null && !Array.isArray(value);
578
+ }
579
+
580
+ // src/verifyWakeRequest.ts
581
+ import {
582
+ DeploymentIdZod as DeploymentIdZod2,
583
+ NormalizedAddressZod as NormalizedAddressZod2,
584
+ WakeEnvelopeZod,
585
+ WakeIdRegExp,
586
+ WakeJwtAudienceZod,
587
+ WakeJwtHeaderZod,
588
+ WakeJwtPayloadZod,
589
+ WakeJwtSchema
590
+ } from "@xyo-network/event-kit-schemas";
591
+ import { Account, PayloadBuilder } from "@xyo-network/sdk";
592
+ var EVENT_KIT_MAX_JWT_LIFETIME_SECONDS = 300;
593
+ var EVENT_KIT_MAX_FUTURE_IAT_SECONDS = 60;
594
+ async function verifyWakeRequest(input) {
595
+ const { nowSeconds } = input;
596
+ const route = snapshotRoute(input.route);
597
+ const configurationFailure = validateConfiguration(route, nowSeconds);
598
+ if (configurationFailure !== void 0) return configurationFailure;
599
+ const parsed = WakeEnvelopeZod.safeParse(input.body);
600
+ if (!parsed.success) {
601
+ const details = parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`);
602
+ if (hasInvertedSafeRange(input.body)) return reject2("range-invalid", details);
603
+ if (isRecord3(input.body) && Object.hasOwn(input.body, "wakeId") && parsed.error.issues.some((issue) => issue.path[0] === "wakeId")) {
604
+ return reject2("wake-id-invalid", details);
605
+ }
606
+ return reject2(
607
+ "event-malformed",
608
+ details
609
+ );
610
+ }
611
+ const event = parsed.data;
612
+ if (!WakeIdRegExp.test(event.wakeId)) return reject2("wake-id-invalid");
613
+ if (!hasSafeEventNumbers(event)) return reject2("event-malformed", ["wake numeric fields must be nonnegative safe integers"]);
614
+ if (event.deploymentId !== route.deploymentId || event.queueId !== route.queueId) {
615
+ return reject2("event-target-mismatch");
616
+ }
617
+ if (event.fromBlock > event.toBlock) return reject2("range-invalid");
618
+ if (event.toBlock - event.fromBlock >= route.maxBlocksPerWake) {
619
+ return reject2("range-too-large", [`maxBlocksPerWake=${route.maxBlocksPerWake}`]);
620
+ }
621
+ const jwtResult = await verifyJwtProfile(input.jwt, route.audience, nowSeconds);
622
+ if (!jwtResult.ok) return jwtResult;
623
+ let eventHash;
624
+ try {
625
+ eventHash = await PayloadBuilder.dataHash(event);
626
+ } catch (error) {
627
+ return reject2("event-hash-failed", [errorMessage2(error)]);
628
+ }
629
+ if (eventHash !== jwtResult.jwt.eventHash) return reject2("event-hash-mismatch");
630
+ const frozenEvent = Object.freeze({ ...event });
631
+ const verified = Object.freeze({
632
+ audience: route.audience,
633
+ deploymentId: event.deploymentId,
634
+ event: frozenEvent,
635
+ eventHash,
636
+ expiresAt: jwtResult.jwt.expiresAt,
637
+ fromBlock: event.fromBlock,
638
+ issuedAt: event.issuedAt,
639
+ jwtId: jwtResult.jwt.jwtId,
640
+ publisher: jwtResult.jwt.publisher,
641
+ ...event.queueId === void 0 ? {} : { queueId: event.queueId },
642
+ route,
643
+ toBlock: event.toBlock,
644
+ tokenIssuedAt: jwtResult.jwt.issuedAt,
645
+ wakeId: event.wakeId
646
+ });
647
+ return Object.freeze({ ok: true, verified });
648
+ }
649
+ function validateConfiguration(route, nowSeconds) {
650
+ if (!WakeJwtAudienceZod.safeParse(route.audience).success) {
651
+ return reject2("configuration-invalid", ["audience must not be empty"]);
652
+ }
653
+ if (!DeploymentIdZod2.safeParse(route.deploymentId).success) {
654
+ return reject2("configuration-invalid", ["deploymentId must contain 1 to 255 characters"]);
655
+ }
656
+ if (route.queueId !== void 0 && (route.queueId.length === 0 || route.queueId.length > 255)) {
657
+ return reject2("configuration-invalid", ["queueId must contain 1 to 255 characters when configured"]);
658
+ }
659
+ if (!NormalizedAddressZod2.safeParse(route.controller).success) {
660
+ return reject2("configuration-invalid", ["controller must be a normalized 40-character lowercase-hex address"]);
661
+ }
662
+ if (!isNonnegativeSafeInteger3(nowSeconds)) {
663
+ return reject2("configuration-invalid", ["nowSeconds must be a nonnegative safe integer"]);
664
+ }
665
+ if (!Number.isSafeInteger(route.maxBlocksPerWake) || route.maxBlocksPerWake < 1) {
666
+ return reject2("configuration-invalid", ["maxBlocksPerWake must be a positive safe integer"]);
667
+ }
668
+ if (!isNonnegativeSafeInteger3(route.maxGrantLagBlocks)) {
669
+ return reject2("configuration-invalid", ["maxGrantLagBlocks must be a nonnegative safe integer"]);
670
+ }
671
+ return void 0;
672
+ }
673
+ async function verifyJwtProfile(token, audience, nowSeconds) {
674
+ let verified;
675
+ try {
676
+ verified = await Account.verifyJwt(token, { audience, now: nowSeconds });
677
+ } catch (error) {
678
+ return reject2("jwt-malformed", [errorMessage2(error)]);
679
+ }
680
+ if (!verified.ok) {
681
+ if (Object.hasOwn(verified.payload ?? {}, "nbf")) return reject2("jwt-nbf-not-allowed");
682
+ return reject2(classifySdkJwtFailure(verified.reasons), verified.reasons);
683
+ }
684
+ const wireResult = parseStrictJwtWire(verified.header, verified.payload, audience);
685
+ if (!wireResult.ok) return wireResult;
686
+ const { header: strictHeader, payload: strictPayload } = wireResult;
687
+ if (strictHeader.kid !== strictPayload.iss) return reject2("jwt-identity-invalid");
688
+ if (strictPayload.exp <= nowSeconds) return reject2("jwt-expired");
689
+ if (strictPayload.exp - strictPayload.iat > EVENT_KIT_MAX_JWT_LIFETIME_SECONDS) return reject2("jwt-lifetime-invalid");
690
+ if (strictPayload.iat - nowSeconds > EVENT_KIT_MAX_FUTURE_IAT_SECONDS) return reject2("jwt-future-iat");
691
+ const jwt = Object.freeze({
692
+ eventHash: strictPayload.eventHash,
693
+ expiresAt: strictPayload.exp,
694
+ issuedAt: strictPayload.iat,
695
+ jwtId: strictPayload.jti,
696
+ publisher: strictPayload.iss
697
+ });
698
+ return Object.freeze({ jwt, ok: true });
699
+ }
700
+ function parseStrictJwtWire(header, payload, audience) {
701
+ const headerResult = WakeJwtHeaderZod.safeParse(header);
702
+ if (!headerResult.success) {
703
+ if (!NormalizedAddressZod2.safeParse(header.kid).success) return reject2("jwt-identity-invalid");
704
+ return reject2("jwt-header-invalid", zodDetails(headerResult.error.issues));
705
+ }
706
+ if (Object.hasOwn(payload, "nbf")) return reject2("jwt-nbf-not-allowed");
707
+ if (!NormalizedAddressZod2.safeParse(payload.iss).success) return reject2("jwt-identity-invalid");
708
+ if (payload.schema !== WakeJwtSchema) return reject2("jwt-schema-mismatch");
709
+ if (payload.aud !== audience) return reject2("jwt-audience-mismatch");
710
+ if (!isNonnegativeSafeInteger3(payload.iat) || !isNonnegativeSafeInteger3(payload.exp) || payload.exp <= payload.iat) {
711
+ return reject2("jwt-lifetime-invalid");
712
+ }
713
+ const payloadResult = WakeJwtPayloadZod.safeParse(payload);
714
+ if (!payloadResult.success) return reject2("jwt-claims-invalid", zodDetails(payloadResult.error.issues));
715
+ return Object.freeze({
716
+ header: headerResult.data,
717
+ ok: true,
718
+ payload: payloadResult.data
719
+ });
720
+ }
721
+ function classifySdkJwtFailure(reasons) {
722
+ if (reasons.some((reason) => reason.startsWith("audience mismatch:"))) return "jwt-audience-mismatch";
723
+ if (reasons.some((reason) => reason.startsWith("expired:"))) return "jwt-expired";
724
+ if (reasons.some((reason) => reason.startsWith("unsupported alg:") || reason.startsWith("unsupported typ:"))) {
725
+ return "jwt-header-invalid";
726
+ }
727
+ if (reasons.some((reason) => reason.startsWith("invalid public key:") || reason.startsWith("public key does not derive to kid:") || reason === "header.kid does not match payload.iss")) {
728
+ return "jwt-identity-invalid";
729
+ }
730
+ return "jwt-invalid";
731
+ }
732
+ function hasSafeEventNumbers(event) {
733
+ return isNonnegativeSafeInteger3(event.fromBlock) && isNonnegativeSafeInteger3(event.toBlock) && isNonnegativeSafeInteger3(event.issuedAt);
734
+ }
735
+ function hasInvertedSafeRange(value) {
736
+ if (!isRecord3(value)) return false;
737
+ const { fromBlock, toBlock } = value;
738
+ return typeof fromBlock === "number" && typeof toBlock === "number" && isNonnegativeSafeInteger3(fromBlock) && isNonnegativeSafeInteger3(toBlock) && fromBlock > toBlock;
739
+ }
740
+ function isNonnegativeSafeInteger3(value) {
741
+ return Number.isSafeInteger(value) && value >= 0;
742
+ }
743
+ function isRecord3(value) {
744
+ return typeof value === "object" && value !== null && !Array.isArray(value);
745
+ }
746
+ function snapshotRoute(route) {
747
+ return Object.freeze({
748
+ audience: route.audience,
749
+ controller: route.controller,
750
+ deploymentId: route.deploymentId,
751
+ maxBlocksPerWake: route.maxBlocksPerWake,
752
+ maxGrantLagBlocks: route.maxGrantLagBlocks,
753
+ ...route.queueId === void 0 ? {} : { queueId: route.queueId }
754
+ });
755
+ }
756
+ function zodDetails(issues) {
757
+ return issues.map((issue) => {
758
+ const path = issue.path.map(String).join(".") || "<root>";
759
+ return `${path}: ${issue.message}`;
760
+ });
761
+ }
762
+ function reject2(reason, details) {
763
+ if (details === void 0) return Object.freeze({ ok: false, reason });
764
+ return Object.freeze({
765
+ details: Object.freeze([...details]),
766
+ ok: false,
767
+ reason
768
+ });
769
+ }
770
+ function errorMessage2(error) {
771
+ return error instanceof Error ? error.message : String(error);
772
+ }
773
+
774
+ // src/wakeQueue.ts
775
+ async function drainQueue(queue, handler) {
776
+ let handled = 0;
777
+ for (; ; ) {
778
+ const leased = await queue.claim();
779
+ if (leased === void 0) return handled;
780
+ try {
781
+ await handler(leased.wake);
782
+ } catch (error) {
783
+ await leased.release();
784
+ throw error;
785
+ }
786
+ await leased.ack();
787
+ handled += 1;
788
+ }
789
+ }
790
+ export {
791
+ EVENT_KIT_MAX_FUTURE_IAT_SECONDS,
792
+ EVENT_KIT_MAX_JWT_LIFETIME_SECONDS,
793
+ EVENT_KIT_REPLAY_RETENTION_SKEW_SECONDS,
794
+ admitAndEnqueue,
795
+ createMemoryStatementGrantGraph,
796
+ createMemoryWakeInbox,
797
+ createMemoryWakeQueue,
798
+ createStatementGraphWakeGrantAuthorizer,
799
+ drainQueue,
800
+ verifyWakeRequest
801
+ };
802
+ //# sourceMappingURL=index.mjs.map