@did-btcr2/method 0.35.0 → 0.36.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.
Files changed (55) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/browser.js +460 -223
  3. package/dist/browser.mjs +460 -223
  4. package/dist/cjs/index.js +461 -223
  5. package/dist/esm/core/aggregation/cohort.js +3 -1
  6. package/dist/esm/core/aggregation/cohort.js.map +1 -1
  7. package/dist/esm/core/aggregation/conditions.js +75 -0
  8. package/dist/esm/core/aggregation/conditions.js.map +1 -0
  9. package/dist/esm/core/aggregation/messages/base.js.map +1 -1
  10. package/dist/esm/core/aggregation/messages/bodies.js +16 -2
  11. package/dist/esm/core/aggregation/messages/bodies.js.map +1 -1
  12. package/dist/esm/core/aggregation/messages/factories.js.map +1 -1
  13. package/dist/esm/core/aggregation/participant.js +20 -7
  14. package/dist/esm/core/aggregation/participant.js.map +1 -1
  15. package/dist/esm/core/aggregation/runner/participant-runner.js +37 -2
  16. package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
  17. package/dist/esm/core/aggregation/runner/service-runner.js +323 -189
  18. package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
  19. package/dist/esm/core/aggregation/service.js +23 -3
  20. package/dist/esm/core/aggregation/service.js.map +1 -1
  21. package/dist/esm/index.js +1 -0
  22. package/dist/esm/index.js.map +1 -1
  23. package/dist/types/core/aggregation/cohort.d.ts.map +1 -1
  24. package/dist/types/core/aggregation/conditions.d.ts +58 -0
  25. package/dist/types/core/aggregation/conditions.d.ts.map +1 -0
  26. package/dist/types/core/aggregation/messages/base.d.ts +2 -3
  27. package/dist/types/core/aggregation/messages/base.d.ts.map +1 -1
  28. package/dist/types/core/aggregation/messages/bodies.d.ts +2 -3
  29. package/dist/types/core/aggregation/messages/bodies.d.ts.map +1 -1
  30. package/dist/types/core/aggregation/messages/factories.d.ts +2 -3
  31. package/dist/types/core/aggregation/messages/factories.d.ts.map +1 -1
  32. package/dist/types/core/aggregation/participant.d.ts +16 -4
  33. package/dist/types/core/aggregation/participant.d.ts.map +1 -1
  34. package/dist/types/core/aggregation/runner/events.d.ts +22 -11
  35. package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
  36. package/dist/types/core/aggregation/runner/participant-runner.d.ts +21 -12
  37. package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
  38. package/dist/types/core/aggregation/runner/service-runner.d.ts +76 -22
  39. package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
  40. package/dist/types/core/aggregation/service.d.ts +8 -4
  41. package/dist/types/core/aggregation/service.d.ts.map +1 -1
  42. package/dist/types/index.d.ts +1 -0
  43. package/dist/types/index.d.ts.map +1 -1
  44. package/package.json +4 -4
  45. package/src/core/aggregation/cohort.ts +3 -1
  46. package/src/core/aggregation/conditions.ts +116 -0
  47. package/src/core/aggregation/messages/base.ts +6 -3
  48. package/src/core/aggregation/messages/bodies.ts +18 -6
  49. package/src/core/aggregation/messages/factories.ts +2 -3
  50. package/src/core/aggregation/participant.ts +28 -11
  51. package/src/core/aggregation/runner/events.ts +23 -14
  52. package/src/core/aggregation/runner/participant-runner.ts +43 -13
  53. package/src/core/aggregation/runner/service-runner.ts +375 -195
  54. package/src/core/aggregation/service.ts +39 -7
  55. package/src/index.ts +1 -0
