@xyo-network/event-kit-protocol 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -2
- package/dist/neutral/canonicalJson.d.ts +5 -0
- package/dist/neutral/canonicalJson.d.ts.map +1 -0
- package/dist/neutral/endpointChallenge.d.ts +36 -0
- package/dist/neutral/endpointChallenge.d.ts.map +1 -0
- package/dist/neutral/handshakeConstruction.d.ts +91 -0
- package/dist/neutral/handshakeConstruction.d.ts.map +1 -0
- package/dist/neutral/index.d.ts +13 -0
- package/dist/neutral/index.d.ts.map +1 -1
- package/dist/neutral/index.mjs +1067 -47
- package/dist/neutral/index.mjs.map +4 -4
- package/dist/neutral/lifecycleSelection.d.ts +105 -0
- package/dist/neutral/lifecycleSelection.d.ts.map +1 -0
- package/dist/neutral/memoryStatementGrantGraph.d.ts +4 -0
- package/dist/neutral/memoryStatementGrantGraph.d.ts.map +1 -1
- package/dist/neutral/memoryWakeInbox.d.ts +3 -2
- package/dist/neutral/memoryWakeInbox.d.ts.map +1 -1
- package/dist/neutral/publisherHandshakeStores.d.ts +97 -0
- package/dist/neutral/publisherHandshakeStores.d.ts.map +1 -0
- package/dist/neutral/publisherSubscription.d.ts +127 -0
- package/dist/neutral/publisherSubscription.d.ts.map +1 -0
- package/dist/neutral/statementGraphPorts.d.ts +5 -2
- package/dist/neutral/statementGraphPorts.d.ts.map +1 -1
- package/dist/neutral/statementGraphWakeGrantAuthorizer.d.ts +4 -4
- package/dist/neutral/statementGraphWakeGrantAuthorizer.d.ts.map +1 -1
- package/dist/neutral/wakeSubscriptionEncoding.d.ts +12 -0
- package/dist/neutral/wakeSubscriptionEncoding.d.ts.map +1 -0
- package/dist/neutral/wakeSubscriptionHpke.d.ts +52 -0
- package/dist/neutral/wakeSubscriptionHpke.d.ts.map +1 -0
- package/package.json +11 -10
package/dist/neutral/index.mjs
CHANGED
|
@@ -239,28 +239,723 @@ function errorMessage(error) {
|
|
|
239
239
|
return error instanceof Error ? error.message : String(error);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
// src/canonicalJson.ts
|
|
243
|
+
var textEncoder = new TextEncoder();
|
|
244
|
+
function hasUnpairedSurrogate(value) {
|
|
245
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
246
|
+
const codePoint = value.codePointAt(index);
|
|
247
|
+
if (codePoint === void 0 || codePoint >= 55296 && codePoint <= 57343) return true;
|
|
248
|
+
if (codePoint > 65535) index += 1;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
function canonicalize(value) {
|
|
253
|
+
if (value === null) return "null";
|
|
254
|
+
switch (typeof value) {
|
|
255
|
+
case "boolean":
|
|
256
|
+
return value ? "true" : "false";
|
|
257
|
+
case "number": {
|
|
258
|
+
if (!Number.isFinite(value)) throw new TypeError("Canonical JSON numbers must be finite");
|
|
259
|
+
return JSON.stringify(value);
|
|
260
|
+
}
|
|
261
|
+
case "string": {
|
|
262
|
+
if (hasUnpairedSurrogate(value)) throw new TypeError("Canonical JSON strings must be valid Unicode");
|
|
263
|
+
return JSON.stringify(value);
|
|
264
|
+
}
|
|
265
|
+
case "object": {
|
|
266
|
+
if (Array.isArray(value)) return `[${value.map((item) => canonicalize(item)).join(",")}]`;
|
|
267
|
+
const prototype = Reflect.getPrototypeOf(value);
|
|
268
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
269
|
+
throw new TypeError("Canonical JSON objects must be plain records");
|
|
270
|
+
}
|
|
271
|
+
const record = value;
|
|
272
|
+
const members = Object.keys(record).sort().map((key) => {
|
|
273
|
+
if (hasUnpairedSurrogate(key)) throw new TypeError("Canonical JSON keys must be valid Unicode");
|
|
274
|
+
const member = record[key];
|
|
275
|
+
if (member === void 0) throw new TypeError("Canonical JSON does not permit undefined members");
|
|
276
|
+
return `${JSON.stringify(key)}:${canonicalize(member)}`;
|
|
277
|
+
});
|
|
278
|
+
return `{${members.join(",")}}`;
|
|
279
|
+
}
|
|
280
|
+
default:
|
|
281
|
+
throw new TypeError(`Canonical JSON cannot encode ${typeof value}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function canonicalJson(value) {
|
|
285
|
+
return canonicalize(value);
|
|
286
|
+
}
|
|
287
|
+
function canonicalJsonBytes(value) {
|
|
288
|
+
return textEncoder.encode(canonicalize(value));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/endpointChallenge.ts
|
|
292
|
+
import {
|
|
293
|
+
Base64Url32BytesZod,
|
|
294
|
+
bytesToCanonicalBase64Url,
|
|
295
|
+
canonicalBase64UrlToBytes,
|
|
296
|
+
EndpointChallengeResponseSchema,
|
|
297
|
+
EndpointChallengeResponseZod,
|
|
298
|
+
EndpointChallengeSchema,
|
|
299
|
+
EndpointChallengeZod,
|
|
300
|
+
generateChallengeNonce,
|
|
301
|
+
generateHandshakeId,
|
|
302
|
+
MAX_ENDPOINT_CHALLENGE_LIFETIME_SECONDS
|
|
303
|
+
} from "@xyo-network/event-kit-schemas";
|
|
304
|
+
import { PayloadBuilder } from "@xyo-network/sdk";
|
|
305
|
+
function hashBytes(hash) {
|
|
306
|
+
return Uint8Array.from({ length: 32 }, (_, index) => Number.parseInt(hash.slice(index * 2, index * 2 + 2), 16));
|
|
307
|
+
}
|
|
308
|
+
async function hmacProof(secret, challengeHash) {
|
|
309
|
+
const secretBytes = canonicalBase64UrlToBytes(Base64Url32BytesZod.parse(secret));
|
|
310
|
+
if (secretBytes?.length !== 32) throw new TypeError("Expected 32-byte challenge secret");
|
|
311
|
+
const cryptoKey = await globalThis.crypto.subtle.importKey(
|
|
312
|
+
"raw",
|
|
313
|
+
Uint8Array.from(secretBytes),
|
|
314
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
315
|
+
false,
|
|
316
|
+
["sign"]
|
|
317
|
+
);
|
|
318
|
+
const signature = await globalThis.crypto.subtle.sign("HMAC", cryptoKey, hashBytes(challengeHash));
|
|
319
|
+
return Base64Url32BytesZod.parse(bytesToCanonicalBase64Url(new Uint8Array(signature)));
|
|
320
|
+
}
|
|
321
|
+
function equalProof(left, right) {
|
|
322
|
+
const leftBytes = canonicalBase64UrlToBytes(left);
|
|
323
|
+
const rightBytes = canonicalBase64UrlToBytes(right);
|
|
324
|
+
if (leftBytes?.length !== 32 || rightBytes?.length !== 32) return false;
|
|
325
|
+
let difference = 0;
|
|
326
|
+
for (let index = 0; index < 32; index += 1) difference |= leftBytes[index] ^ rightBytes[index];
|
|
327
|
+
return difference === 0;
|
|
328
|
+
}
|
|
329
|
+
async function endpointChallengeHash(challenge) {
|
|
330
|
+
const parsed = EndpointChallengeZod.parse(challenge);
|
|
331
|
+
return await PayloadBuilder.dataHash(parsed);
|
|
332
|
+
}
|
|
333
|
+
function createEndpointChallenge(input) {
|
|
334
|
+
const lifetimeSeconds = input.lifetimeSeconds ?? MAX_ENDPOINT_CHALLENGE_LIFETIME_SECONDS;
|
|
335
|
+
if (!Number.isSafeInteger(lifetimeSeconds) || lifetimeSeconds < 1) {
|
|
336
|
+
throw new RangeError("Challenge lifetime must be a positive safe integer");
|
|
337
|
+
}
|
|
338
|
+
if (!Number.isSafeInteger(input.issuedAt) || input.issuedAt < 0) {
|
|
339
|
+
throw new RangeError("Challenge issuedAt must be a nonnegative safe integer");
|
|
340
|
+
}
|
|
341
|
+
if (input.issuedAt > Number.MAX_SAFE_INTEGER - lifetimeSeconds) {
|
|
342
|
+
throw new RangeError("Challenge expiry exceeds the JSON safe-integer range");
|
|
343
|
+
}
|
|
344
|
+
return EndpointChallengeZod.parse(new PayloadBuilder({ schema: EndpointChallengeSchema }).fields({
|
|
345
|
+
challengeId: generateHandshakeId(),
|
|
346
|
+
chainId: input.chainId,
|
|
347
|
+
publisher: input.publisher,
|
|
348
|
+
subscriber: input.subscriber,
|
|
349
|
+
service: input.service,
|
|
350
|
+
encryptionKey: input.encryptionKey,
|
|
351
|
+
keyId: input.keyId,
|
|
352
|
+
subscription: input.subscription,
|
|
353
|
+
grant: input.grant,
|
|
354
|
+
eventKind: input.eventKind,
|
|
355
|
+
nonce: generateChallengeNonce(),
|
|
356
|
+
issuedAt: input.issuedAt,
|
|
357
|
+
expiresAt: input.issuedAt + lifetimeSeconds
|
|
358
|
+
}).build());
|
|
359
|
+
}
|
|
360
|
+
async function createEndpointChallengeResponse(challenge, secret) {
|
|
361
|
+
const challengeHash = await endpointChallengeHash(challenge);
|
|
362
|
+
return EndpointChallengeResponseZod.parse(new PayloadBuilder({ schema: EndpointChallengeResponseSchema }).fields({
|
|
363
|
+
challenge: challengeHash,
|
|
364
|
+
proof: await hmacProof(secret, challengeHash)
|
|
365
|
+
}).build());
|
|
366
|
+
}
|
|
367
|
+
async function verifyEndpointChallengeProof(input) {
|
|
368
|
+
try {
|
|
369
|
+
const challenge = EndpointChallengeZod.parse(input.challenge);
|
|
370
|
+
const response = EndpointChallengeResponseZod.parse(input.response);
|
|
371
|
+
const challengeHash = await endpointChallengeHash(challenge);
|
|
372
|
+
if (response.challenge !== challengeHash) return false;
|
|
373
|
+
return equalProof(response.proof, await hmacProof(input.secret, challengeHash));
|
|
374
|
+
} catch {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async function verifyEndpointChallengeResponse(input) {
|
|
379
|
+
try {
|
|
380
|
+
const challenge = EndpointChallengeZod.parse(input.challenge);
|
|
381
|
+
if (!Number.isSafeInteger(input.nowSeconds) || input.nowSeconds < challenge.issuedAt || input.nowSeconds >= challenge.expiresAt) return false;
|
|
382
|
+
return await verifyEndpointChallengeProof(input);
|
|
383
|
+
} catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/handshakeConstruction.ts
|
|
389
|
+
import {
|
|
390
|
+
bytesToCanonicalBase64Url as bytesToCanonicalBase64Url3,
|
|
391
|
+
EncryptedWakeSubscriptionSchema,
|
|
392
|
+
EncryptedWakeSubscriptionZod as EncryptedWakeSubscriptionZod3,
|
|
393
|
+
ENDPOINT_CHALLENGE_HMAC_SHA256_PROFILE,
|
|
394
|
+
EndpointChallengeResponseZod as EndpointChallengeResponseZod2,
|
|
395
|
+
EndpointChallengeZod as EndpointChallengeZod2,
|
|
396
|
+
FINALIZED_HEAD_EVENT_KIND,
|
|
397
|
+
FINALIZED_HEAD_FILTER_PROFILE,
|
|
398
|
+
generateGrantId,
|
|
399
|
+
generateHandshakeId as generateHandshakeId2,
|
|
400
|
+
HashZod as HashZod2,
|
|
401
|
+
MAX_SUBSCRIPTION_PLAINTEXT_BYTES as MAX_SUBSCRIPTION_PLAINTEXT_BYTES2,
|
|
402
|
+
PublisherEncryptionKeyRevocationSchema,
|
|
403
|
+
PublisherEncryptionKeyRevocationZod,
|
|
404
|
+
PublisherEncryptionKeySchema,
|
|
405
|
+
PublisherEncryptionKeyZod,
|
|
406
|
+
PublisherServiceRevocationSchema,
|
|
407
|
+
PublisherServiceRevocationZod,
|
|
408
|
+
PublisherServiceSchema,
|
|
409
|
+
PublisherServiceZod,
|
|
410
|
+
WAKE_SUBSCRIPTION_HPKE_PROFILE as WAKE_SUBSCRIPTION_HPKE_PROFILE3,
|
|
411
|
+
WAKE_WALLET_JWT_DELIVERY_PROFILE,
|
|
412
|
+
WakeGrantObjectSchema,
|
|
413
|
+
WakeGrantObjectZod,
|
|
414
|
+
WakeGrantRevocationSchema,
|
|
415
|
+
WakeGrantRevocationZod,
|
|
416
|
+
WakeSubscriptionAcceptanceRevocationSchema,
|
|
417
|
+
WakeSubscriptionAcceptanceRevocationZod,
|
|
418
|
+
WakeSubscriptionAcceptanceSchema,
|
|
419
|
+
WakeSubscriptionAcceptanceZod,
|
|
420
|
+
WakeSubscriptionCleartextZod as WakeSubscriptionCleartextZod3,
|
|
421
|
+
WakeSubscriptionPlaintextSchema,
|
|
422
|
+
WakeSubscriptionPlaintextZod as WakeSubscriptionPlaintextZod2,
|
|
423
|
+
WakeSubscriptionRevocationSchema,
|
|
424
|
+
WakeSubscriptionRevocationZod
|
|
425
|
+
} from "@xyo-network/event-kit-schemas";
|
|
426
|
+
import { PayloadBuilder as PayloadBuilder2 } from "@xyo-network/sdk";
|
|
427
|
+
|
|
428
|
+
// src/wakeSubscriptionHpke.ts
|
|
429
|
+
import {
|
|
430
|
+
Aes128Gcm,
|
|
431
|
+
CipherSuite,
|
|
432
|
+
DhkemX25519HkdfSha256,
|
|
433
|
+
HkdfSha256
|
|
434
|
+
} from "@hpke/core";
|
|
435
|
+
import {
|
|
436
|
+
Base64Url32BytesZod as Base64Url32BytesZod2,
|
|
437
|
+
bytesToCanonicalBase64Url as bytesToCanonicalBase64Url2,
|
|
438
|
+
canonicalBase64UrlToBytes as canonicalBase64UrlToBytes2,
|
|
439
|
+
EncryptedWakeSubscriptionZod as EncryptedWakeSubscriptionZod2,
|
|
440
|
+
HandshakeIdZod,
|
|
441
|
+
HashZod,
|
|
442
|
+
NormalizedAddressZod as NormalizedAddressZod2,
|
|
443
|
+
WAKE_SUBSCRIPTION_HPKE_PROFILE as WAKE_SUBSCRIPTION_HPKE_PROFILE2,
|
|
444
|
+
WakeSubscriptionCleartextZod as WakeSubscriptionCleartextZod2
|
|
445
|
+
} from "@xyo-network/event-kit-schemas";
|
|
446
|
+
|
|
447
|
+
// src/wakeSubscriptionEncoding.ts
|
|
448
|
+
import {
|
|
449
|
+
EncryptedWakeSubscriptionZod,
|
|
450
|
+
MAX_SUBSCRIPTION_PLAINTEXT_BYTES,
|
|
451
|
+
NormalizedAddressZod,
|
|
452
|
+
WAKE_SUBSCRIPTION_HPKE_PROFILE,
|
|
453
|
+
WakeSubscriptionCleartextZod,
|
|
454
|
+
WakeSubscriptionPlaintextZod
|
|
455
|
+
} from "@xyo-network/event-kit-schemas";
|
|
456
|
+
var textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
457
|
+
function equalBytes(left, right) {
|
|
458
|
+
if (left.length !== right.length) return false;
|
|
459
|
+
let difference = 0;
|
|
460
|
+
for (let index = 0; index < left.length; index += 1) difference |= left[index] ^ right[index];
|
|
461
|
+
return difference === 0;
|
|
462
|
+
}
|
|
463
|
+
function cleartextFromEncrypted(subscription) {
|
|
464
|
+
const { encryption: _encryption, ...cleartext } = subscription;
|
|
465
|
+
return cleartext;
|
|
466
|
+
}
|
|
467
|
+
function wakeSubscriptionAadBytes(subscription, subscriber) {
|
|
468
|
+
const parsedSubscriber = NormalizedAddressZod.parse(subscriber);
|
|
469
|
+
const parsedSubscription = "encryption" in subscription ? cleartextFromEncrypted(EncryptedWakeSubscriptionZod.parse(subscription)) : WakeSubscriptionCleartextZod.parse(subscription);
|
|
470
|
+
return canonicalJsonBytes({
|
|
471
|
+
schema: parsedSubscription.schema,
|
|
472
|
+
chainId: parsedSubscription.chainId,
|
|
473
|
+
subscriber: parsedSubscriber,
|
|
474
|
+
publisher: parsedSubscription.publisher,
|
|
475
|
+
service: parsedSubscription.service,
|
|
476
|
+
encryptionKey: parsedSubscription.encryptionKey,
|
|
477
|
+
keyId: parsedSubscription.keyId,
|
|
478
|
+
grant: parsedSubscription.grant,
|
|
479
|
+
subscriptionId: parsedSubscription.subscriptionId,
|
|
480
|
+
revision: parsedSubscription.revision,
|
|
481
|
+
supersedes: parsedSubscription.supersedes ?? null,
|
|
482
|
+
eventKind: parsedSubscription.eventKind,
|
|
483
|
+
expiresAtBlock: parsedSubscription.expiresAtBlock,
|
|
484
|
+
profile: WAKE_SUBSCRIPTION_HPKE_PROFILE
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
function wakeSubscriptionPlaintextBytes(plaintext) {
|
|
488
|
+
const parsed = WakeSubscriptionPlaintextZod.parse(plaintext);
|
|
489
|
+
const encoded = canonicalJsonBytes(parsed);
|
|
490
|
+
if (encoded.length > MAX_SUBSCRIPTION_PLAINTEXT_BYTES) {
|
|
491
|
+
throw new RangeError(`Wake subscription plaintext exceeds ${MAX_SUBSCRIPTION_PLAINTEXT_BYTES} bytes`);
|
|
492
|
+
}
|
|
493
|
+
return encoded;
|
|
494
|
+
}
|
|
495
|
+
function wakeSubscriptionPlaintextFromBytes(encoded) {
|
|
496
|
+
if (encoded.length > MAX_SUBSCRIPTION_PLAINTEXT_BYTES) {
|
|
497
|
+
throw new RangeError(`Wake subscription plaintext exceeds ${MAX_SUBSCRIPTION_PLAINTEXT_BYTES} bytes`);
|
|
498
|
+
}
|
|
499
|
+
const decoded = textDecoder.decode(encoded);
|
|
500
|
+
const parsed = WakeSubscriptionPlaintextZod.parse(JSON.parse(decoded));
|
|
501
|
+
if (!equalBytes(encoded, canonicalJsonBytes(parsed))) {
|
|
502
|
+
throw new TypeError("Wake subscription plaintext is not canonically encoded");
|
|
503
|
+
}
|
|
504
|
+
return parsed;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/wakeSubscriptionHpke.ts
|
|
508
|
+
var suite = new CipherSuite({
|
|
509
|
+
kem: new DhkemX25519HkdfSha256(),
|
|
510
|
+
kdf: new HkdfSha256(),
|
|
511
|
+
aead: new Aes128Gcm()
|
|
512
|
+
});
|
|
513
|
+
var hpkeInfo = new TextEncoder().encode(WAKE_SUBSCRIPTION_HPKE_PROFILE2);
|
|
514
|
+
function asInternalHpkeKeyPair(value) {
|
|
515
|
+
if (typeof value !== "object" || value === null || !("privateKey" in value) || !("publicKey" in value)) {
|
|
516
|
+
throw new TypeError("HPKE implementation returned an invalid key pair");
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
privateKey: value.privateKey,
|
|
520
|
+
publicKey: value.publicKey
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
var WakeSubscriptionDecryptionError = class extends Error {
|
|
524
|
+
name = "WakeSubscriptionDecryptionError";
|
|
525
|
+
constructor() {
|
|
526
|
+
super("Wake subscription cryptographic validation failed");
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
function parsePublicKey(value) {
|
|
530
|
+
return {
|
|
531
|
+
encryptionKey: HashZod.parse(value.encryptionKey),
|
|
532
|
+
keyId: HandshakeIdZod.parse(value.keyId),
|
|
533
|
+
publicKey: Base64Url32BytesZod2.parse(value.publicKey)
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function parsePrivateKey(value) {
|
|
537
|
+
return {
|
|
538
|
+
encryptionKey: HashZod.parse(value.encryptionKey),
|
|
539
|
+
keyId: HandshakeIdZod.parse(value.keyId),
|
|
540
|
+
privateKey: Base64Url32BytesZod2.parse(value.privateKey)
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function decoded32(value) {
|
|
544
|
+
const decoded = canonicalBase64UrlToBytes2(value);
|
|
545
|
+
if (decoded?.length !== 32) throw new TypeError("Expected 32 canonical bytes");
|
|
546
|
+
return decoded;
|
|
547
|
+
}
|
|
548
|
+
async function generateWakeSubscriptionEncryptionKeyPair() {
|
|
549
|
+
const generated = await suite.kem.generateKeyPair();
|
|
550
|
+
const keyPair = asInternalHpkeKeyPair(generated);
|
|
551
|
+
const [publicKey, privateKey] = await Promise.all([
|
|
552
|
+
suite.kem.serializePublicKey(keyPair.publicKey),
|
|
553
|
+
suite.kem.serializePrivateKey(keyPair.privateKey)
|
|
554
|
+
]);
|
|
555
|
+
return {
|
|
556
|
+
publicKey: Base64Url32BytesZod2.parse(bytesToCanonicalBase64Url2(new Uint8Array(publicKey))),
|
|
557
|
+
privateKey: Base64Url32BytesZod2.parse(bytesToCanonicalBase64Url2(new Uint8Array(privateKey)))
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
async function encryptWakeSubscription(input) {
|
|
561
|
+
const subscription = WakeSubscriptionCleartextZod2.parse(input.subscription);
|
|
562
|
+
const subscriber = NormalizedAddressZod2.parse(input.subscriber);
|
|
563
|
+
const publisherKey = parsePublicKey(input.publisherKey);
|
|
564
|
+
if (subscription.encryptionKey !== publisherKey.encryptionKey || subscription.keyId !== publisherKey.keyId) {
|
|
565
|
+
throw new TypeError("Subscription key reference does not match the supplied publisher key");
|
|
566
|
+
}
|
|
567
|
+
const recipientPublicKey = await suite.kem.deserializePublicKey(decoded32(publisherKey.publicKey));
|
|
568
|
+
const { ct, enc } = await suite.seal(
|
|
569
|
+
{ recipientPublicKey, info: hpkeInfo },
|
|
570
|
+
wakeSubscriptionPlaintextBytes(input.plaintext),
|
|
571
|
+
wakeSubscriptionAadBytes(subscription, subscriber)
|
|
572
|
+
);
|
|
573
|
+
return EncryptedWakeSubscriptionZod2.parse({
|
|
574
|
+
...subscription,
|
|
575
|
+
encryption: {
|
|
576
|
+
profile: WAKE_SUBSCRIPTION_HPKE_PROFILE2,
|
|
577
|
+
enc: bytesToCanonicalBase64Url2(new Uint8Array(enc)),
|
|
578
|
+
ciphertext: bytesToCanonicalBase64Url2(new Uint8Array(ct))
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
async function decryptWakeSubscription(input) {
|
|
583
|
+
try {
|
|
584
|
+
const subscription = EncryptedWakeSubscriptionZod2.parse(input.subscription);
|
|
585
|
+
const subscriber = NormalizedAddressZod2.parse(input.subscriber);
|
|
586
|
+
const publisherKey = parsePrivateKey(input.publisherKey);
|
|
587
|
+
if (subscription.encryptionKey !== publisherKey.encryptionKey || subscription.keyId !== publisherKey.keyId) throw new TypeError("Key reference mismatch");
|
|
588
|
+
const recipientKey = await suite.kem.deserializePrivateKey(decoded32(publisherKey.privateKey));
|
|
589
|
+
const plaintext = await suite.open(
|
|
590
|
+
{
|
|
591
|
+
recipientKey,
|
|
592
|
+
enc: decoded32(subscription.encryption.enc),
|
|
593
|
+
info: hpkeInfo
|
|
594
|
+
},
|
|
595
|
+
canonicalBase64UrlToBytes2(subscription.encryption.ciphertext) ?? new Uint8Array(),
|
|
596
|
+
wakeSubscriptionAadBytes(subscription, subscriber)
|
|
597
|
+
);
|
|
598
|
+
return wakeSubscriptionPlaintextFromBytes(new Uint8Array(plaintext));
|
|
599
|
+
} catch {
|
|
600
|
+
throw new WakeSubscriptionDecryptionError();
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/handshakeConstruction.ts
|
|
605
|
+
var publicObjectParsers = [
|
|
606
|
+
PublisherServiceZod,
|
|
607
|
+
PublisherEncryptionKeyZod,
|
|
608
|
+
PublisherServiceRevocationZod,
|
|
609
|
+
PublisherEncryptionKeyRevocationZod,
|
|
610
|
+
EncryptedWakeSubscriptionZod3,
|
|
611
|
+
WakeGrantObjectZod,
|
|
612
|
+
WakeGrantRevocationZod,
|
|
613
|
+
WakeSubscriptionRevocationZod,
|
|
614
|
+
WakeSubscriptionAcceptanceZod,
|
|
615
|
+
WakeSubscriptionAcceptanceRevocationZod,
|
|
616
|
+
EndpointChallengeZod2,
|
|
617
|
+
EndpointChallengeResponseZod2
|
|
618
|
+
];
|
|
619
|
+
function parseHandshakePublicObject(value) {
|
|
620
|
+
for (const parser of publicObjectParsers) {
|
|
621
|
+
const parsed = parser.safeParse(value);
|
|
622
|
+
if (parsed.success) return parsed.data;
|
|
623
|
+
}
|
|
624
|
+
throw new TypeError("Expected a strict Event Kit handshake public object");
|
|
625
|
+
}
|
|
626
|
+
async function handshakeObjectHash(object) {
|
|
627
|
+
return HashZod2.parse(await PayloadBuilder2.dataHash(parseHandshakePublicObject(object)));
|
|
628
|
+
}
|
|
629
|
+
async function withHandshakeObjectHash(body) {
|
|
630
|
+
return Object.freeze({ body, objectHash: await handshakeObjectHash(body) });
|
|
631
|
+
}
|
|
632
|
+
function createPublisherEncryptionKey(input) {
|
|
633
|
+
return PublisherEncryptionKeyZod.parse(new PayloadBuilder2({ schema: PublisherEncryptionKeySchema }).fields({
|
|
634
|
+
keyId: input.keyId ?? generateHandshakeId2(),
|
|
635
|
+
profile: WAKE_SUBSCRIPTION_HPKE_PROFILE3,
|
|
636
|
+
publicKey: input.publicKey,
|
|
637
|
+
validFromBlock: input.validFromBlock,
|
|
638
|
+
...input.expiresAtBlock === void 0 ? {} : { expiresAtBlock: input.expiresAtBlock }
|
|
639
|
+
}).build());
|
|
640
|
+
}
|
|
641
|
+
async function generatePublisherEncryptionKey(input) {
|
|
642
|
+
const material = await generateWakeSubscriptionEncryptionKeyPair();
|
|
643
|
+
const descriptor = createPublisherEncryptionKey({
|
|
644
|
+
...input,
|
|
645
|
+
publicKey: material.publicKey
|
|
646
|
+
});
|
|
647
|
+
const descriptorHash = await handshakeObjectHash(descriptor);
|
|
648
|
+
return Object.freeze({
|
|
649
|
+
descriptor,
|
|
650
|
+
descriptorHash,
|
|
651
|
+
privateKey: Object.freeze({
|
|
652
|
+
encryptionKey: descriptorHash,
|
|
653
|
+
keyId: descriptor.keyId,
|
|
654
|
+
privateKey: material.privateKey
|
|
655
|
+
}),
|
|
656
|
+
publicKey: Object.freeze({
|
|
657
|
+
encryptionKey: descriptorHash,
|
|
658
|
+
keyId: descriptor.keyId,
|
|
659
|
+
publicKey: descriptor.publicKey
|
|
660
|
+
})
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
function createPublisherService(input) {
|
|
664
|
+
return PublisherServiceZod.parse(new PayloadBuilder2({ schema: PublisherServiceSchema }).fields({
|
|
665
|
+
serviceId: input.serviceId ?? generateHandshakeId2(),
|
|
666
|
+
revision: input.revision ?? 1,
|
|
667
|
+
...input.supersedes === void 0 ? {} : { supersedes: input.supersedes },
|
|
668
|
+
validFromBlock: input.validFromBlock,
|
|
669
|
+
...input.expiresAtBlock === void 0 ? {} : { expiresAtBlock: input.expiresAtBlock },
|
|
670
|
+
events: [{ kind: FINALIZED_HEAD_EVENT_KIND, filterProfile: FINALIZED_HEAD_FILTER_PROFILE }],
|
|
671
|
+
deliveryProfile: WAKE_WALLET_JWT_DELIVERY_PROFILE,
|
|
672
|
+
challengeProfile: ENDPOINT_CHALLENGE_HMAC_SHA256_PROFILE,
|
|
673
|
+
encryptionProfile: WAKE_SUBSCRIPTION_HPKE_PROFILE3,
|
|
674
|
+
encryptionKeys: [...input.encryptionKeys],
|
|
675
|
+
preferredEncryptionKey: input.preferredEncryptionKey,
|
|
676
|
+
limits: {
|
|
677
|
+
maxBlocksPerWake: input.maxBlocksPerWake,
|
|
678
|
+
maxSubscriptionLifetimeBlocks: input.maxSubscriptionLifetimeBlocks,
|
|
679
|
+
maxPlaintextBytes: MAX_SUBSCRIPTION_PLAINTEXT_BYTES2
|
|
680
|
+
},
|
|
681
|
+
retirementPolicy: "existing-acceptances-continue"
|
|
682
|
+
}).build());
|
|
683
|
+
}
|
|
684
|
+
function createWakeGrant(input) {
|
|
685
|
+
return WakeGrantObjectZod.parse(new PayloadBuilder2({ schema: WakeGrantObjectSchema }).fields({
|
|
686
|
+
grantId: input.grantId ?? generateGrantId(),
|
|
687
|
+
deploymentId: input.deploymentId,
|
|
688
|
+
publisher: input.publisher,
|
|
689
|
+
...input.queueId === void 0 ? {} : { queueId: input.queueId },
|
|
690
|
+
permissions: ["enqueue"]
|
|
691
|
+
}).build());
|
|
692
|
+
}
|
|
693
|
+
function createWakeSubscriptionPlaintext(input) {
|
|
694
|
+
const secretBytes = new Uint8Array(32);
|
|
695
|
+
if (input.challengeSecret === void 0) {
|
|
696
|
+
globalThis.crypto.getRandomValues(secretBytes);
|
|
697
|
+
}
|
|
698
|
+
const challengeSecret = input.challengeSecret ?? bytesToCanonicalBase64Url3(secretBytes);
|
|
699
|
+
return WakeSubscriptionPlaintextZod2.parse(new PayloadBuilder2({ schema: WakeSubscriptionPlaintextSchema }).fields({
|
|
700
|
+
endpoint: input.endpoint,
|
|
701
|
+
audience: input.audience,
|
|
702
|
+
deploymentId: input.deploymentId,
|
|
703
|
+
queueId: input.queueId ?? null,
|
|
704
|
+
filter: {
|
|
705
|
+
schema: FINALIZED_HEAD_FILTER_PROFILE,
|
|
706
|
+
maxBlocksPerWake: input.maxBlocksPerWake
|
|
707
|
+
},
|
|
708
|
+
challenge: {
|
|
709
|
+
profile: ENDPOINT_CHALLENGE_HMAC_SHA256_PROFILE,
|
|
710
|
+
secret: challengeSecret
|
|
711
|
+
}
|
|
712
|
+
}).build());
|
|
713
|
+
}
|
|
714
|
+
async function createEncryptedWakeSubscription(input) {
|
|
715
|
+
const cleartext = WakeSubscriptionCleartextZod3.parse(
|
|
716
|
+
new PayloadBuilder2({ schema: EncryptedWakeSubscriptionSchema }).fields({
|
|
717
|
+
subscriptionId: input.subscriptionId ?? generateHandshakeId2(),
|
|
718
|
+
revision: input.revision ?? 1,
|
|
719
|
+
...input.supersedes === void 0 ? {} : { supersedes: input.supersedes },
|
|
720
|
+
chainId: input.chainId,
|
|
721
|
+
publisher: input.publisher,
|
|
722
|
+
service: input.service,
|
|
723
|
+
encryptionKey: input.publisherKey.encryptionKey,
|
|
724
|
+
keyId: input.publisherKey.keyId,
|
|
725
|
+
grant: input.grant,
|
|
726
|
+
eventKind: FINALIZED_HEAD_EVENT_KIND,
|
|
727
|
+
expiresAtBlock: input.expiresAtBlock
|
|
728
|
+
}).build()
|
|
729
|
+
);
|
|
730
|
+
return await encryptWakeSubscription({
|
|
731
|
+
plaintext: input.plaintext,
|
|
732
|
+
publisherKey: input.publisherKey,
|
|
733
|
+
subscriber: input.subscriber,
|
|
734
|
+
subscription: cleartext
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
function createWakeSubscriptionAcceptance(input) {
|
|
738
|
+
return WakeSubscriptionAcceptanceZod.parse(new PayloadBuilder2({ schema: WakeSubscriptionAcceptanceSchema }).fields({
|
|
739
|
+
challenge: input.challenge,
|
|
740
|
+
encryptionKey: input.encryptionKey,
|
|
741
|
+
grant: input.grant,
|
|
742
|
+
response: input.response,
|
|
743
|
+
service: input.service,
|
|
744
|
+
subscription: input.subscription
|
|
745
|
+
}).build());
|
|
746
|
+
}
|
|
747
|
+
function createPublisherServiceRevocation(service) {
|
|
748
|
+
return PublisherServiceRevocationZod.parse(new PayloadBuilder2({ schema: PublisherServiceRevocationSchema }).fields({ service }).build());
|
|
749
|
+
}
|
|
750
|
+
function createPublisherEncryptionKeyRevocation(key) {
|
|
751
|
+
return PublisherEncryptionKeyRevocationZod.parse(
|
|
752
|
+
new PayloadBuilder2({ schema: PublisherEncryptionKeyRevocationSchema }).fields({ key }).build()
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
function createWakeGrantRevocation(grant) {
|
|
756
|
+
return WakeGrantRevocationZod.parse(new PayloadBuilder2({ schema: WakeGrantRevocationSchema }).fields({ grant }).build());
|
|
757
|
+
}
|
|
758
|
+
function createWakeSubscriptionRevocation(subscription) {
|
|
759
|
+
return WakeSubscriptionRevocationZod.parse(new PayloadBuilder2({ schema: WakeSubscriptionRevocationSchema }).fields({ subscription }).build());
|
|
760
|
+
}
|
|
761
|
+
function createWakeSubscriptionAcceptanceRevocation(acceptance) {
|
|
762
|
+
return WakeSubscriptionAcceptanceRevocationZod.parse(
|
|
763
|
+
new PayloadBuilder2({ schema: WakeSubscriptionAcceptanceRevocationSchema }).fields({ acceptance }).build()
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/lifecycleSelection.ts
|
|
768
|
+
function isNonnegativeSafeInteger2(value) {
|
|
769
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
770
|
+
}
|
|
771
|
+
function isVisible(claimedAtBlock, finalizedHead) {
|
|
772
|
+
return isNonnegativeSafeInteger2(claimedAtBlock) && claimedAtBlock <= finalizedHead;
|
|
773
|
+
}
|
|
774
|
+
function isRevoked(revokedAtBlock, finalizedHead) {
|
|
775
|
+
return revokedAtBlock !== void 0 && revokedAtBlock <= finalizedHead;
|
|
776
|
+
}
|
|
777
|
+
function isUnrevokedEvidence(claimedAtBlock, revokedAtBlock, finalizedHead) {
|
|
778
|
+
return isVisible(claimedAtBlock, finalizedHead) && (revokedAtBlock === void 0 || isNonnegativeSafeInteger2(revokedAtBlock) && revokedAtBlock >= claimedAtBlock) && !isRevoked(revokedAtBlock, finalizedHead);
|
|
779
|
+
}
|
|
780
|
+
function isInsideExclusiveWindow(finalizedHead, validFromBlock, expiresAtBlock) {
|
|
781
|
+
if (!isNonnegativeSafeInteger2(validFromBlock) || finalizedHead < validFromBlock) return false;
|
|
782
|
+
return expiresAtBlock === void 0 || isNonnegativeSafeInteger2(expiresAtBlock) && expiresAtBlock > validFromBlock && finalizedHead < expiresAtBlock;
|
|
783
|
+
}
|
|
784
|
+
function isRevisionShapeValid(revision) {
|
|
785
|
+
if (!Number.isSafeInteger(revision.revision) || revision.revision < 1) return false;
|
|
786
|
+
if (!isNonnegativeSafeInteger2(revision.claimedAtBlock)) return false;
|
|
787
|
+
if (revision.revokedAtBlock !== void 0 && (!isNonnegativeSafeInteger2(revision.revokedAtBlock) || revision.revokedAtBlock < revision.claimedAtBlock)) return false;
|
|
788
|
+
return revision.revision === 1 ? revision.supersedes === void 0 : revision.supersedes !== void 0;
|
|
789
|
+
}
|
|
790
|
+
function resolveLinearChain(revisions, finalizedHead) {
|
|
791
|
+
const visible = revisions.filter((revision) => isVisible(revision.claimedAtBlock, finalizedHead));
|
|
792
|
+
if (visible.length === 0) return { ok: true, chain: [] };
|
|
793
|
+
if (visible.some((revision) => !isRevisionShapeValid(revision))) return { ok: false };
|
|
794
|
+
const byHash = /* @__PURE__ */ new Map();
|
|
795
|
+
const childByParent = /* @__PURE__ */ new Map();
|
|
796
|
+
let root;
|
|
797
|
+
for (const revision of visible) {
|
|
798
|
+
if (byHash.has(revision.objectHash)) return { ok: false };
|
|
799
|
+
byHash.set(revision.objectHash, revision);
|
|
800
|
+
if (revision.revision === 1) {
|
|
801
|
+
if (root !== void 0) return { ok: false };
|
|
802
|
+
root = revision;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (root === void 0) return { ok: false };
|
|
806
|
+
for (const revision of visible) {
|
|
807
|
+
if (revision.revision === 1) continue;
|
|
808
|
+
const parentHash = revision.supersedes;
|
|
809
|
+
if (parentHash === void 0 || childByParent.has(parentHash)) return { ok: false };
|
|
810
|
+
const parent = byHash.get(parentHash);
|
|
811
|
+
if (parent === void 0 || revision.revision !== parent.revision + 1 || revision.claimedAtBlock < parent.claimedAtBlock) return { ok: false };
|
|
812
|
+
childByParent.set(parentHash, revision);
|
|
813
|
+
}
|
|
814
|
+
const chain = [root];
|
|
815
|
+
let current = root;
|
|
816
|
+
while (childByParent.has(current.objectHash)) {
|
|
817
|
+
const child = childByParent.get(current.objectHash);
|
|
818
|
+
if (child === void 0) return { ok: false };
|
|
819
|
+
chain.push(child);
|
|
820
|
+
current = child;
|
|
821
|
+
}
|
|
822
|
+
return chain.length === visible.length ? { ok: true, chain } : { ok: false };
|
|
823
|
+
}
|
|
824
|
+
function groupServicesByIdentity(services, input) {
|
|
825
|
+
const groups = /* @__PURE__ */ new Map();
|
|
826
|
+
for (const service of services) {
|
|
827
|
+
if (service.source !== input.publisherSource) continue;
|
|
828
|
+
if (input.serviceId !== void 0 && service.serviceId !== input.serviceId) continue;
|
|
829
|
+
const group = groups.get(service.serviceId) ?? [];
|
|
830
|
+
group.push(service);
|
|
831
|
+
groups.set(service.serviceId, group);
|
|
832
|
+
}
|
|
833
|
+
return groups;
|
|
834
|
+
}
|
|
835
|
+
function authoritativeService(chain, finalizedHead) {
|
|
836
|
+
let authoritative;
|
|
837
|
+
for (const service of chain) {
|
|
838
|
+
if (service.validFromBlock <= finalizedHead) authoritative = service;
|
|
839
|
+
}
|
|
840
|
+
return authoritative;
|
|
841
|
+
}
|
|
842
|
+
function isActiveService(service, finalizedHead) {
|
|
843
|
+
return isUnrevokedEvidence(service.claimedAtBlock, service.revokedAtBlock, finalizedHead) && isInsideExclusiveWindow(finalizedHead, service.validFromBlock, service.expiresAtBlock);
|
|
844
|
+
}
|
|
845
|
+
function selectPublisherService(input) {
|
|
846
|
+
const groups = groupServicesByIdentity(input.services, input);
|
|
847
|
+
const selectable = [];
|
|
848
|
+
for (const revisions of groups.values()) {
|
|
849
|
+
if (input.serviceId === void 0 && !revisions.some((service2) => service2.eventKinds.includes(input.eventKind))) {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
const resolution = resolveLinearChain(revisions, input.finalizedHead);
|
|
853
|
+
if (!resolution.ok) return { ok: false, reason: "invalid-revision-chain" };
|
|
854
|
+
const service = authoritativeService(resolution.chain, input.finalizedHead);
|
|
855
|
+
if (service?.eventKinds.includes(input.eventKind) && isActiveService(service, input.finalizedHead)) {
|
|
856
|
+
selectable.push(service);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (selectable.length === 0) return { ok: false, reason: "service-not-found" };
|
|
860
|
+
if (selectable.length > 1) return { ok: false, reason: "ambiguous-service" };
|
|
861
|
+
return { ok: true, service: selectable[0] };
|
|
862
|
+
}
|
|
863
|
+
function resolvePreferredEncryptionKey(input) {
|
|
864
|
+
const preferredHash = input.service.preferredEncryptionKey;
|
|
865
|
+
if (!input.service.encryptionKeys.includes(preferredHash)) {
|
|
866
|
+
return { ok: false, reason: "preferred-key-not-referenced" };
|
|
867
|
+
}
|
|
868
|
+
const exact = input.keys.filter((key2) => key2.source === input.service.source && key2.objectHash === preferredHash && isVisible(key2.claimedAtBlock, input.finalizedHead));
|
|
869
|
+
if (exact.length === 0) return { ok: false, reason: "preferred-key-not-found" };
|
|
870
|
+
if (exact.length > 1) return { ok: false, reason: "ambiguous-key" };
|
|
871
|
+
const key = exact[0];
|
|
872
|
+
const sameStableIdentity = input.keys.filter((candidate) => candidate.source === key.source && candidate.keyId === key.keyId && candidate.objectHash !== key.objectHash && isVisible(candidate.claimedAtBlock, input.finalizedHead));
|
|
873
|
+
if (sameStableIdentity.length > 0) return { ok: false, reason: "ambiguous-key" };
|
|
874
|
+
if (!isUnrevokedEvidence(key.claimedAtBlock, key.revokedAtBlock, input.finalizedHead) || !isInsideExclusiveWindow(input.finalizedHead, key.validFromBlock, key.expiresAtBlock)) {
|
|
875
|
+
return { ok: false, reason: "preferred-key-inactive" };
|
|
876
|
+
}
|
|
877
|
+
return { ok: true, key };
|
|
878
|
+
}
|
|
879
|
+
function acceptanceFor(subscription, input) {
|
|
880
|
+
return input.acceptances.filter((acceptance) => acceptance.source === input.publisherSource && acceptance.subscription === subscription.objectHash && isVisible(acceptance.acceptanceBlock, input.finalizedHead));
|
|
881
|
+
}
|
|
882
|
+
function acceptedSubscription(subscription, input) {
|
|
883
|
+
const acceptances = acceptanceFor(subscription, input);
|
|
884
|
+
if (acceptances.length > 1) return "ambiguous";
|
|
885
|
+
const acceptance = acceptances[0];
|
|
886
|
+
if (acceptance === void 0 || !isNonnegativeSafeInteger2(acceptance.acceptanceBlock)) return void 0;
|
|
887
|
+
if (!Number.isSafeInteger(subscription.expiresAtBlock) || subscription.expiresAtBlock < 1) return void 0;
|
|
888
|
+
if (acceptance.acceptanceBlock <= subscription.claimedAtBlock || acceptance.acceptanceBlock >= Number.MAX_SAFE_INTEGER) return void 0;
|
|
889
|
+
const activeFromBlock = acceptance.acceptanceBlock + 1;
|
|
890
|
+
if (activeFromBlock >= subscription.expiresAtBlock) return void 0;
|
|
891
|
+
return {
|
|
892
|
+
acceptance,
|
|
893
|
+
activeFromBlock,
|
|
894
|
+
subscription
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
function isActiveAcceptedSubscription(accepted, finalizedHead) {
|
|
898
|
+
return isUnrevokedEvidence(
|
|
899
|
+
accepted.subscription.claimedAtBlock,
|
|
900
|
+
accepted.subscription.revokedAtBlock,
|
|
901
|
+
finalizedHead
|
|
902
|
+
) && isUnrevokedEvidence(
|
|
903
|
+
accepted.acceptance.acceptanceBlock,
|
|
904
|
+
accepted.acceptance.revokedAtBlock,
|
|
905
|
+
finalizedHead
|
|
906
|
+
) && finalizedHead < accepted.subscription.expiresAtBlock;
|
|
907
|
+
}
|
|
908
|
+
function selectSubscriptionActivation(input) {
|
|
909
|
+
const revisions = input.subscriptions.filter((subscription) => subscription.source === input.subscriberSource && subscription.subscriptionId === input.subscriptionId);
|
|
910
|
+
const resolution = resolveLinearChain(revisions, input.finalizedHead);
|
|
911
|
+
if (!resolution.ok) return { ok: false, reason: "invalid-revision-chain" };
|
|
912
|
+
if (resolution.chain.length === 0) return { ok: false, reason: "subscription-not-found" };
|
|
913
|
+
let selected;
|
|
914
|
+
for (const subscription of resolution.chain) {
|
|
915
|
+
const accepted = acceptedSubscription(subscription, input);
|
|
916
|
+
if (accepted === "ambiguous") return { ok: false, reason: "ambiguous-acceptance" };
|
|
917
|
+
if (accepted !== void 0 && accepted.activeFromBlock <= input.finalizedHead) selected = accepted;
|
|
918
|
+
}
|
|
919
|
+
if (selected === void 0) return { ok: false, reason: "subscription-pending" };
|
|
920
|
+
const latest = resolution.chain.at(-1);
|
|
921
|
+
if (!isActiveAcceptedSubscription(selected, input.finalizedHead)) {
|
|
922
|
+
if (latest?.objectHash !== selected.subscription.objectHash) {
|
|
923
|
+
return { ok: false, reason: "subscription-pending" };
|
|
924
|
+
}
|
|
925
|
+
return { ok: false, reason: "subscription-inactive" };
|
|
926
|
+
}
|
|
927
|
+
return {
|
|
928
|
+
acceptance: selected.acceptance,
|
|
929
|
+
activeFromBlock: selected.activeFromBlock,
|
|
930
|
+
ok: true,
|
|
931
|
+
pendingReplacement: latest?.objectHash !== selected.subscription.objectHash,
|
|
932
|
+
subscription: selected.subscription
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
242
936
|
// src/memoryStatementGrantGraph.ts
|
|
243
937
|
import {
|
|
244
938
|
normalizeAddress,
|
|
245
|
-
WakeGrantObjectSchema,
|
|
246
|
-
WakeGrantRevocationSchema
|
|
939
|
+
WakeGrantObjectSchema as WakeGrantObjectSchema2,
|
|
940
|
+
WakeGrantRevocationSchema as WakeGrantRevocationSchema2
|
|
247
941
|
} from "@xyo-network/event-kit-schemas";
|
|
248
942
|
function createMemoryStatementGrantGraph(initialHead = 0) {
|
|
249
943
|
let head = initialHead;
|
|
250
944
|
const claims = /* @__PURE__ */ new Map();
|
|
251
945
|
const objects = /* @__PURE__ */ new Map();
|
|
252
|
-
function claimKey(source, objectHash) {
|
|
253
|
-
return `${normalizeAddress(source)}|${objectHash}`;
|
|
946
|
+
function claimKey(source, subject, objectHash) {
|
|
947
|
+
return `${normalizeAddress(source)}|${normalizeAddress(subject)}|${objectHash}`;
|
|
254
948
|
}
|
|
255
949
|
function materialize(input) {
|
|
256
950
|
const source = normalizeAddress(input.source);
|
|
951
|
+
const subject = normalizeAddress(input.subject);
|
|
257
952
|
objects.set(input.objectHash, Object.freeze({
|
|
258
953
|
assurance: "resolved",
|
|
259
954
|
declaredSchema: input.declaredSchema,
|
|
260
955
|
objectHash: input.objectHash,
|
|
261
956
|
params: Object.freeze({ ...input.params })
|
|
262
957
|
}));
|
|
263
|
-
const key = claimKey(source, input.objectHash);
|
|
958
|
+
const key = claimKey(source, subject, input.objectHash);
|
|
264
959
|
const existing = claims.get(key);
|
|
265
960
|
claims.set(key, Object.freeze(existing === void 0 ? {
|
|
266
961
|
declaredObjectSchema: input.declaredSchema,
|
|
@@ -268,7 +963,8 @@ function createMemoryStatementGrantGraph(initialHead = 0) {
|
|
|
268
963
|
firstEventHash: input.eventHash,
|
|
269
964
|
lastBlock: input.block,
|
|
270
965
|
objectHash: input.objectHash,
|
|
271
|
-
source
|
|
966
|
+
source,
|
|
967
|
+
subject
|
|
272
968
|
} : { ...existing, lastBlock: Math.max(existing.lastBlock, input.block) }));
|
|
273
969
|
if (input.block > head) head = input.block;
|
|
274
970
|
}
|
|
@@ -284,6 +980,7 @@ function createMemoryStatementGrantGraph(initialHead = 0) {
|
|
|
284
980
|
const records = [];
|
|
285
981
|
for (const record of claims.values()) {
|
|
286
982
|
if (record.source !== normalizedSource) continue;
|
|
983
|
+
if (query?.subject !== void 0 && record.subject !== normalizeAddress(query.subject)) continue;
|
|
287
984
|
if (query?.objectHash !== void 0 && record.objectHash !== query.objectHash) continue;
|
|
288
985
|
if (query?.declaredObjectSchemaPrefix !== void 0 && !record.declaredObjectSchema.startsWith(query.declaredObjectSchemaPrefix)) {
|
|
289
986
|
continue;
|
|
@@ -296,31 +993,43 @@ function createMemoryStatementGrantGraph(initialHead = 0) {
|
|
|
296
993
|
return objects.get(objectHash);
|
|
297
994
|
},
|
|
298
995
|
claim(input) {
|
|
299
|
-
if (input.grant.schema !==
|
|
300
|
-
throw new Error(`expected ${
|
|
996
|
+
if (input.grant.schema !== WakeGrantObjectSchema2) {
|
|
997
|
+
throw new Error(`expected ${WakeGrantObjectSchema2}`);
|
|
301
998
|
}
|
|
302
999
|
const { schema: _schema, ...params } = input.grant;
|
|
303
1000
|
materialize({
|
|
304
1001
|
block: input.block,
|
|
305
|
-
declaredSchema:
|
|
1002
|
+
declaredSchema: WakeGrantObjectSchema2,
|
|
306
1003
|
eventHash: input.eventHash,
|
|
307
1004
|
objectHash: input.objectHash,
|
|
308
1005
|
params: {
|
|
309
1006
|
...params,
|
|
310
1007
|
permissions: Object.freeze([...params.permissions])
|
|
311
1008
|
},
|
|
312
|
-
source: input.source
|
|
1009
|
+
source: input.source,
|
|
1010
|
+
subject: input.subject ?? input.grant.publisher
|
|
313
1011
|
});
|
|
314
1012
|
},
|
|
315
1013
|
revoke(input) {
|
|
316
1014
|
const revocation = { grant: input.grantObjectHash };
|
|
1015
|
+
const source = normalizeAddress(input.source);
|
|
1016
|
+
const subjects = [...claims.values()].filter((claim) => claim.source === source && claim.objectHash === input.grantObjectHash).map((claim) => claim.subject);
|
|
1017
|
+
const uniqueSubjects = [...new Set(subjects)];
|
|
1018
|
+
if (input.subject === void 0 && uniqueSubjects.length > 1) {
|
|
1019
|
+
throw new Error(`cannot infer one subject for ambiguous grant Claim ${input.grantObjectHash}`);
|
|
1020
|
+
}
|
|
1021
|
+
const subject = input.subject ?? uniqueSubjects[0];
|
|
1022
|
+
if (subject === void 0) {
|
|
1023
|
+
throw new Error(`cannot infer subject for unknown grant Claim ${input.grantObjectHash}`);
|
|
1024
|
+
}
|
|
317
1025
|
materialize({
|
|
318
1026
|
block: input.block,
|
|
319
|
-
declaredSchema:
|
|
1027
|
+
declaredSchema: WakeGrantRevocationSchema2,
|
|
320
1028
|
eventHash: input.eventHash,
|
|
321
1029
|
objectHash: input.objectHash,
|
|
322
1030
|
params: revocation,
|
|
323
|
-
source: input.source
|
|
1031
|
+
source: input.source,
|
|
1032
|
+
subject
|
|
324
1033
|
});
|
|
325
1034
|
}
|
|
326
1035
|
};
|
|
@@ -447,15 +1156,284 @@ function createMemoryWakeQueue() {
|
|
|
447
1156
|
};
|
|
448
1157
|
}
|
|
449
1158
|
|
|
1159
|
+
// src/publisherSubscription.ts
|
|
1160
|
+
import {
|
|
1161
|
+
ChainIdZod,
|
|
1162
|
+
EncryptedWakeSubscriptionZod as EncryptedWakeSubscriptionZod4,
|
|
1163
|
+
EndpointChallengeZod as EndpointChallengeZod3,
|
|
1164
|
+
HashZod as HashZod3,
|
|
1165
|
+
NormalizedAddressZod as NormalizedAddressZod3,
|
|
1166
|
+
PublisherEncryptionKeyZod as PublisherEncryptionKeyZod2,
|
|
1167
|
+
PublisherServiceZod as PublisherServiceZod2,
|
|
1168
|
+
WakeGrantObjectZod as WakeGrantObjectZod2
|
|
1169
|
+
} from "@xyo-network/event-kit-schemas";
|
|
1170
|
+
function rejectPublisherSubscription(reason) {
|
|
1171
|
+
return Object.freeze({ ok: false, reason });
|
|
1172
|
+
}
|
|
1173
|
+
function isNonnegativeSafeInteger3(value) {
|
|
1174
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
1175
|
+
}
|
|
1176
|
+
function isActiveAt(value, block) {
|
|
1177
|
+
return value.validFromBlock <= block && (value.expiresAtBlock === void 0 || block < value.expiresAtBlock);
|
|
1178
|
+
}
|
|
1179
|
+
function occursExactlyOnce(hashes, expected) {
|
|
1180
|
+
return hashes.filter((hash) => hash === expected).length === 1;
|
|
1181
|
+
}
|
|
1182
|
+
function hasExactGrantBinding(grant, publisher) {
|
|
1183
|
+
return grant.publisher === publisher && grant.permissions.length === 1 && grant.permissions[0] === "enqueue";
|
|
1184
|
+
}
|
|
1185
|
+
function hasExactQueueScope(grant, plaintext) {
|
|
1186
|
+
const grantQueue = Object.hasOwn(grant, "queueId") ? grant.queueId : null;
|
|
1187
|
+
return plaintext.deploymentId === grant.deploymentId && plaintext.queueId === grantQueue;
|
|
1188
|
+
}
|
|
1189
|
+
function hasExactSubscriptionBinding(candidate, subscription) {
|
|
1190
|
+
return subscription.chainId === candidate.chainId && subscription.publisher === candidate.publisher && subscription.service === candidate.service.objectHash && subscription.encryptionKey === candidate.key.objectHash && subscription.keyId === candidate.key.body.keyId && subscription.grant === candidate.grant.objectHash;
|
|
1191
|
+
}
|
|
1192
|
+
function hasExactKeyBinding(service, key, subscription) {
|
|
1193
|
+
return service.encryptionKeys.includes(key.objectHash) && service.preferredEncryptionKey === key.objectHash && key.body.keyId === subscription.keyId && key.objectHash === subscription.encryptionKey;
|
|
1194
|
+
}
|
|
1195
|
+
function expiryWithinServicePolicy(subscription, service, tupleBlock) {
|
|
1196
|
+
return subscription.expiresAtBlock > tupleBlock && subscription.expiresAtBlock - tupleBlock <= service.limits.maxSubscriptionLifetimeBlocks;
|
|
1197
|
+
}
|
|
1198
|
+
function parsePublisherSubscriptionCandidate(candidate) {
|
|
1199
|
+
try {
|
|
1200
|
+
return {
|
|
1201
|
+
chainId: ChainIdZod.parse(candidate.chainId),
|
|
1202
|
+
grant: WakeGrantObjectZod2.parse(candidate.grant.body),
|
|
1203
|
+
key: PublisherEncryptionKeyZod2.parse(candidate.key.body),
|
|
1204
|
+
publisher: NormalizedAddressZod3.parse(candidate.publisher),
|
|
1205
|
+
service: PublisherServiceZod2.parse(candidate.service.body),
|
|
1206
|
+
subscriber: NormalizedAddressZod3.parse(candidate.subscriber),
|
|
1207
|
+
subscription: EncryptedWakeSubscriptionZod4.parse(candidate.subscription.body),
|
|
1208
|
+
tupleObjectHashes: candidate.tupleObjectHashes.map((hash) => HashZod3.parse(hash)),
|
|
1209
|
+
tupleSubject: NormalizedAddressZod3.parse(candidate.tupleSubject)
|
|
1210
|
+
};
|
|
1211
|
+
} catch {
|
|
1212
|
+
return void 0;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
function tupleBindingIsValid(candidate, parsed) {
|
|
1216
|
+
return isNonnegativeSafeInteger3(candidate.tupleBlock) && isNonnegativeSafeInteger3(candidate.finalizedHead) && candidate.finalizedHead >= candidate.tupleBlock && parsed.tupleSubject === parsed.publisher && occursExactlyOnce(parsed.tupleObjectHashes, candidate.service.objectHash) && occursExactlyOnce(parsed.tupleObjectHashes, candidate.grant.objectHash) && occursExactlyOnce(parsed.tupleObjectHashes, candidate.subscription.objectHash);
|
|
1217
|
+
}
|
|
1218
|
+
async function bodyHashesAreValid(candidate, parsed) {
|
|
1219
|
+
const canonicalHashes = await Promise.all([
|
|
1220
|
+
handshakeObjectHash(parsed.service),
|
|
1221
|
+
handshakeObjectHash(parsed.key),
|
|
1222
|
+
handshakeObjectHash(parsed.grant),
|
|
1223
|
+
handshakeObjectHash(parsed.subscription)
|
|
1224
|
+
]);
|
|
1225
|
+
return canonicalHashes[0] === candidate.service.objectHash && canonicalHashes[1] === candidate.key.objectHash && canonicalHashes[2] === candidate.grant.objectHash && canonicalHashes[3] === candidate.subscription.objectHash;
|
|
1226
|
+
}
|
|
1227
|
+
function publicBindingFailure(candidate, parsed) {
|
|
1228
|
+
if (!hasExactGrantBinding(parsed.grant, parsed.publisher)) return "grant-binding-invalid";
|
|
1229
|
+
if (!hasExactSubscriptionBinding(candidate, parsed.subscription)) return "subscription-binding-invalid";
|
|
1230
|
+
const hashedKey = { body: parsed.key, objectHash: candidate.key.objectHash };
|
|
1231
|
+
if (!hasExactKeyBinding(parsed.service, hashedKey, parsed.subscription)) return "key-binding-invalid";
|
|
1232
|
+
if (!isActiveAt(parsed.service, candidate.tupleBlock) || !isActiveAt(parsed.service, candidate.finalizedHead)) return "service-inactive";
|
|
1233
|
+
if (!isActiveAt(parsed.key, candidate.tupleBlock) || !isActiveAt(parsed.key, candidate.finalizedHead)) return "key-inactive";
|
|
1234
|
+
if (!expiryWithinServicePolicy(parsed.subscription, parsed.service, candidate.tupleBlock) || parsed.subscription.expiresAtBlock - candidate.finalizedHead <= 2) return "subscription-expiry-invalid";
|
|
1235
|
+
return void 0;
|
|
1236
|
+
}
|
|
1237
|
+
async function decryptCandidate(candidate, parsed) {
|
|
1238
|
+
let privateKey;
|
|
1239
|
+
try {
|
|
1240
|
+
privateKey = await candidate.keys.getPrivateKey({
|
|
1241
|
+
encryptionKey: candidate.key.objectHash,
|
|
1242
|
+
keyId: parsed.key.keyId,
|
|
1243
|
+
publisher: parsed.publisher
|
|
1244
|
+
});
|
|
1245
|
+
} catch {
|
|
1246
|
+
return rejectPublisherSubscription("key-store-unavailable");
|
|
1247
|
+
}
|
|
1248
|
+
if (privateKey === void 0) return rejectPublisherSubscription("private-key-not-found");
|
|
1249
|
+
if (privateKey.encryptionKey !== candidate.key.objectHash || privateKey.keyId !== parsed.key.keyId) {
|
|
1250
|
+
return rejectPublisherSubscription("key-binding-invalid");
|
|
1251
|
+
}
|
|
1252
|
+
try {
|
|
1253
|
+
return {
|
|
1254
|
+
plaintext: await decryptWakeSubscription({
|
|
1255
|
+
publisherKey: privateKey,
|
|
1256
|
+
subscriber: parsed.subscriber,
|
|
1257
|
+
subscription: parsed.subscription
|
|
1258
|
+
})
|
|
1259
|
+
};
|
|
1260
|
+
} catch {
|
|
1261
|
+
return rejectPublisherSubscription("decryption-failed");
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
function validatedPublisherSubscription(candidate, parsed, plaintext) {
|
|
1265
|
+
return Object.freeze({
|
|
1266
|
+
context: Object.freeze({
|
|
1267
|
+
chainId: parsed.chainId,
|
|
1268
|
+
encryptionKey: candidate.key.objectHash,
|
|
1269
|
+
eventKind: parsed.subscription.eventKind,
|
|
1270
|
+
grant: candidate.grant.objectHash,
|
|
1271
|
+
keyId: parsed.subscription.keyId,
|
|
1272
|
+
publisher: parsed.publisher,
|
|
1273
|
+
service: candidate.service.objectHash,
|
|
1274
|
+
subscriber: parsed.subscriber,
|
|
1275
|
+
subscription: candidate.subscription.objectHash
|
|
1276
|
+
}),
|
|
1277
|
+
finalizedHead: candidate.finalizedHead,
|
|
1278
|
+
grant: Object.freeze({ body: parsed.grant, objectHash: candidate.grant.objectHash }),
|
|
1279
|
+
key: Object.freeze({ body: parsed.key, objectHash: candidate.key.objectHash }),
|
|
1280
|
+
ok: true,
|
|
1281
|
+
plaintext,
|
|
1282
|
+
service: Object.freeze({ body: parsed.service, objectHash: candidate.service.objectHash }),
|
|
1283
|
+
subscription: Object.freeze({ body: parsed.subscription, objectHash: candidate.subscription.objectHash }),
|
|
1284
|
+
tupleBlock: candidate.tupleBlock
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
async function validateAndDecryptPublisherSubscription(candidate) {
|
|
1288
|
+
const parsed = parsePublisherSubscriptionCandidate(candidate);
|
|
1289
|
+
if (parsed === void 0) return rejectPublisherSubscription("malformed-candidate");
|
|
1290
|
+
if (!tupleBindingIsValid(candidate, parsed)) return rejectPublisherSubscription("tuple-binding-invalid");
|
|
1291
|
+
if (!await bodyHashesAreValid(candidate, parsed)) return rejectPublisherSubscription("body-hash-mismatch");
|
|
1292
|
+
const publicFailure = publicBindingFailure(candidate, parsed);
|
|
1293
|
+
if (publicFailure !== void 0) return rejectPublisherSubscription(publicFailure);
|
|
1294
|
+
const decrypted = await decryptCandidate(candidate, parsed);
|
|
1295
|
+
if ("ok" in decrypted) return decrypted;
|
|
1296
|
+
if (!hasExactQueueScope(parsed.grant, decrypted.plaintext)) {
|
|
1297
|
+
return rejectPublisherSubscription("grant-scope-invalid");
|
|
1298
|
+
}
|
|
1299
|
+
if (decrypted.plaintext.filter.maxBlocksPerWake > parsed.service.limits.maxBlocksPerWake) {
|
|
1300
|
+
return rejectPublisherSubscription("service-limit-invalid");
|
|
1301
|
+
}
|
|
1302
|
+
return validatedPublisherSubscription(candidate, parsed, decrypted.plaintext);
|
|
1303
|
+
}
|
|
1304
|
+
function endpointChallengeMatchesPublisherContext(challenge, context) {
|
|
1305
|
+
const parsed = EndpointChallengeZod3.safeParse(challenge);
|
|
1306
|
+
if (!parsed.success) return false;
|
|
1307
|
+
return parsed.data.chainId === context.chainId && parsed.data.publisher === context.publisher && parsed.data.subscriber === context.subscriber && parsed.data.service === context.service && parsed.data.encryptionKey === context.encryptionKey && parsed.data.keyId === context.keyId && parsed.data.subscription === context.subscription && parsed.data.grant === context.grant && parsed.data.eventKind === context.eventKind;
|
|
1308
|
+
}
|
|
1309
|
+
async function challengeRecordMatches(record, challenge, challengeHash, responseHash) {
|
|
1310
|
+
if (record.challengeHash !== challengeHash || await endpointChallengeHash(record.challenge) !== challengeHash || await endpointChallengeHash(challenge) !== challengeHash) return false;
|
|
1311
|
+
return responseHash === void 0 || record.responseHash === responseHash;
|
|
1312
|
+
}
|
|
1313
|
+
async function issuePublisherEndpointChallenge(input) {
|
|
1314
|
+
const challenge = createEndpointChallenge({
|
|
1315
|
+
...input.subscription.context,
|
|
1316
|
+
issuedAt: input.issuedAt,
|
|
1317
|
+
...input.lifetimeSeconds === void 0 ? {} : { lifetimeSeconds: input.lifetimeSeconds }
|
|
1318
|
+
});
|
|
1319
|
+
const challengeHash = await endpointChallengeHash(challenge);
|
|
1320
|
+
try {
|
|
1321
|
+
const result = await input.store.issue({ challenge, challengeHash });
|
|
1322
|
+
if (result.kind === "collision") return { ok: false, reason: "challenge-collision" };
|
|
1323
|
+
if (result.kind !== "duplicate" && result.kind !== "issued" || !await challengeRecordMatches(result.record, challenge, challengeHash)) {
|
|
1324
|
+
return { ok: false, reason: "challenge-store-invalid" };
|
|
1325
|
+
}
|
|
1326
|
+
return Object.freeze({
|
|
1327
|
+
challenge,
|
|
1328
|
+
challengeHash,
|
|
1329
|
+
disposition: result.kind,
|
|
1330
|
+
ok: true
|
|
1331
|
+
});
|
|
1332
|
+
} catch {
|
|
1333
|
+
return { ok: false, reason: "challenge-store-unavailable" };
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
async function beginPublisherEndpointChallengeAttempt(input) {
|
|
1337
|
+
if (!isNonnegativeSafeInteger3(input.nowSeconds)) return { kind: "store-invalid" };
|
|
1338
|
+
try {
|
|
1339
|
+
const result = await input.store.beginAttempt({
|
|
1340
|
+
challengeHash: HashZod3.parse(input.challengeHash),
|
|
1341
|
+
nowSeconds: input.nowSeconds
|
|
1342
|
+
});
|
|
1343
|
+
if (!["already-attempted", "expired", "missing", "started", "verified"].includes(result.kind)) {
|
|
1344
|
+
return { kind: "store-invalid" };
|
|
1345
|
+
}
|
|
1346
|
+
if (result.kind !== "missing" && !await challengeRecordMatches(
|
|
1347
|
+
result.record,
|
|
1348
|
+
result.record.challenge,
|
|
1349
|
+
HashZod3.parse(input.challengeHash)
|
|
1350
|
+
)) return { kind: "store-invalid" };
|
|
1351
|
+
if ((result.kind === "started" || result.kind === "already-attempted") && result.record.state !== "attempted") return { kind: "store-invalid" };
|
|
1352
|
+
if (result.kind === "verified" && result.record.state !== "verified") return { kind: "store-invalid" };
|
|
1353
|
+
return result;
|
|
1354
|
+
} catch {
|
|
1355
|
+
return { kind: "store-unavailable" };
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function completeFailure(reason) {
|
|
1359
|
+
return Object.freeze({ ok: false, reason });
|
|
1360
|
+
}
|
|
1361
|
+
async function validateChallengeCompletion(input, result, challengeHash, responseHash, insideDecisionWindow, proofValid) {
|
|
1362
|
+
if (!insideDecisionWindow && result.kind !== "duplicate") {
|
|
1363
|
+
return completeFailure(result.kind === "collision" ? "challenge-response-collision" : "challenge-expired");
|
|
1364
|
+
}
|
|
1365
|
+
if (!proofValid) return completeFailure("invalid-proof");
|
|
1366
|
+
if (result.kind === "missing") return completeFailure("challenge-not-found");
|
|
1367
|
+
if (result.kind === "expired") return completeFailure("challenge-expired");
|
|
1368
|
+
if (result.kind === "not-attempted") return completeFailure("challenge-not-attempted");
|
|
1369
|
+
if (result.kind === "collision") return completeFailure("challenge-response-collision");
|
|
1370
|
+
if (result.kind !== "duplicate" && result.kind !== "verified" || !await challengeRecordMatches(result.record, input.challenge, challengeHash, responseHash)) {
|
|
1371
|
+
return completeFailure("challenge-store-invalid");
|
|
1372
|
+
}
|
|
1373
|
+
return result;
|
|
1374
|
+
}
|
|
1375
|
+
async function completePublisherEndpointChallenge(input) {
|
|
1376
|
+
if (!endpointChallengeMatchesPublisherContext(input.challenge, input.subscription.context)) {
|
|
1377
|
+
return completeFailure("challenge-context-invalid");
|
|
1378
|
+
}
|
|
1379
|
+
if (!isNonnegativeSafeInteger3(input.nowSeconds)) return completeFailure("challenge-expired");
|
|
1380
|
+
const insideDecisionWindow = input.challenge.issuedAt <= input.nowSeconds && input.nowSeconds < input.challenge.expiresAt;
|
|
1381
|
+
const proofInput = {
|
|
1382
|
+
challenge: input.challenge,
|
|
1383
|
+
response: input.response,
|
|
1384
|
+
secret: input.subscription.plaintext.challenge.secret
|
|
1385
|
+
};
|
|
1386
|
+
const proofValid = insideDecisionWindow ? await verifyEndpointChallengeResponse({ ...proofInput, nowSeconds: input.nowSeconds }) : await verifyEndpointChallengeProof(proofInput);
|
|
1387
|
+
if (insideDecisionWindow && !proofValid) return completeFailure("invalid-proof");
|
|
1388
|
+
const [challengeHash, responseHash] = await Promise.all([
|
|
1389
|
+
endpointChallengeHash(input.challenge),
|
|
1390
|
+
handshakeObjectHash(input.response)
|
|
1391
|
+
]);
|
|
1392
|
+
try {
|
|
1393
|
+
const result = await input.store.completeVerified({
|
|
1394
|
+
challengeHash,
|
|
1395
|
+
nowSeconds: input.nowSeconds,
|
|
1396
|
+
responseHash
|
|
1397
|
+
});
|
|
1398
|
+
const completion = await validateChallengeCompletion(
|
|
1399
|
+
input,
|
|
1400
|
+
result,
|
|
1401
|
+
challengeHash,
|
|
1402
|
+
responseHash,
|
|
1403
|
+
insideDecisionWindow,
|
|
1404
|
+
proofValid
|
|
1405
|
+
);
|
|
1406
|
+
if ("ok" in completion) return completion;
|
|
1407
|
+
const acceptance = createWakeSubscriptionAcceptance({
|
|
1408
|
+
challenge: challengeHash,
|
|
1409
|
+
encryptionKey: input.subscription.context.encryptionKey,
|
|
1410
|
+
grant: input.subscription.context.grant,
|
|
1411
|
+
response: responseHash,
|
|
1412
|
+
service: input.subscription.context.service,
|
|
1413
|
+
subscription: input.subscription.context.subscription
|
|
1414
|
+
});
|
|
1415
|
+
return Object.freeze({
|
|
1416
|
+
acceptance,
|
|
1417
|
+
acceptanceHash: await handshakeObjectHash(acceptance),
|
|
1418
|
+
challengeHash,
|
|
1419
|
+
disposition: completion.kind,
|
|
1420
|
+
ok: true,
|
|
1421
|
+
responseHash
|
|
1422
|
+
});
|
|
1423
|
+
} catch {
|
|
1424
|
+
return completeFailure("challenge-store-unavailable");
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
450
1428
|
// src/statementGraphWakeGrantAuthorizer.ts
|
|
451
1429
|
import {
|
|
452
1430
|
asWakeGrantObject,
|
|
453
1431
|
asWakeGrantRevocation,
|
|
454
1432
|
DeploymentIdZod,
|
|
455
|
-
HashZod,
|
|
456
|
-
NormalizedAddressZod,
|
|
457
|
-
WakeGrantObjectSchema as
|
|
458
|
-
WakeGrantRevocationSchema as
|
|
1433
|
+
HashZod as HashZod4,
|
|
1434
|
+
NormalizedAddressZod as NormalizedAddressZod4,
|
|
1435
|
+
WakeGrantObjectSchema as WakeGrantObjectSchema3,
|
|
1436
|
+
WakeGrantRevocationSchema as WakeGrantRevocationSchema3
|
|
459
1437
|
} from "@xyo-network/event-kit-schemas";
|
|
460
1438
|
function createStatementGraphWakeGrantAuthorizer(options) {
|
|
461
1439
|
const { viewer } = options;
|
|
@@ -466,8 +1444,11 @@ function createStatementGraphWakeGrantAuthorizer(options) {
|
|
|
466
1444
|
if (atBlock > request.expectedHead) {
|
|
467
1445
|
throw new Error(`Statement Graph head ${atBlock} is ahead of expected head ${request.expectedHead}`);
|
|
468
1446
|
}
|
|
469
|
-
const claims = await viewer.claims(request.controller, {
|
|
470
|
-
|
|
1447
|
+
const claims = await viewer.claims(request.controller, {
|
|
1448
|
+
declaredObjectSchemaPrefix: WakeGrantObjectSchema3,
|
|
1449
|
+
subject: request.publisher
|
|
1450
|
+
});
|
|
1451
|
+
const revokedGrantHashes = await readRevocations(viewer, request.controller, request.publisher, atBlock);
|
|
471
1452
|
let evidence;
|
|
472
1453
|
for (const claim of claims) {
|
|
473
1454
|
evidence = await resolveEvidence(viewer, claim, request, atBlock, revokedGrantHashes);
|
|
@@ -496,14 +1477,17 @@ function createStatementGraphWakeGrantAuthorizer(options) {
|
|
|
496
1477
|
}
|
|
497
1478
|
});
|
|
498
1479
|
}
|
|
499
|
-
async function readRevocations(viewer, controller, atBlock) {
|
|
1480
|
+
async function readRevocations(viewer, controller, subject, atBlock) {
|
|
500
1481
|
const revoked = /* @__PURE__ */ new Set();
|
|
501
|
-
const claims = await viewer.claims(controller, {
|
|
1482
|
+
const claims = await viewer.claims(controller, {
|
|
1483
|
+
declaredObjectSchemaPrefix: WakeGrantRevocationSchema3,
|
|
1484
|
+
subject
|
|
1485
|
+
});
|
|
502
1486
|
for (const claim of claims) {
|
|
503
|
-
if (!isLiveClaim(claim, controller,
|
|
1487
|
+
if (!isLiveClaim(claim, controller, subject, WakeGrantRevocationSchema3, atBlock)) continue;
|
|
504
1488
|
const object = await viewer.object(claim.objectHash);
|
|
505
1489
|
if (object?.assurance !== "resolved") continue;
|
|
506
|
-
if (object.declaredSchema !==
|
|
1490
|
+
if (object.declaredSchema !== WakeGrantRevocationSchema3 || object.objectHash !== claim.objectHash) continue;
|
|
507
1491
|
if (!isRecord2(object.params) || Object.hasOwn(object.params, "schema")) continue;
|
|
508
1492
|
const revocation = asWakeGrantRevocation({ ...object.params, schema: object.declaredSchema });
|
|
509
1493
|
if (revocation !== void 0) revoked.add(revocation.grant);
|
|
@@ -511,7 +1495,7 @@ async function readRevocations(viewer, controller, atBlock) {
|
|
|
511
1495
|
return revoked;
|
|
512
1496
|
}
|
|
513
1497
|
async function resolveEvidence(viewer, claim, request, atBlock, revokedGrantHashes) {
|
|
514
|
-
if (!isLiveClaim(claim, request.controller,
|
|
1498
|
+
if (!isLiveClaim(claim, request.controller, request.publisher, WakeGrantObjectSchema3, atBlock)) return void 0;
|
|
515
1499
|
if (revokedGrantHashes.has(claim.objectHash)) return void 0;
|
|
516
1500
|
const object = await viewer.object(claim.objectHash);
|
|
517
1501
|
const grant = parseExactGrantObject(object, claim.objectHash);
|
|
@@ -529,17 +1513,18 @@ async function resolveEvidence(viewer, claim, request, atBlock, revokedGrantHash
|
|
|
529
1513
|
target: grant.publisher
|
|
530
1514
|
});
|
|
531
1515
|
}
|
|
532
|
-
function isLiveClaim(claim, controller, declaredObjectSchema, atBlock) {
|
|
1516
|
+
function isLiveClaim(claim, controller, subject, declaredObjectSchema, atBlock) {
|
|
533
1517
|
if (claim.source !== controller) return false;
|
|
1518
|
+
if (claim.subject !== subject) return false;
|
|
534
1519
|
if (claim.declaredObjectSchema !== declaredObjectSchema) return false;
|
|
535
|
-
if (!
|
|
536
|
-
if (!
|
|
537
|
-
if (!
|
|
538
|
-
return
|
|
1520
|
+
if (!isNonnegativeSafeInteger4(claim.firstBlock) || claim.firstBlock > atBlock) return false;
|
|
1521
|
+
if (!isNonnegativeSafeInteger4(claim.lastBlock) || claim.lastBlock < claim.firstBlock) return false;
|
|
1522
|
+
if (!HashZod4.safeParse(claim.firstEventHash).success) return false;
|
|
1523
|
+
return HashZod4.safeParse(claim.objectHash).success;
|
|
539
1524
|
}
|
|
540
1525
|
function parseExactGrantObject(object, expectedObjectHash) {
|
|
541
1526
|
if (object?.assurance !== "resolved") return void 0;
|
|
542
|
-
if (object.declaredSchema !==
|
|
1527
|
+
if (object.declaredSchema !== WakeGrantObjectSchema3 || object.objectHash !== expectedObjectHash) return void 0;
|
|
543
1528
|
if (!isRecord2(object.params) || Object.hasOwn(object.params, "schema")) return void 0;
|
|
544
1529
|
return asWakeGrantObject({
|
|
545
1530
|
...object.params,
|
|
@@ -547,10 +1532,10 @@ function parseExactGrantObject(object, expectedObjectHash) {
|
|
|
547
1532
|
});
|
|
548
1533
|
}
|
|
549
1534
|
function assertValidRequest(request) {
|
|
550
|
-
if (!
|
|
1535
|
+
if (!NormalizedAddressZod4.safeParse(request.controller).success) {
|
|
551
1536
|
throw new TypeError("Grant controller must be a normalized address");
|
|
552
1537
|
}
|
|
553
|
-
if (!
|
|
1538
|
+
if (!NormalizedAddressZod4.safeParse(request.publisher).success) {
|
|
554
1539
|
throw new TypeError("Grant publisher must be a normalized address");
|
|
555
1540
|
}
|
|
556
1541
|
if (!DeploymentIdZod.safeParse(request.deploymentId).success) {
|
|
@@ -559,18 +1544,18 @@ function assertValidRequest(request) {
|
|
|
559
1544
|
if (request.queueId !== void 0 && (request.queueId.length === 0 || request.queueId.length > 255)) {
|
|
560
1545
|
throw new TypeError("Grant queueId must contain 1 to 255 characters when configured");
|
|
561
1546
|
}
|
|
562
|
-
if (!
|
|
1547
|
+
if (!isNonnegativeSafeInteger4(request.expectedHead)) {
|
|
563
1548
|
throw new RangeError("Grant expectedHead must be a nonnegative safe integer");
|
|
564
1549
|
}
|
|
565
1550
|
}
|
|
566
1551
|
async function readHead(viewer) {
|
|
567
1552
|
const head = await viewer.head();
|
|
568
|
-
if (!
|
|
1553
|
+
if (!isNonnegativeSafeInteger4(head)) {
|
|
569
1554
|
throw new RangeError("Statement Graph head must be a nonnegative safe integer");
|
|
570
1555
|
}
|
|
571
1556
|
return head;
|
|
572
1557
|
}
|
|
573
|
-
function
|
|
1558
|
+
function isNonnegativeSafeInteger4(value) {
|
|
574
1559
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
575
1560
|
}
|
|
576
1561
|
function isRecord2(value) {
|
|
@@ -580,7 +1565,7 @@ function isRecord2(value) {
|
|
|
580
1565
|
// src/verifyWakeRequest.ts
|
|
581
1566
|
import {
|
|
582
1567
|
DeploymentIdZod as DeploymentIdZod2,
|
|
583
|
-
NormalizedAddressZod as
|
|
1568
|
+
NormalizedAddressZod as NormalizedAddressZod5,
|
|
584
1569
|
WakeEnvelopeZod,
|
|
585
1570
|
WakeIdRegExp,
|
|
586
1571
|
WakeJwtAudienceZod,
|
|
@@ -588,7 +1573,7 @@ import {
|
|
|
588
1573
|
WakeJwtPayloadZod,
|
|
589
1574
|
WakeJwtSchema
|
|
590
1575
|
} from "@xyo-network/event-kit-schemas";
|
|
591
|
-
import { Account, PayloadBuilder } from "@xyo-network/sdk";
|
|
1576
|
+
import { Account, PayloadBuilder as PayloadBuilder3 } from "@xyo-network/sdk";
|
|
592
1577
|
var EVENT_KIT_MAX_JWT_LIFETIME_SECONDS = 300;
|
|
593
1578
|
var EVENT_KIT_MAX_FUTURE_IAT_SECONDS = 60;
|
|
594
1579
|
async function verifyWakeRequest(input) {
|
|
@@ -622,7 +1607,7 @@ async function verifyWakeRequest(input) {
|
|
|
622
1607
|
if (!jwtResult.ok) return jwtResult;
|
|
623
1608
|
let eventHash;
|
|
624
1609
|
try {
|
|
625
|
-
eventHash = await
|
|
1610
|
+
eventHash = await PayloadBuilder3.dataHash(event);
|
|
626
1611
|
} catch (error) {
|
|
627
1612
|
return reject2("event-hash-failed", [errorMessage2(error)]);
|
|
628
1613
|
}
|
|
@@ -656,16 +1641,16 @@ function validateConfiguration(route, nowSeconds) {
|
|
|
656
1641
|
if (route.queueId !== void 0 && (route.queueId.length === 0 || route.queueId.length > 255)) {
|
|
657
1642
|
return reject2("configuration-invalid", ["queueId must contain 1 to 255 characters when configured"]);
|
|
658
1643
|
}
|
|
659
|
-
if (!
|
|
1644
|
+
if (!NormalizedAddressZod5.safeParse(route.controller).success) {
|
|
660
1645
|
return reject2("configuration-invalid", ["controller must be a normalized 40-character lowercase-hex address"]);
|
|
661
1646
|
}
|
|
662
|
-
if (!
|
|
1647
|
+
if (!isNonnegativeSafeInteger5(nowSeconds)) {
|
|
663
1648
|
return reject2("configuration-invalid", ["nowSeconds must be a nonnegative safe integer"]);
|
|
664
1649
|
}
|
|
665
1650
|
if (!Number.isSafeInteger(route.maxBlocksPerWake) || route.maxBlocksPerWake < 1) {
|
|
666
1651
|
return reject2("configuration-invalid", ["maxBlocksPerWake must be a positive safe integer"]);
|
|
667
1652
|
}
|
|
668
|
-
if (!
|
|
1653
|
+
if (!isNonnegativeSafeInteger5(route.maxGrantLagBlocks)) {
|
|
669
1654
|
return reject2("configuration-invalid", ["maxGrantLagBlocks must be a nonnegative safe integer"]);
|
|
670
1655
|
}
|
|
671
1656
|
return void 0;
|
|
@@ -700,14 +1685,14 @@ async function verifyJwtProfile(token, audience, nowSeconds) {
|
|
|
700
1685
|
function parseStrictJwtWire(header, payload, audience) {
|
|
701
1686
|
const headerResult = WakeJwtHeaderZod.safeParse(header);
|
|
702
1687
|
if (!headerResult.success) {
|
|
703
|
-
if (!
|
|
1688
|
+
if (!NormalizedAddressZod5.safeParse(header.kid).success) return reject2("jwt-identity-invalid");
|
|
704
1689
|
return reject2("jwt-header-invalid", zodDetails(headerResult.error.issues));
|
|
705
1690
|
}
|
|
706
1691
|
if (Object.hasOwn(payload, "nbf")) return reject2("jwt-nbf-not-allowed");
|
|
707
|
-
if (!
|
|
1692
|
+
if (!NormalizedAddressZod5.safeParse(payload.iss).success) return reject2("jwt-identity-invalid");
|
|
708
1693
|
if (payload.schema !== WakeJwtSchema) return reject2("jwt-schema-mismatch");
|
|
709
1694
|
if (payload.aud !== audience) return reject2("jwt-audience-mismatch");
|
|
710
|
-
if (!
|
|
1695
|
+
if (!isNonnegativeSafeInteger5(payload.iat) || !isNonnegativeSafeInteger5(payload.exp) || payload.exp <= payload.iat) {
|
|
711
1696
|
return reject2("jwt-lifetime-invalid");
|
|
712
1697
|
}
|
|
713
1698
|
const payloadResult = WakeJwtPayloadZod.safeParse(payload);
|
|
@@ -730,14 +1715,14 @@ function classifySdkJwtFailure(reasons) {
|
|
|
730
1715
|
return "jwt-invalid";
|
|
731
1716
|
}
|
|
732
1717
|
function hasSafeEventNumbers(event) {
|
|
733
|
-
return
|
|
1718
|
+
return isNonnegativeSafeInteger5(event.fromBlock) && isNonnegativeSafeInteger5(event.toBlock) && isNonnegativeSafeInteger5(event.issuedAt);
|
|
734
1719
|
}
|
|
735
1720
|
function hasInvertedSafeRange(value) {
|
|
736
1721
|
if (!isRecord3(value)) return false;
|
|
737
1722
|
const { fromBlock, toBlock } = value;
|
|
738
|
-
return typeof fromBlock === "number" && typeof toBlock === "number" &&
|
|
1723
|
+
return typeof fromBlock === "number" && typeof toBlock === "number" && isNonnegativeSafeInteger5(fromBlock) && isNonnegativeSafeInteger5(toBlock) && fromBlock > toBlock;
|
|
739
1724
|
}
|
|
740
|
-
function
|
|
1725
|
+
function isNonnegativeSafeInteger5(value) {
|
|
741
1726
|
return Number.isSafeInteger(value) && value >= 0;
|
|
742
1727
|
}
|
|
743
1728
|
function isRecord3(value) {
|
|
@@ -791,12 +1776,47 @@ export {
|
|
|
791
1776
|
EVENT_KIT_MAX_FUTURE_IAT_SECONDS,
|
|
792
1777
|
EVENT_KIT_MAX_JWT_LIFETIME_SECONDS,
|
|
793
1778
|
EVENT_KIT_REPLAY_RETENTION_SKEW_SECONDS,
|
|
1779
|
+
WakeSubscriptionDecryptionError,
|
|
794
1780
|
admitAndEnqueue,
|
|
1781
|
+
beginPublisherEndpointChallengeAttempt,
|
|
1782
|
+
canonicalJson,
|
|
1783
|
+
canonicalJsonBytes,
|
|
1784
|
+
completePublisherEndpointChallenge,
|
|
1785
|
+
createEncryptedWakeSubscription,
|
|
1786
|
+
createEndpointChallenge,
|
|
1787
|
+
createEndpointChallengeResponse,
|
|
795
1788
|
createMemoryStatementGrantGraph,
|
|
796
1789
|
createMemoryWakeInbox,
|
|
797
1790
|
createMemoryWakeQueue,
|
|
1791
|
+
createPublisherEncryptionKey,
|
|
1792
|
+
createPublisherEncryptionKeyRevocation,
|
|
1793
|
+
createPublisherService,
|
|
1794
|
+
createPublisherServiceRevocation,
|
|
798
1795
|
createStatementGraphWakeGrantAuthorizer,
|
|
1796
|
+
createWakeGrant,
|
|
1797
|
+
createWakeGrantRevocation,
|
|
1798
|
+
createWakeSubscriptionAcceptance,
|
|
1799
|
+
createWakeSubscriptionAcceptanceRevocation,
|
|
1800
|
+
createWakeSubscriptionPlaintext,
|
|
1801
|
+
createWakeSubscriptionRevocation,
|
|
1802
|
+
decryptWakeSubscription,
|
|
799
1803
|
drainQueue,
|
|
800
|
-
|
|
1804
|
+
encryptWakeSubscription,
|
|
1805
|
+
endpointChallengeHash,
|
|
1806
|
+
endpointChallengeMatchesPublisherContext,
|
|
1807
|
+
generatePublisherEncryptionKey,
|
|
1808
|
+
generateWakeSubscriptionEncryptionKeyPair,
|
|
1809
|
+
handshakeObjectHash,
|
|
1810
|
+
issuePublisherEndpointChallenge,
|
|
1811
|
+
resolvePreferredEncryptionKey,
|
|
1812
|
+
selectPublisherService,
|
|
1813
|
+
selectSubscriptionActivation,
|
|
1814
|
+
validateAndDecryptPublisherSubscription,
|
|
1815
|
+
verifyEndpointChallengeResponse,
|
|
1816
|
+
verifyWakeRequest,
|
|
1817
|
+
wakeSubscriptionAadBytes,
|
|
1818
|
+
wakeSubscriptionPlaintextBytes,
|
|
1819
|
+
wakeSubscriptionPlaintextFromBytes,
|
|
1820
|
+
withHandshakeObjectHash
|
|
801
1821
|
};
|
|
802
1822
|
//# sourceMappingURL=index.mjs.map
|