package/dist/cjs/index.js CHANGED
@@ -86,6 +86,7 @@ __export(index_exports, {
86
86
  InMemoryRateLimitStore: () => InMemoryRateLimitStore,
87
87
  InMemoryTransport: () => InMemoryTransport,
88
88
  InboxBuffer: () => InboxBuffer,
89
+ KNOWN_BEACON_TYPES: () => KNOWN_BEACON_TYPES,
89
90
  KeyPairAggregationSigner: () => KeyPairAggregationSigner,
90
91
  NONCE_CONTRIBUTION: () => NONCE_CONTRIBUTION,
91
92
  NonceCache: () => NonceCache,
@@ -158,6 +159,7 @@ __export(index_exports, {
158
159
  registerBeaconStrategy: () => registerBeaconStrategy,
159
160
  reviveFromWire: () => reviveFromWire,
160
161
  signEnvelope: () => signEnvelope,
162
+ validateCohortConditions: () => validateCohortConditions,
161
163
  verifyEnvelope: () => verifyEnvelope,
162
164
  verifyRequestAuth: () => verifyRequestAuth
163
165
  });
@@ -301,7 +303,7 @@ var AggregationCohort = class {
301
303
  validationRejections = /* @__PURE__ */ new Set();
302
304
  constructor({ id, minParticipants, serviceDid, network, beaconType }) {
303
305
  this.id = id || crypto.randomUUID();
304
- this.minParticipants = minParticipants || 2;
306
+ this.minParticipants = minParticipants ?? 2;
305
307
  this.serviceDid = serviceDid || "";
306
308
  this.network = network;
307
309
  this.beaconType = beaconType || "CASBeacon";
@@ -467,6 +469,56 @@ var AggregationCohort = class {
467
469
  }
468
470
  };
469
471
 
472
+ // src/core/aggregation/conditions.ts
473
+ var KNOWN_BEACON_TYPES = ["CASBeacon", "SMTBeacon"];
474
+ function checkPair(problems, label, min, max) {
475
+ if (min !== void 0 && (!Number.isInteger(min) || min < 0)) {
476
+ problems.push(`min${label} must be an integer >= 0`);
477
+ }
478
+ if (max !== void 0 && (!Number.isInteger(max) || max < 0)) {
479
+ problems.push(`max${label} must be an integer >= 0`);
480
+ }
481
+ if (min !== void 0 && max !== void 0 && Number.isInteger(min) && Number.isInteger(max) && max < min) {
482
+ problems.push(`max${label} must be >= min${label}`);
483
+ }
484
+ }
485
+ function checkCost(problems, label, cost) {
486
+ if (cost === void 0) return;
487
+ if (typeof cost.amount !== "number" || !Number.isFinite(cost.amount) || cost.amount < 0) {
488
+ problems.push(`${label}.amount must be a finite number >= 0`);
489
+ }
490
+ if (typeof cost.unit !== "string" || cost.unit.length === 0) {
491
+ problems.push(`${label}.unit must be a non-empty string`);
492
+ }
493
+ if (cost.basis !== void 0 && cost.basis !== "per-did" && cost.basis !== "per-participant") {
494
+ problems.push(`${label}.basis must be 'per-did' or 'per-participant'`);
495
+ }
496
+ }
497
+ function validateCohortConditions(c) {
498
+ const problems = [];
499
+ if (!KNOWN_BEACON_TYPES.includes(c.beaconType)) {
500
+ problems.push(`beaconType must be one of ${KNOWN_BEACON_TYPES.join(", ")}`);
501
+ }
502
+ if (!Number.isInteger(c.minParticipants) || c.minParticipants < 1) {
503
+ problems.push("minParticipants must be an integer >= 1");
504
+ }
505
+ if (c.maxParticipants !== void 0) {
506
+ if (!Number.isInteger(c.maxParticipants) || c.maxParticipants < 1) {
507
+ problems.push("maxParticipants must be an integer >= 1");
508
+ } else if (Number.isInteger(c.minParticipants) && c.maxParticipants < c.minParticipants) {
509
+ problems.push("maxParticipants must be >= minParticipants");
510
+ }
511
+ }
512
+ checkPair(problems, "DidsPerParticipant", c.minDidsPerParticipant, c.maxDidsPerParticipant);
513
+ checkPair(problems, "SecondsBetweenAnnouncements", c.minSecondsBetweenAnnouncements, c.maxSecondsBetweenAnnouncements);
514
+ if (c.pendingUpdateTrigger !== void 0 && (!Number.isInteger(c.pendingUpdateTrigger) || c.pendingUpdateTrigger < 1)) {
515
+ problems.push("pendingUpdateTrigger must be an integer >= 1");
516
+ }
517
+ checkCost(problems, "costOfEnrollment", c.costOfEnrollment);
518
+ checkCost(problems, "costPerAnnouncement", c.costPerAnnouncement);
519
+ return problems;
520
+ }
521
+
470
522
  // src/core/aggregation/messages/base.ts
471
523
  var AGGREGATION_WIRE_VERSION = 1;
472
524
  var BaseMessage = class {
@@ -899,6 +951,14 @@ var AggregationService = class {
899
951
  * Cohort starts in `Created` phase — call `advertise()` to broadcast.
900
952
  */
901
953
  createCohort(config) {
954
+ const problems = validateCohortConditions(config);
955
+ if (problems.length > 0) {
956
+ throw new AggregationServiceError(
957
+ `Invalid cohort conditions: ${problems.join("; ")}`,
958
+ "INVALID_COHORT_CONDITIONS",
959
+ { problems }
960
+ );
961
+ }
902
962
  const cohort = new AggregationCohort({
903
963
  serviceDid: this.did,
904
964
  minParticipants: config.minParticipants,
@@ -931,13 +991,13 @@ var AggregationService = class {
931
991
  { cohortId, phase: state.phase }
932
992
  );
933
993
  }
994
+ const { network, ...conditions } = state.config;
934
995
  const message = createCohortAdvertMessage({
935
996
  from: this.did,
936
997
  cohortId,
937
- cohortSize: state.config.minParticipants,
938
- beaconType: state.config.beaconType,
939
- network: state.config.network,
940
- communicationPk: this.publicKey.compressed
998
+ network,
999
+ communicationPk: this.publicKey.compressed,
1000
+ ...conditions
941
1001
  });
942
1002
  state.phase = "Advertised" /* Advertised */;
943
1003
  return [message];
@@ -996,6 +1056,14 @@ var AggregationService = class {
996
1056
  { cohortId, participantDid }
997
1057
  );
998
1058
  }
1059
+ const maxParticipants = state.config.maxParticipants;
1060
+ if (maxParticipants !== void 0 && state.acceptedParticipants.size >= maxParticipants) {
1061
+ throw new AggregationServiceError(
1062
+ `Cohort ${cohortId} is full: ${maxParticipants} participants already accepted.`,
1063
+ "COHORT_FULL",
1064
+ { cohortId, maxParticipants }
1065
+ );
1066
+ }
999
1067
  state.acceptedParticipants.add(participantDid);
1000
1068
  state.cohort.participants.push(participantDid);
1001
1069
  state.cohort.participantKeys.set(participantDid, optIn.participantPk);
@@ -1029,6 +1097,14 @@ var AggregationService = class {
1029
1097
  { cohortId }
1030
1098
  );
1031
1099
  }
1100
+ const maxParticipants = state.config.maxParticipants;
1101
+ if (maxParticipants !== void 0 && state.acceptedParticipants.size > maxParticipants) {
1102
+ throw new AggregationServiceError(
1103
+ `Cohort ${cohortId} has ${state.acceptedParticipants.size} accepted participants, exceeds max ${maxParticipants}.`,
1104
+ "TOO_MANY_PARTICIPANTS",
1105
+ { cohortId, maxParticipants }
1106
+ );
1107
+ }
1032
1108
  const beaconAddress = state.cohort.computeBeaconAddress();
1033
1109
  state.phase = "CohortSet" /* CohortSet */;
1034
1110
  const messages = [];
@@ -1347,6 +1423,58 @@ var AggregationService = class {
1347
1423
  var import_common5 = require("@did-btcr2/common");
1348
1424
  var import_utils3 = require("@noble/hashes/utils");
1349
1425
  var import_btc_signer3 = require("@scure/btc-signer");
1426
+
1427
+ // src/core/aggregation/messages/bodies.ts
1428
+ var hasStr = (b, k) => !!b && typeof b[k] === "string";
1429
+ var hasIntMin = (b, k, min) => {
1430
+ const v = b ? b[k] : void 0;
1431
+ return typeof v === "number" && Number.isInteger(v) && v >= min;
1432
+ };
1433
+ var optIntMin = (b, k, min) => {
1434
+ const v = b ? b[k] : void 0;
1435
+ return v === void 0 || typeof v === "number" && Number.isInteger(v) && v >= min;
1436
+ };
1437
+ var hasBool = (b, k) => !!b && typeof b[k] === "boolean";
1438
+ var hasBytes = (b, k) => !!b && b[k] instanceof Uint8Array;
1439
+ var hasBytesArray = (b, k) => {
1440
+ const v = b ? b[k] : void 0;
1441
+ return Array.isArray(v) && v.every((x) => x instanceof Uint8Array);
1442
+ };
1443
+ function isCohortAdvertMessage(m) {
1444
+ return m.type === COHORT_ADVERT && hasStr(m.body, "cohortId") && hasIntMin(m.body, "minParticipants", 1) && optIntMin(m.body, "maxParticipants", 1) && hasStr(m.body, "beaconType") && hasStr(m.body, "network") && hasBytes(m.body, "communicationPk");
1445
+ }
1446
+ function isCohortOptInMessage(m) {
1447
+ return m.type === COHORT_OPT_IN && hasStr(m.body, "cohortId") && hasBytes(m.body, "participantPk") && hasBytes(m.body, "communicationPk");
1448
+ }
1449
+ function isCohortOptInAcceptMessage(m) {
1450
+ return m.type === COHORT_OPT_IN_ACCEPT && hasStr(m.body, "cohortId");
1451
+ }
1452
+ function isCohortReadyMessage(m) {
1453
+ return m.type === COHORT_READY && hasStr(m.body, "cohortId") && hasStr(m.body, "beaconAddress") && hasBytesArray(m.body, "cohortKeys");
1454
+ }
1455
+ function isSubmitUpdateMessage(m) {
1456
+ return m.type === SUBMIT_UPDATE && hasStr(m.body, "cohortId") && !!m.body && typeof m.body.signedUpdate === "object";
1457
+ }
1458
+ function isDistributeAggregatedDataMessage(m) {
1459
+ return m.type === DISTRIBUTE_AGGREGATED_DATA && hasStr(m.body, "cohortId") && hasStr(m.body, "beaconType") && hasStr(m.body, "signalBytesHex");
1460
+ }
1461
+ function isValidationAckMessage(m) {
1462
+ return m.type === VALIDATION_ACK && hasStr(m.body, "cohortId") && hasBool(m.body, "approved");
1463
+ }
1464
+ function isAuthorizationRequestMessage(m) {
1465
+ return m.type === AUTHORIZATION_REQUEST && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasStr(m.body, "pendingTx") && hasStr(m.body, "prevOutScriptHex") && hasStr(m.body, "prevOutValue");
1466
+ }
1467
+ function isNonceContributionMessage(m) {
1468
+ return m.type === NONCE_CONTRIBUTION && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "nonceContribution");
1469
+ }
1470
+ function isAggregatedNonceMessage(m) {
1471
+ return m.type === AGGREGATED_NONCE && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "aggregatedNonce");
1472
+ }
1473
+ function isSignatureAuthorizationMessage(m) {
1474
+ return m.type === SIGNATURE_AUTHORIZATION && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "partialSignature");
1475
+ }
1476
+
1477
+ // src/core/aggregation/participant.ts
1350
1478
  var AggregationParticipant = class {
1351
1479
  did;
1352
1480
  /** MuSig2 signing capability. The raw secret never lives as a field here. */
@@ -1404,16 +1532,15 @@ var AggregationParticipant = class {
1404
1532
  return map;
1405
1533
  }
1406
1534
  #handleCohortAdvert(message) {
1407
- const cohortId = message.body?.cohortId;
1408
- if (!cohortId) return;
1535
+ if (!isCohortAdvertMessage(message)) return;
1536
+ const { cohortId, network, communicationPk, ...conditions } = message.body;
1409
1537
  if (this.#cohortStates.has(cohortId)) return;
1410
1538
  const advert = {
1411
1539
  cohortId,
1412
1540
  serviceDid: message.from,
1413
- cohortSize: message.body?.cohortSize ?? 0,
1414
- network: message.body?.network ?? "",
1415
- beaconType: message.body?.beaconType ?? "CASBeacon",
1416
- serviceCommunicationPk: message.body?.communicationPk ?? new Uint8Array()
1541
+ network,
1542
+ serviceCommunicationPk: communicationPk,
1543
+ ...conditions
1417
1544
  };
1418
1545
  this.#cohortStates.set(cohortId, {
1419
1546
  phase: "Discovered" /* Discovered */,
@@ -1438,7 +1565,7 @@ var AggregationParticipant = class {
1438
1565
  const cohort = new AggregationCohort({
1439
1566
  id: cohortId,
1440
1567
  serviceDid: state.serviceDid,
1441
- minParticipants: state.advert.cohortSize,
1568
+ minParticipants: state.advert.minParticipants,
1442
1569
  network: state.advert.network,
1443
1570
  beaconType: state.advert.beaconType
1444
1571
  });
@@ -1518,6 +1645,17 @@ var AggregationParticipant = class {
1518
1645
  }
1519
1646
  return map;
1520
1647
  }
1648
+ /**
1649
+ * The validated aggregated data retained for a cohort, regardless of phase.
1650
+ * Unlike {@link pendingValidations} (which lists only cohorts still awaiting
1651
+ * the validate decision), this returns the stored validation — including the
1652
+ * participant's sidecar (the CAS Announcement map or its SMT inclusion proof)
1653
+ * — so it is still readable once the cohort reaches Complete. Returns
1654
+ * undefined before aggregated data has been received.
1655
+ */
1656
+ getValidation(cohortId) {
1657
+ return this.#cohortStates.get(cohortId)?.validation;
1658
+ }
1521
1659
  #handleDistributeAggregatedData(message) {
1522
1660
  const cohortId = message.body?.cohortId;
1523
1661
  if (!cohortId) return;
@@ -1758,49 +1896,6 @@ var SILENT_LOGGER = {
1758
1896
  }
1759
1897
  };
1760
1898
 
1761
- // src/core/aggregation/messages/bodies.ts
1762
- var hasStr = (b, k) => !!b && typeof b[k] === "string";
1763
- var hasNum = (b, k) => !!b && typeof b[k] === "number";
1764
- var hasBool = (b, k) => !!b && typeof b[k] === "boolean";
1765
- var hasBytes = (b, k) => !!b && b[k] instanceof Uint8Array;
1766
- var hasBytesArray = (b, k) => {
1767
- const v = b ? b[k] : void 0;
1768
- return Array.isArray(v) && v.every((x) => x instanceof Uint8Array);
1769
- };
1770
- function isCohortAdvertMessage(m) {
1771
- return m.type === COHORT_ADVERT && hasStr(m.body, "cohortId") && hasNum(m.body, "cohortSize") && hasStr(m.body, "beaconType") && hasStr(m.body, "network") && hasBytes(m.body, "communicationPk");
1772
- }
1773
- function isCohortOptInMessage(m) {
1774
- return m.type === COHORT_OPT_IN && hasStr(m.body, "cohortId") && hasBytes(m.body, "participantPk") && hasBytes(m.body, "communicationPk");
1775
- }
1776
- function isCohortOptInAcceptMessage(m) {
1777
- return m.type === COHORT_OPT_IN_ACCEPT && hasStr(m.body, "cohortId");
1778
- }
1779
- function isCohortReadyMessage(m) {
1780
- return m.type === COHORT_READY && hasStr(m.body, "cohortId") && hasStr(m.body, "beaconAddress") && hasBytesArray(m.body, "cohortKeys");
1781
- }
1782
- function isSubmitUpdateMessage(m) {
1783
- return m.type === SUBMIT_UPDATE && hasStr(m.body, "cohortId") && !!m.body && typeof m.body.signedUpdate === "object";
1784
- }
1785
- function isDistributeAggregatedDataMessage(m) {
1786
- return m.type === DISTRIBUTE_AGGREGATED_DATA && hasStr(m.body, "cohortId") && hasStr(m.body, "beaconType") && hasStr(m.body, "signalBytesHex");
1787
- }
1788
- function isValidationAckMessage(m) {
1789
- return m.type === VALIDATION_ACK && hasStr(m.body, "cohortId") && hasBool(m.body, "approved");
1790
- }
1791
- function isAuthorizationRequestMessage(m) {
1792
- return m.type === AUTHORIZATION_REQUEST && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasStr(m.body, "pendingTx") && hasStr(m.body, "prevOutScriptHex") && hasStr(m.body, "prevOutValue");
1793
- }
1794
- function isNonceContributionMessage(m) {
1795
- return m.type === NONCE_CONTRIBUTION && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "nonceContribution");
1796
- }
1797
- function isAggregatedNonceMessage(m) {
1798
- return m.type === AGGREGATED_NONCE && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "aggregatedNonce");
1799
- }
1800
- function isSignatureAuthorizationMessage(m) {
1801
- return m.type === SIGNATURE_AUTHORIZATION && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "partialSignature");
1802
- }
1803
-
1804
1899
  // src/core/aggregation/messages/guards.ts
1805
1900
  var KEYGEN_VALUES = /* @__PURE__ */ new Set([
1806
1901
  COHORT_ADVERT,
@@ -3933,35 +4028,22 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
3933
4028
  session;
3934
4029
  #transport;
3935
4030
  #did;
3936
- #config;
4031
+ #defaultConfig;
3937
4032
  #onOptInReceived;
3938
4033
  #onReadyToFinalize;
3939
4034
  #onProvideTxData;
3940
4035
  #cohortTtlMs;
3941
4036
  #phaseTimeoutMs;
3942
4037
  #advertRepeatIntervalMs;
3943
- #cohortId;
4038
+ /** Per-cohort run state, keyed by cohortId. */
4039
+ #contexts = /* @__PURE__ */ new Map();
3944
4040
  #handlersRegistered = false;
3945
4041
  #stopped = false;
3946
- /**
3947
- * Guard against the async race where two concurrent #handleOptIn invocations
3948
- * both pass the `participants.length >= minParticipants` check before either
3949
- * mutates the cohort phase. Set synchronously before any `await` so subsequent
3950
- * handlers observe it on their next resumption.
3951
- */
3952
- #finalizing = false;
3953
- #resolveRun;
3954
- #rejectRun;
3955
- #cohortTtlTimer;
3956
- #phaseTimer;
3957
- #lastObservedPhase;
3958
- /** Stop handle for the repeating COHORT_ADVERT publish loop. */
3959
- #stopAdvertRepeat;
3960
4042
  constructor(options) {
3961
4043
  super();
3962
4044
  this.#transport = options.transport;
3963
4045
  this.#did = options.did;
3964
- this.#config = options.config;
4046
+ this.#defaultConfig = options.config;
3965
4047
  this.#onOptInReceived = options.onOptInReceived ?? (async () => ({ accepted: true }));
3966
4048
  this.#onReadyToFinalize = options.onReadyToFinalize ?? (async ({ acceptedCount, minRequired }) => ({
3967
4049
  finalize: acceptedCount >= minRequired
@@ -3979,55 +4061,126 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
3979
4061
  maxUpdateSizeBytes: options.maxUpdateSizeBytes
3980
4062
  });
3981
4063
  }
4064
+ /** Resolve the {@link RunContext} an inbound message belongs to, by cohortId. */
4065
+ #contextFor(msg) {
4066
+ const cohortId = msg.body?.cohortId;
4067
+ if (!cohortId) return void 0;
4068
+ return this.#contexts.get(cohortId);
4069
+ }
3982
4070
  /**
3983
- * Drain any silent rejections the state machine recorded during the most
3984
- * recent receive() and surface them as `message-rejected` events. Safe to
3985
- * call even before a cohortId is assigned.
4071
+ * Drain any silent rejections the state machine recorded for a cohort during
4072
+ * the most recent receive() and surface them as `message-rejected` events.
3986
4073
  */
3987
- #drainRejections() {
3988
- if (!this.#cohortId) return;
3989
- for (const r of this.session.drainRejections(this.#cohortId)) {
3990
- this.emit("message-rejected", { cohortId: this.#cohortId, ...r });
4074
+ #drainRejections(ctx) {
4075
+ for (const r of this.session.drainRejections(ctx.cohortId)) {
4076
+ this.emit("message-rejected", { cohortId: ctx.cohortId, ...r });
3991
4077
  }
3992
4078
  }
3993
4079
  /**
3994
- * Run the protocol to completion. Resolves with the final aggregation result
3995
- * (signature + signed transaction) once signing is complete.
4080
+ * Advertise a new cohort and begin driving it to completion. Callable many
4081
+ * times on one runner; each cohort runs concurrently and independently.
4082
+ *
4083
+ * @param config Per-cohort conditions + network (see {@link CohortConfig}).
4084
+ * @returns The new cohort's id and a `completion` promise that resolves with
4085
+ * that cohort's {@link AggregationResult} (or rejects if it fails/stalls).
4086
+ * @throws If the runner has been stopped, or the config is invalid
4087
+ * (fail-fast via `createCohort`).
4088
+ */
4089
+ advertiseCohort(config) {
4090
+ if (this.#stopped) {
4091
+ throw new AggregationServiceError("Cannot advertise on a stopped runner.", "RUNNER_STOPPED", {});
4092
+ }
4093
+ this.#registerHandlers();
4094
+ const cohortId = this.session.createCohort(config);
4095
+ let resolve;
4096
+ let reject;
4097
+ const completion = new Promise((res, rej) => {
4098
+ resolve = res;
4099
+ reject = rej;
4100
+ });
4101
+ const ctx = {
4102
+ cohortId,
4103
+ config,
4104
+ resolve,
4105
+ reject,
4106
+ completion,
4107
+ finalizing: false,
4108
+ settled: false
4109
+ };
4110
+ this.#contexts.set(cohortId, ctx);
4111
+ try {
4112
+ this.#startTimers(ctx);
4113
+ const advertMsgs = this.session.advertise(cohortId);
4114
+ this.#onPhaseMaybeChanged(ctx);
4115
+ this.emit("cohort-advertised", { cohortId });
4116
+ if (this.#advertRepeatIntervalMs > 0) {
4117
+ this.#startAdvertRepeat(ctx, advertMsgs);
4118
+ } else {
4119
+ this.#sendAll(advertMsgs).catch((err) => this.#failCohort(ctx, err));
4120
+ }
4121
+ } catch (err) {
4122
+ this.#failCohort(ctx, err);
4123
+ }
4124
+ return { cohortId, completion };
4125
+ }
4126
+ /**
4127
+ * Run a single cohort to completion using the `config` supplied in the
4128
+ * runner options. Thin convenience over {@link advertiseCohort} for the
4129
+ * single-cohort case (and the path {@link AggregationRunner.solo} rides).
3996
4130
  *
3997
4131
  * @returns {Promise<AggregationResult>} The final result with signature and signed tx.
3998
4132
  */
3999
4133
  run() {
4000
- return new Promise((resolve, reject) => {
4001
- this.#resolveRun = resolve;
4002
- this.#rejectRun = reject;
4003
- try {
4004
- this.#registerHandlers();
4005
- this.#cohortId = this.session.createCohort(this.#config);
4006
- this.#startTimers();
4007
- const advertMsgs = this.session.advertise(this.#cohortId);
4008
- this.#onPhaseMaybeChanged();
4009
- this.emit("cohort-advertised", { cohortId: this.#cohortId });
4010
- if (this.#advertRepeatIntervalMs > 0) {
4011
- this.#startAdvertRepeat(advertMsgs);
4012
- } else {
4013
- this.#sendAll(advertMsgs).catch((err) => this.#fail(err));
4014
- }
4015
- } catch (err) {
4016
- this.#fail(err);
4134
+ if (!this.#defaultConfig) {
4135
+ return Promise.reject(new AggregationServiceError(
4136
+ "run() requires `config` in the runner options; use advertiseCohort(config) to drive cohorts explicitly.",
4137
+ "MISSING_COHORT_CONFIG",
4138
+ {}
4139
+ ));
4140
+ }
4141
+ try {
4142
+ return this.advertiseCohort(this.#defaultConfig).completion;
4143
+ } catch (err) {
4144
+ return Promise.reject(err);
4145
+ }
4146
+ }
4147
+ /**
4148
+ * Wait for every currently-outstanding cohort to settle and return the
4149
+ * successful results. Dynamic drain: cohorts advertised while this is pending
4150
+ * are included, and it resolves only once no cohorts remain. Failed cohorts
4151
+ * are surfaced via `error` / `cohort-failed` events and their rejected
4152
+ * `completion` promises; they are omitted from the returned array (this
4153
+ * method does not throw). Bound long-running cohorts with `cohortTtlMs` /
4154
+ * `phaseTimeoutMs` or this may never resolve.
4155
+ *
4156
+ * @returns {Promise<AggregationResult[]>} Results of the cohorts that completed.
4157
+ */
4158
+ async runAll() {
4159
+ const collected = /* @__PURE__ */ new Map();
4160
+ const onComplete = (result) => {
4161
+ collected.set(result.cohortId, result);
4162
+ };
4163
+ this.on("signing-complete", onComplete);
4164
+ try {
4165
+ while (this.#contexts.size > 0) {
4166
+ await Promise.allSettled([...this.#contexts.values()].map((c) => c.completion));
4017
4167
  }
4018
- });
4168
+ } finally {
4169
+ this.off("signing-complete", onComplete);
4170
+ }
4171
+ return [...collected.values()];
4019
4172
  }
4020
4173
  /**
4021
- * Begin publishing the cohort advert immediately and on a repeating interval
4022
- * until {@link #stopAdvertRepeating} is called. Each advert is broadcast
4023
- * (no recipient) via the transport's `publishRepeating` primitive.
4174
+ * Begin publishing a cohort's advert immediately and on a repeating interval
4175
+ * until the cohort's advert loop is stopped. Each advert is broadcast (no
4176
+ * recipient) via the transport's `publishRepeating` primitive.
4024
4177
  */
4025
- #startAdvertRepeat(advertMsgs) {
4178
+ #startAdvertRepeat(ctx, advertMsgs) {
4026
4179
  const stops = [];
4027
4180
  for (const msg of advertMsgs) {
4028
4181
  stops.push(this.#transport.publishRepeating(msg, this.#did, this.#advertRepeatIntervalMs));
4029
4182
  }
4030
- this.#stopAdvertRepeat = () => {
4183
+ ctx.stopAdvertRepeat = () => {
4031
4184
  for (const stop of stops) {
4032
4185
  try {
4033
4186
  stop();
@@ -4036,61 +4189,118 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
4036
4189
  }
4037
4190
  };
4038
4191
  }
4039
- /** Stop the advert republish loop. Idempotent. */
4040
- #stopAdvertRepeating() {
4041
- if (!this.#stopAdvertRepeat) return;
4042
- const stop = this.#stopAdvertRepeat;
4043
- this.#stopAdvertRepeat = void 0;
4192
+ /** Stop a cohort's advert republish loop. Idempotent. */
4193
+ #stopAdvertRepeating(ctx) {
4194
+ if (!ctx.stopAdvertRepeat) return;
4195
+ const stop = ctx.stopAdvertRepeat;
4196
+ ctx.stopAdvertRepeat = void 0;
4044
4197
  stop();
4045
4198
  }
4046
- /** Schedule cohort TTL + phase timeout at the start of a run. */
4047
- #startTimers() {
4199
+ /** Schedule a cohort's TTL + phase timeout when it is advertised. */
4200
+ #startTimers(ctx) {
4048
4201
  if (this.#cohortTtlMs !== void 0) {
4049
- this.#cohortTtlTimer = setTimeout(() => {
4050
- const reason = `Cohort ${this.#cohortId ?? ""} exceeded TTL of ${this.#cohortTtlMs}ms`;
4051
- this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
4052
- this.#fail(new Error(reason));
4202
+ ctx.cohortTtlTimer = setTimeout(() => {
4203
+ const reason = `Cohort ${ctx.cohortId} exceeded TTL of ${this.#cohortTtlMs}ms`;
4204
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
4205
+ this.#failCohort(ctx, new Error(reason));
4053
4206
  }, this.#cohortTtlMs);
4054
4207
  }
4055
- this.#resetPhaseTimer();
4208
+ this.#resetPhaseTimer(ctx);
4056
4209
  }
4057
- /** Reset the per-phase stall timer. Called when a phase transition is observed. */
4058
- #resetPhaseTimer() {
4059
- if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
4060
- this.#phaseTimer = void 0;
4210
+ /** Reset a cohort's per-phase stall timer. Called when a phase transition is observed. */
4211
+ #resetPhaseTimer(ctx) {
4212
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
4213
+ ctx.phaseTimer = void 0;
4061
4214
  if (this.#phaseTimeoutMs === void 0) return;
4062
- this.#phaseTimer = setTimeout(() => {
4063
- const reason = `Cohort ${this.#cohortId ?? ""} stalled in phase ${this.#lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
4064
- this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
4065
- this.#fail(new Error(reason));
4215
+ ctx.phaseTimer = setTimeout(() => {
4216
+ const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
4217
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
4218
+ this.#failCohort(ctx, new Error(reason));
4066
4219
  }, this.#phaseTimeoutMs);
4067
4220
  }
4068
- /** Detect a phase change since the last observation and reset the phase timer. */
4069
- #onPhaseMaybeChanged() {
4070
- if (!this.#cohortId) return;
4071
- const phase = this.session.getCohortPhase(this.#cohortId);
4072
- if (phase !== this.#lastObservedPhase) {
4073
- this.#lastObservedPhase = phase;
4074
- this.#resetPhaseTimer();
4075
- }
4221
+ /** Detect a phase change for a cohort since the last observation and reset its phase timer. */
4222
+ #onPhaseMaybeChanged(ctx) {
4223
+ const phase = this.session.getCohortPhase(ctx.cohortId);
4224
+ if (phase !== ctx.lastObservedPhase) {
4225
+ ctx.lastObservedPhase = phase;
4226
+ this.#resetPhaseTimer(ctx);
4227
+ }
4228
+ }
4229
+ /** Clear a cohort's timers. Called on completion, stop, and failure. */
4230
+ #clearTimers(ctx) {
4231
+ if (ctx.cohortTtlTimer) clearTimeout(ctx.cohortTtlTimer);
4232
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
4233
+ ctx.cohortTtlTimer = void 0;
4234
+ ctx.phaseTimer = void 0;
4235
+ }
4236
+ /**
4237
+ * Reclaim one cohort's runner-layer bookkeeping: stop its advert loop, clear
4238
+ * its timers, and drop its {@link RunContext}. Does NOT touch sibling cohorts
4239
+ * and does NOT detach the shared transport handlers. Leaves the cohort in the
4240
+ * state machine; whether that cohort's `session` state is also removed is the
4241
+ * caller's choice (see {@link #completeCohort} vs {@link #failCohort}).
4242
+ */
4243
+ #disposeCohort(ctx) {
4244
+ this.#stopAdvertRepeating(ctx);
4245
+ this.#clearTimers(ctx);
4246
+ this.#contexts.delete(ctx.cohortId);
4247
+ }
4248
+ /**
4249
+ * Settle one cohort successfully. Reclaims the runner context but leaves the
4250
+ * completed cohort in `session` so callers can read its beaconAddress / cohort
4251
+ * via `session.getCohort(result.cohortId)`; reclaim it with
4252
+ * `session.removeCohort(cohortId)` when done. Idempotent via `ctx.settled`.
4253
+ */
4254
+ #completeCohort(ctx, result) {
4255
+ if (ctx.settled) return;
4256
+ ctx.settled = true;
4257
+ this.#disposeCohort(ctx);
4258
+ this.emit("signing-complete", result);
4259
+ ctx.resolve(result);
4260
+ }
4261
+ /**
4262
+ * Fail one cohort. Reclaims its runner context, drops its now-dead state from
4263
+ * the state machine, and rejects only its completion; siblings keep running
4264
+ * and the shared transport handlers stay registered. Idempotent via
4265
+ * `ctx.settled`.
4266
+ */
4267
+ #failCohort(ctx, err) {
4268
+ if (ctx.settled) return;
4269
+ ctx.settled = true;
4270
+ this.#disposeCohort(ctx);
4271
+ this.session.removeCohort(ctx.cohortId);
4272
+ this.emit("error", err);
4273
+ ctx.reject(err);
4076
4274
  }
4077
- /** Clear both timers. Called on successful completion, stop(), and #fail. */
4078
- #clearTimers() {
4079
- if (this.#cohortTtlTimer) clearTimeout(this.#cohortTtlTimer);
4080
- if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
4081
- this.#cohortTtlTimer = void 0;
4082
- this.#phaseTimer = void 0;
4275
+ /**
4276
+ * Stop a single cohort early without affecting the rest of the runner. Drops
4277
+ * the cohort's state machine state; its `completion` promise rejects with a
4278
+ * stopped error.
4279
+ */
4280
+ stopCohort(cohortId) {
4281
+ const ctx = this.#contexts.get(cohortId);
4282
+ if (!ctx || ctx.settled) return;
4283
+ ctx.settled = true;
4284
+ this.#disposeCohort(ctx);
4285
+ this.session.removeCohort(cohortId);
4286
+ ctx.reject(new AggregationServiceError(`Cohort ${cohortId} stopped.`, "COHORT_STOPPED", { cohortId }));
4083
4287
  }
4084
4288
  /**
4085
- * Stop the runner early. Marks the runner stopped and detaches transport
4086
- * handlers so a restart or a new runner doesn't inherit stale dispatch.
4289
+ * Stop the whole runner. Fails every outstanding cohort, then detaches the
4290
+ * shared transport handlers so a restart or a new runner doesn't inherit
4291
+ * stale dispatch. Safe to call repeatedly.
4087
4292
  */
4088
4293
  stop() {
4089
4294
  this.#stopped = true;
4090
- this.#stopAdvertRepeating();
4091
- this.#clearTimers();
4295
+ for (const ctx of [...this.#contexts.values()]) {
4296
+ if (ctx.settled) continue;
4297
+ ctx.settled = true;
4298
+ this.#disposeCohort(ctx);
4299
+ this.session.removeCohort(ctx.cohortId);
4300
+ ctx.reject(new AggregationServiceError("Service runner stopped.", "RUNNER_STOPPED", { cohortId: ctx.cohortId }));
4301
+ }
4302
+ this.#contexts.clear();
4092
4303
  this.#unregisterHandlers();
4093
- if (this.#cohortId) this.session.removeCohort(this.#cohortId);
4094
4304
  }
4095
4305
  /** Message types this runner listens for on the transport. */
4096
4306
  static #HANDLED_MESSAGE_TYPES = [
@@ -4101,7 +4311,10 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
4101
4311
  SIGNATURE_AUTHORIZATION
4102
4312
  ];
4103
4313
  /**
4104
- * Internal: handler registration with the transport. Idempotent.
4314
+ * Internal: handler registration with the transport. Idempotent. Handlers
4315
+ * are DID-scoped and cohort-agnostic — one registration serves every cohort
4316
+ * this runner drives; demux to the right {@link RunContext} happens in each
4317
+ * handler via the inbound message's cohortId.
4105
4318
  */
4106
4319
  #registerHandlers() {
4107
4320
  if (this.#handlersRegistered) return;
@@ -4122,23 +4335,25 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
4122
4335
  }
4123
4336
  /**
4124
4337
  * Internal: message handlers for each protocol step. Each handler:
4125
- * 1) feeds the message into the state machine via session.receive()
4126
- * 2) emits a high-level event for external observers
4127
- * 3) checks if the new state triggers any automatic next steps, and if so:
4338
+ * 1) resolves the cohort the message belongs to (by cohortId); ignores it if unknown
4339
+ * 2) feeds the message into the state machine via session.receive()
4340
+ * 3) emits a high-level event (carrying cohortId) for external observers
4341
+ * 4) checks if the new state triggers any automatic next steps, and if so:
4128
4342
  * a) calls the appropriate decision callback(s)
4129
4343
  * b) sends any resulting messages from the state machine
4344
+ * Errors fail only the owning cohort. A stopped runner ignores messages.
4130
4345
  * @param {BaseMessage} msg - The incoming message to handle.
4131
4346
  * @returns {Promise<void>} Resolves when handling is complete.
4132
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
4133
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
4134
4347
  */
4135
4348
  async #handleOptIn(msg) {
4136
4349
  if (this.#stopped) return;
4350
+ const ctx = this.#contextFor(msg);
4351
+ if (!ctx) return;
4137
4352
  try {
4138
4353
  this.session.receive(msg);
4139
- this.#drainRejections();
4140
- this.#onPhaseMaybeChanged();
4141
- const optIn = this.session.pendingOptIns(this.#cohortId).get(msg.from);
4354
+ this.#drainRejections(ctx);
4355
+ this.#onPhaseMaybeChanged(ctx);
4356
+ const optIn = this.session.pendingOptIns(ctx.cohortId).get(msg.from);
4142
4357
  if (!optIn) return;
4143
4358
  this.emit("opt-in-received", optIn);
4144
4359
  if (optIn.communicationPk) {
@@ -4146,29 +4361,34 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
4146
4361
  }
4147
4362
  const decision = await this.#onOptInReceived(optIn);
4148
4363
  if (!decision.accepted) return;
4149
- await this.#sendAll(this.session.acceptParticipant(this.#cohortId, msg.from));
4150
- this.emit("participant-accepted", { participantDid: msg.from });
4151
- const cohort = this.session.getCohort(this.#cohortId);
4152
- if (cohort.participants.length >= this.#config.minParticipants && !this.#finalizing) {
4153
- this.#finalizing = true;
4364
+ const maxParticipants = ctx.config.maxParticipants;
4365
+ const cohortNow = this.session.getCohort(ctx.cohortId);
4366
+ if (maxParticipants !== void 0 && cohortNow && cohortNow.participants.length >= maxParticipants) {
4367
+ return;
4368
+ }
4369
+ await this.#sendAll(this.session.acceptParticipant(ctx.cohortId, msg.from));
4370
+ this.emit("participant-accepted", { cohortId: ctx.cohortId, participantDid: msg.from });
4371
+ const cohort = this.session.getCohort(ctx.cohortId);
4372
+ if (cohort.participants.length >= ctx.config.minParticipants && !ctx.finalizing) {
4373
+ ctx.finalizing = true;
4154
4374
  const finalizeDecision = await this.#onReadyToFinalize({
4155
4375
  acceptedCount: cohort.participants.length,
4156
- minRequired: this.#config.minParticipants
4376
+ minRequired: ctx.config.minParticipants
4157
4377
  });
4158
4378
  if (!finalizeDecision.finalize) {
4159
- this.#finalizing = false;
4379
+ ctx.finalizing = false;
4160
4380
  return;
4161
4381
  }
4162
- const readyMsgs = this.session.finalizeKeygen(this.#cohortId);
4163
- this.#stopAdvertRepeating();
4382
+ const readyMsgs = this.session.finalizeKeygen(ctx.cohortId);
4383
+ this.#stopAdvertRepeating(ctx);
4164
4384
  this.emit("keygen-complete", {
4165
- cohortId: this.#cohortId,
4385
+ cohortId: ctx.cohortId,
4166
4386
  beaconAddress: cohort.beaconAddress
4167
4387
  });
4168
4388
  await this.#sendAll(readyMsgs);
4169
4389
  }
4170
4390
  } catch (err) {
4171
- this.#fail(err);
4391
+ this.#failCohort(ctx, err);
4172
4392
  }
4173
4393
  }
4174
4394
  /**
@@ -4176,23 +4396,23 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
4176
4396
  * and distributes the data for validation.
4177
4397
  * @param {BaseMessage} msg - The incoming message to handle.
4178
4398
  * @returns {Promise<void>} Resolves when handling is complete.
4179
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
4180
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
4181
4399
  */
4182
4400
  async #handleSubmitUpdate(msg) {
4183
4401
  if (this.#stopped) return;
4402
+ const ctx = this.#contextFor(msg);
4403
+ if (!ctx) return;
4184
4404
  try {
4185
4405
  this.session.receive(msg);
4186
- this.#drainRejections();
4187
- this.#onPhaseMaybeChanged();
4188
- this.emit("update-received", { participantDid: msg.from });
4189
- if (this.session.getCohortPhase(this.#cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
4190
- const distributeMsgs = this.session.buildAndDistribute(this.#cohortId);
4191
- this.emit("data-distributed", { cohortId: this.#cohortId });
4406
+ this.#drainRejections(ctx);
4407
+ this.#onPhaseMaybeChanged(ctx);
4408
+ this.emit("update-received", { cohortId: ctx.cohortId, participantDid: msg.from });
4409
+ if (this.session.getCohortPhase(ctx.cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
4410
+ const distributeMsgs = this.session.buildAndDistribute(ctx.cohortId);
4411
+ this.emit("data-distributed", { cohortId: ctx.cohortId });
4192
4412
  await this.#sendAll(distributeMsgs);
4193
4413
  }
4194
4414
  } catch (err) {
4195
- this.#fail(err);
4415
+ this.#failCohort(ctx, err);
4196
4416
  }
4197
4417
  }
4198
4418
  /**
@@ -4200,111 +4420,95 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
4200
4420
  * automatically requests tx data and starts signing.
4201
4421
  * @param {BaseMessage} msg - The incoming message to handle.
4202
4422
  * @returns {Promise<void>} Resolves when handling is complete.
4203
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
4204
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
4205
4423
  */
4206
4424
  async #handleValidationAck(msg) {
4207
4425
  if (this.#stopped) return;
4426
+ const ctx = this.#contextFor(msg);
4427
+ if (!ctx) return;
4208
4428
  try {
4209
4429
  this.session.receive(msg);
4210
- this.#drainRejections();
4211
- this.#onPhaseMaybeChanged();
4430
+ this.#drainRejections(ctx);
4431
+ this.#onPhaseMaybeChanged(ctx);
4212
4432
  const approved = !!msg.body?.approved;
4213
- this.emit("validation-received", { participantDid: msg.from, approved });
4214
- const phase = this.session.getCohortPhase(this.#cohortId);
4433
+ this.emit("validation-received", { cohortId: ctx.cohortId, participantDid: msg.from, approved });
4434
+ const phase = this.session.getCohortPhase(ctx.cohortId);
4215
4435
  if (phase === "Failed" /* Failed */) {
4216
4436
  const reason = `Validation rejected by participant ${msg.from}`;
4217
- this.emit("cohort-failed", { cohortId: this.#cohortId, reason });
4218
- this.#fail(new Error(reason));
4437
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
4438
+ this.#failCohort(ctx, new Error(reason));
4219
4439
  return;
4220
4440
  }
4221
4441
  if (phase === "Validated" /* Validated */) {
4222
- const cohort = this.session.getCohort(this.#cohortId);
4442
+ const cohort = this.session.getCohort(ctx.cohortId);
4223
4443
  const txData = await this.#onProvideTxData({
4224
- cohortId: this.#cohortId,
4444
+ cohortId: ctx.cohortId,
4225
4445
  beaconAddress: cohort.beaconAddress,
4226
4446
  signalBytes: cohort.signalBytes
4227
4447
  });
4228
- const authMsgs = this.session.startSigning(this.#cohortId, txData);
4229
- const sessionId = this.session.getSigningSessionId(this.#cohortId) ?? "";
4230
- this.emit("signing-started", { sessionId });
4448
+ const authMsgs = this.session.startSigning(ctx.cohortId, txData);
4449
+ const sessionId = this.session.getSigningSessionId(ctx.cohortId) ?? "";
4450
+ this.emit("signing-started", { cohortId: ctx.cohortId, sessionId });
4231
4451
  await this.#sendAll(authMsgs);
4232
4452
  }
4233
4453
  } catch (err) {
4234
- this.#fail(err);
4454
+ this.#failCohort(ctx, err);
4235
4455
  }
4236
4456
  }
4237
4457
  /**
4238
- * Handler for receiving nonce contributions and signature authorizations. When all nonces or
4239
- * signatures are received,
4458
+ * Handler for receiving nonce contributions. When all nonces are received, sends the aggregated
4459
+ * nonce back to the cohort.
4240
4460
  * @param {BaseMessage} msg - The incoming message to handle.
4241
4461
  * @returns {Promise<void>} Resolves when handling is complete.
4242
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
4243
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
4244
4462
  */
4245
4463
  async #handleNonceContribution(msg) {
4246
4464
  if (this.#stopped) return;
4465
+ const ctx = this.#contextFor(msg);
4466
+ if (!ctx) return;
4247
4467
  try {
4248
4468
  this.session.receive(msg);
4249
- this.#drainRejections();
4250
- this.#onPhaseMaybeChanged();
4251
- this.emit("nonce-received", { participantDid: msg.from });
4252
- if (this.session.getCohortPhase(this.#cohortId) === "NoncesCollected" /* NoncesCollected */) {
4253
- await this.#sendAll(this.session.sendAggregatedNonce(this.#cohortId));
4469
+ this.#drainRejections(ctx);
4470
+ this.#onPhaseMaybeChanged(ctx);
4471
+ this.emit("nonce-received", { cohortId: ctx.cohortId, participantDid: msg.from });
4472
+ if (this.session.getCohortPhase(ctx.cohortId) === "NoncesCollected" /* NoncesCollected */) {
4473
+ await this.#sendAll(this.session.sendAggregatedNonce(ctx.cohortId));
4254
4474
  }
4255
4475
  } catch (err) {
4256
- this.#fail(err);
4476
+ this.#failCohort(ctx, err);
4257
4477
  }
4258
4478
  }
4259
4479
  /**
4260
4480
  * Handler for receiving signature authorizations. When all partial signatures are received, the
4261
- * session automatically completes and the final result is emitted and the run() promise is resolved.
4481
+ * session automatically completes; the final result is emitted and the cohort's completion
4482
+ * promise resolves.
4262
4483
  * @param {BaseMessage} msg - The incoming message to handle.
4263
4484
  * @returns {Promise<void>} Resolves when handling is complete.
4264
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
4265
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
4266
4485
  */
4267
4486
  async #handleSignatureAuthorization(msg) {
4268
4487
  if (this.#stopped) return;
4488
+ const ctx = this.#contextFor(msg);
4489
+ if (!ctx) return;
4269
4490
  try {
4270
4491
  this.session.receive(msg);
4271
- this.#drainRejections();
4272
- this.#onPhaseMaybeChanged();
4273
- const result = this.session.getResult(this.#cohortId);
4492
+ this.#drainRejections(ctx);
4493
+ this.#onPhaseMaybeChanged(ctx);
4494
+ const result = this.session.getResult(ctx.cohortId);
4274
4495
  if (result) {
4275
- this.#clearTimers();
4276
- this.#unregisterHandlers();
4277
- this.emit("signing-complete", result);
4278
- this.#resolveRun?.(result);
4496
+ this.#completeCohort(ctx, result);
4279
4497
  }
4280
4498
  } catch (err) {
4281
- this.#fail(err);
4499
+ this.#failCohort(ctx, err);
4282
4500
  }
4283
4501
  }
4284
4502
  /**
4285
4503
  * Internal: helper to send all messages sequentially. Catches and propagates errors.
4286
4504
  * @param {BaseMessage[]} msgs - The messages to send.
4287
4505
  * @returns {Promise<void>} Resolves when all messages have been sent.
4288
- * @throws {Error} If sending any message fails, the error is emitted and the run promise is
4289
- * rejected.
4290
4506
  */
4291
4507
  async #sendAll(msgs) {
4292
4508
  for (const m of msgs) {
4293
4509
  await this.#transport.sendMessage(m, this.#did, m.to);
4294
4510
  }
4295
4511
  }
4296
- /**
4297
- * Internal: helper to handle errors. Emits an 'error' event and rejects the run promise.
4298
- * @param {Error} err - The error to handle.
4299
- */
4300
- #fail(err) {
4301
- this.#stopAdvertRepeating();
4302
- this.#clearTimers();
4303
- this.#unregisterHandlers();
4304
- if (this.#cohortId) this.session.removeCohort(this.#cohortId);
4305
- this.emit("error", err);
4306
- this.#rejectRun?.(err);
4307
- }
4308
4512
  };
4309
4513
 
4310
4514
  // src/core/aggregation/runner/participant-runner.ts
@@ -4364,7 +4568,8 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
4364
4568
  }
4365
4569
  /**
4366
4570
  * Single-shot helper: start, join the first cohort that passes `shouldJoin`,
4367
- * drive it to completion, and resolve. Convenient for tests and demos.
4571
+ * drive it to completion, and resolve. Convenient for tests and demos. The
4572
+ * single-cohort special case of {@link joinMatching} (count = 1).
4368
4573
  */
4369
4574
  static async joinFirst(options) {
4370
4575
  return new Promise((resolve, reject) => {
@@ -4377,6 +4582,37 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
4377
4582
  runner.start().catch(reject);
4378
4583
  });
4379
4584
  }
4585
+ /**
4586
+ * Multi-cohort helper: start, join EVERY cohort whose advert passes
4587
+ * `shouldJoin`, drive each to completion in parallel, and resolve once
4588
+ * `count` cohorts have completed (the runner stops at that point). The
4589
+ * N-cohort generalization of {@link joinFirst}, for a participant that joins
4590
+ * several cohorts advertised by one service.
4591
+ *
4592
+ * For an open-ended, long-lived subscriber (no fixed count), construct an
4593
+ * {@link AggregationParticipantRunner} directly, set `shouldJoin`, call
4594
+ * `start()`, and listen for `cohort-complete` — the runner already drives
4595
+ * any number of cohorts concurrently.
4596
+ *
4597
+ * @param options Participant runner options (set `shouldJoin` to select cohorts).
4598
+ * @param count Number of completed cohorts to collect before resolving.
4599
+ * @returns The {@link CohortCompleteInfo} for each completed cohort, in completion order.
4600
+ */
4601
+ static async joinMatching(options, count) {
4602
+ return new Promise((resolve, reject) => {
4603
+ const runner = new _AggregationParticipantRunner(options);
4604
+ const completed = [];
4605
+ runner.on("cohort-complete", (info) => {
4606
+ completed.push(info);
4607
+ if (completed.length >= count) {
4608
+ runner.stop();
4609
+ resolve(completed);
4610
+ }
4611
+ });
4612
+ runner.on("error", reject);
4613
+ runner.start().catch(reject);
4614
+ });
4615
+ }
4380
4616
  /**
4381
4617
  * Internal: handler registration with the transport. Idempotent and safe to call multiple times,
4382
4618
  * but only registers handlers once.
@@ -4525,7 +4761,7 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
4525
4761
  if (this.session.getCohortPhase(cohortId) === "Complete" /* Complete */) {
4526
4762
  const info = this.session.joinedCohorts.get(cohortId);
4527
4763
  if (info) {
4528
- const validation = this.session.pendingValidations.get(cohortId);
4764
+ const validation = this.session.getValidation(cohortId);
4529
4765
  this.emit("cohort-complete", {
4530
4766
  cohortId,
4531
4767
  beaconAddress: info.beaconAddress,
@@ -7826,6 +8062,7 @@ var DidDocumentBuilder = class {
7826
8062
  InMemoryRateLimitStore,
7827
8063
  InMemoryTransport,
7828
8064
  InboxBuffer,
8065
+ KNOWN_BEACON_TYPES,
7829
8066
  KeyPairAggregationSigner,
7830
8067
  NONCE_CONTRIBUTION,
7831
8068
  NonceCache,
@@ -7898,6 +8135,7 @@ var DidDocumentBuilder = class {
7898
8135
  registerBeaconStrategy,
7899
8136
  reviveFromWire,
7900
8137
  signEnvelope,
8138
+ validateCohortConditions,
7901
8139
  verifyEnvelope,
7902
8140
  verifyRequestAuth
7903
8141
  });