@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/browser.js CHANGED
@@ -93203,6 +93203,7 @@ a=end-of-candidates
93203
93203
  InMemoryRateLimitStore: () => InMemoryRateLimitStore,
93204
93204
  InMemoryTransport: () => InMemoryTransport,
93205
93205
  InboxBuffer: () => InboxBuffer,
93206
+ KNOWN_BEACON_TYPES: () => KNOWN_BEACON_TYPES,
93206
93207
  KeyPairAggregationSigner: () => KeyPairAggregationSigner,
93207
93208
  NONCE_CONTRIBUTION: () => NONCE_CONTRIBUTION,
93208
93209
  NonceCache: () => NonceCache,
@@ -93275,6 +93276,7 @@ a=end-of-candidates
93275
93276
  registerBeaconStrategy: () => registerBeaconStrategy,
93276
93277
  reviveFromWire: () => reviveFromWire,
93277
93278
  signEnvelope: () => signEnvelope,
93279
+ validateCohortConditions: () => validateCohortConditions,
93278
93280
  verifyEnvelope: () => verifyEnvelope,
93279
93281
  verifyRequestAuth: () => verifyRequestAuth
93280
93282
  });
@@ -103381,7 +103383,7 @@ a=end-of-candidates
103381
103383
  validationRejections = /* @__PURE__ */ new Set();
103382
103384
  constructor({ id, minParticipants, serviceDid, network, beaconType }) {
103383
103385
  this.id = id || crypto.randomUUID();
103384
- this.minParticipants = minParticipants || 2;
103386
+ this.minParticipants = minParticipants ?? 2;
103385
103387
  this.serviceDid = serviceDid || "";
103386
103388
  this.network = network;
103387
103389
  this.beaconType = beaconType || "CASBeacon";
@@ -103547,6 +103549,57 @@ a=end-of-candidates
103547
103549
  }
103548
103550
  };
103549
103551
 
103552
+ // src/core/aggregation/conditions.ts
103553
+ init_shim();
103554
+ var KNOWN_BEACON_TYPES = ["CASBeacon", "SMTBeacon"];
103555
+ function checkPair(problems, label, min, max) {
103556
+ if (min !== void 0 && (!Number.isInteger(min) || min < 0)) {
103557
+ problems.push(`min${label} must be an integer >= 0`);
103558
+ }
103559
+ if (max !== void 0 && (!Number.isInteger(max) || max < 0)) {
103560
+ problems.push(`max${label} must be an integer >= 0`);
103561
+ }
103562
+ if (min !== void 0 && max !== void 0 && Number.isInteger(min) && Number.isInteger(max) && max < min) {
103563
+ problems.push(`max${label} must be >= min${label}`);
103564
+ }
103565
+ }
103566
+ function checkCost(problems, label, cost) {
103567
+ if (cost === void 0) return;
103568
+ if (typeof cost.amount !== "number" || !Number.isFinite(cost.amount) || cost.amount < 0) {
103569
+ problems.push(`${label}.amount must be a finite number >= 0`);
103570
+ }
103571
+ if (typeof cost.unit !== "string" || cost.unit.length === 0) {
103572
+ problems.push(`${label}.unit must be a non-empty string`);
103573
+ }
103574
+ if (cost.basis !== void 0 && cost.basis !== "per-did" && cost.basis !== "per-participant") {
103575
+ problems.push(`${label}.basis must be 'per-did' or 'per-participant'`);
103576
+ }
103577
+ }
103578
+ function validateCohortConditions(c2) {
103579
+ const problems = [];
103580
+ if (!KNOWN_BEACON_TYPES.includes(c2.beaconType)) {
103581
+ problems.push(`beaconType must be one of ${KNOWN_BEACON_TYPES.join(", ")}`);
103582
+ }
103583
+ if (!Number.isInteger(c2.minParticipants) || c2.minParticipants < 1) {
103584
+ problems.push("minParticipants must be an integer >= 1");
103585
+ }
103586
+ if (c2.maxParticipants !== void 0) {
103587
+ if (!Number.isInteger(c2.maxParticipants) || c2.maxParticipants < 1) {
103588
+ problems.push("maxParticipants must be an integer >= 1");
103589
+ } else if (Number.isInteger(c2.minParticipants) && c2.maxParticipants < c2.minParticipants) {
103590
+ problems.push("maxParticipants must be >= minParticipants");
103591
+ }
103592
+ }
103593
+ checkPair(problems, "DidsPerParticipant", c2.minDidsPerParticipant, c2.maxDidsPerParticipant);
103594
+ checkPair(problems, "SecondsBetweenAnnouncements", c2.minSecondsBetweenAnnouncements, c2.maxSecondsBetweenAnnouncements);
103595
+ if (c2.pendingUpdateTrigger !== void 0 && (!Number.isInteger(c2.pendingUpdateTrigger) || c2.pendingUpdateTrigger < 1)) {
103596
+ problems.push("pendingUpdateTrigger must be an integer >= 1");
103597
+ }
103598
+ checkCost(problems, "costOfEnrollment", c2.costOfEnrollment);
103599
+ checkCost(problems, "costPerAnnouncement", c2.costPerAnnouncement);
103600
+ return problems;
103601
+ }
103602
+
103550
103603
  // src/core/aggregation/messages/base.ts
103551
103604
  init_shim();
103552
103605
  var AGGREGATION_WIRE_VERSION = 1;
@@ -103981,6 +104034,14 @@ a=end-of-candidates
103981
104034
  * Cohort starts in `Created` phase — call `advertise()` to broadcast.
103982
104035
  */
103983
104036
  createCohort(config) {
104037
+ const problems = validateCohortConditions(config);
104038
+ if (problems.length > 0) {
104039
+ throw new AggregationServiceError(
104040
+ `Invalid cohort conditions: ${problems.join("; ")}`,
104041
+ "INVALID_COHORT_CONDITIONS",
104042
+ { problems }
104043
+ );
104044
+ }
103984
104045
  const cohort = new AggregationCohort({
103985
104046
  serviceDid: this.did,
103986
104047
  minParticipants: config.minParticipants,
@@ -104013,13 +104074,13 @@ a=end-of-candidates
104013
104074
  { cohortId, phase: state.phase }
104014
104075
  );
104015
104076
  }
104077
+ const { network, ...conditions } = state.config;
104016
104078
  const message2 = createCohortAdvertMessage({
104017
104079
  from: this.did,
104018
104080
  cohortId,
104019
- cohortSize: state.config.minParticipants,
104020
- beaconType: state.config.beaconType,
104021
- network: state.config.network,
104022
- communicationPk: this.publicKey.compressed
104081
+ network,
104082
+ communicationPk: this.publicKey.compressed,
104083
+ ...conditions
104023
104084
  });
104024
104085
  state.phase = "Advertised" /* Advertised */;
104025
104086
  return [message2];
@@ -104078,6 +104139,14 @@ a=end-of-candidates
104078
104139
  { cohortId, participantDid }
104079
104140
  );
104080
104141
  }
104142
+ const maxParticipants = state.config.maxParticipants;
104143
+ if (maxParticipants !== void 0 && state.acceptedParticipants.size >= maxParticipants) {
104144
+ throw new AggregationServiceError(
104145
+ `Cohort ${cohortId} is full: ${maxParticipants} participants already accepted.`,
104146
+ "COHORT_FULL",
104147
+ { cohortId, maxParticipants }
104148
+ );
104149
+ }
104081
104150
  state.acceptedParticipants.add(participantDid);
104082
104151
  state.cohort.participants.push(participantDid);
104083
104152
  state.cohort.participantKeys.set(participantDid, optIn.participantPk);
@@ -104111,6 +104180,14 @@ a=end-of-candidates
104111
104180
  { cohortId }
104112
104181
  );
104113
104182
  }
104183
+ const maxParticipants = state.config.maxParticipants;
104184
+ if (maxParticipants !== void 0 && state.acceptedParticipants.size > maxParticipants) {
104185
+ throw new AggregationServiceError(
104186
+ `Cohort ${cohortId} has ${state.acceptedParticipants.size} accepted participants, exceeds max ${maxParticipants}.`,
104187
+ "TOO_MANY_PARTICIPANTS",
104188
+ { cohortId, maxParticipants }
104189
+ );
104190
+ }
104114
104191
  const beaconAddress = state.cohort.computeBeaconAddress();
104115
104192
  state.phase = "CohortSet" /* CohortSet */;
104116
104193
  const messages2 = [];
@@ -104428,6 +104505,59 @@ a=end-of-candidates
104428
104505
  // src/core/aggregation/participant.ts
104429
104506
  init_shim();
104430
104507
  init_utils();
104508
+
104509
+ // src/core/aggregation/messages/bodies.ts
104510
+ init_shim();
104511
+ var hasStr = (b, k) => !!b && typeof b[k] === "string";
104512
+ var hasIntMin = (b, k, min) => {
104513
+ const v = b ? b[k] : void 0;
104514
+ return typeof v === "number" && Number.isInteger(v) && v >= min;
104515
+ };
104516
+ var optIntMin = (b, k, min) => {
104517
+ const v = b ? b[k] : void 0;
104518
+ return v === void 0 || typeof v === "number" && Number.isInteger(v) && v >= min;
104519
+ };
104520
+ var hasBool = (b, k) => !!b && typeof b[k] === "boolean";
104521
+ var hasBytes = (b, k) => !!b && b[k] instanceof Uint8Array;
104522
+ var hasBytesArray = (b, k) => {
104523
+ const v = b ? b[k] : void 0;
104524
+ return Array.isArray(v) && v.every((x) => x instanceof Uint8Array);
104525
+ };
104526
+ function isCohortAdvertMessage(m2) {
104527
+ return m2.type === COHORT_ADVERT && hasStr(m2.body, "cohortId") && hasIntMin(m2.body, "minParticipants", 1) && optIntMin(m2.body, "maxParticipants", 1) && hasStr(m2.body, "beaconType") && hasStr(m2.body, "network") && hasBytes(m2.body, "communicationPk");
104528
+ }
104529
+ function isCohortOptInMessage(m2) {
104530
+ return m2.type === COHORT_OPT_IN && hasStr(m2.body, "cohortId") && hasBytes(m2.body, "participantPk") && hasBytes(m2.body, "communicationPk");
104531
+ }
104532
+ function isCohortOptInAcceptMessage(m2) {
104533
+ return m2.type === COHORT_OPT_IN_ACCEPT && hasStr(m2.body, "cohortId");
104534
+ }
104535
+ function isCohortReadyMessage(m2) {
104536
+ return m2.type === COHORT_READY && hasStr(m2.body, "cohortId") && hasStr(m2.body, "beaconAddress") && hasBytesArray(m2.body, "cohortKeys");
104537
+ }
104538
+ function isSubmitUpdateMessage(m2) {
104539
+ return m2.type === SUBMIT_UPDATE && hasStr(m2.body, "cohortId") && !!m2.body && typeof m2.body.signedUpdate === "object";
104540
+ }
104541
+ function isDistributeAggregatedDataMessage(m2) {
104542
+ return m2.type === DISTRIBUTE_AGGREGATED_DATA && hasStr(m2.body, "cohortId") && hasStr(m2.body, "beaconType") && hasStr(m2.body, "signalBytesHex");
104543
+ }
104544
+ function isValidationAckMessage(m2) {
104545
+ return m2.type === VALIDATION_ACK && hasStr(m2.body, "cohortId") && hasBool(m2.body, "approved");
104546
+ }
104547
+ function isAuthorizationRequestMessage(m2) {
104548
+ return m2.type === AUTHORIZATION_REQUEST && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasStr(m2.body, "pendingTx") && hasStr(m2.body, "prevOutScriptHex") && hasStr(m2.body, "prevOutValue");
104549
+ }
104550
+ function isNonceContributionMessage(m2) {
104551
+ return m2.type === NONCE_CONTRIBUTION && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "nonceContribution");
104552
+ }
104553
+ function isAggregatedNonceMessage(m2) {
104554
+ return m2.type === AGGREGATED_NONCE && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "aggregatedNonce");
104555
+ }
104556
+ function isSignatureAuthorizationMessage(m2) {
104557
+ return m2.type === SIGNATURE_AUTHORIZATION && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "partialSignature");
104558
+ }
104559
+
104560
+ // src/core/aggregation/participant.ts
104431
104561
  var AggregationParticipant = class {
104432
104562
  did;
104433
104563
  /** MuSig2 signing capability. The raw secret never lives as a field here. */
@@ -104485,16 +104615,15 @@ a=end-of-candidates
104485
104615
  return map3;
104486
104616
  }
104487
104617
  #handleCohortAdvert(message2) {
104488
- const cohortId = message2.body?.cohortId;
104489
- if (!cohortId) return;
104618
+ if (!isCohortAdvertMessage(message2)) return;
104619
+ const { cohortId, network, communicationPk, ...conditions } = message2.body;
104490
104620
  if (this.#cohortStates.has(cohortId)) return;
104491
104621
  const advert = {
104492
104622
  cohortId,
104493
104623
  serviceDid: message2.from,
104494
- cohortSize: message2.body?.cohortSize ?? 0,
104495
- network: message2.body?.network ?? "",
104496
- beaconType: message2.body?.beaconType ?? "CASBeacon",
104497
- serviceCommunicationPk: message2.body?.communicationPk ?? new Uint8Array()
104624
+ network,
104625
+ serviceCommunicationPk: communicationPk,
104626
+ ...conditions
104498
104627
  };
104499
104628
  this.#cohortStates.set(cohortId, {
104500
104629
  phase: "Discovered" /* Discovered */,
@@ -104519,7 +104648,7 @@ a=end-of-candidates
104519
104648
  const cohort = new AggregationCohort({
104520
104649
  id: cohortId,
104521
104650
  serviceDid: state.serviceDid,
104522
- minParticipants: state.advert.cohortSize,
104651
+ minParticipants: state.advert.minParticipants,
104523
104652
  network: state.advert.network,
104524
104653
  beaconType: state.advert.beaconType
104525
104654
  });
@@ -104598,6 +104727,17 @@ a=end-of-candidates
104598
104727
  }
104599
104728
  return map3;
104600
104729
  }
104730
+ /**
104731
+ * The validated aggregated data retained for a cohort, regardless of phase.
104732
+ * Unlike {@link pendingValidations} (which lists only cohorts still awaiting
104733
+ * the validate decision), this returns the stored validation — including the
104734
+ * participant's sidecar (the CAS Announcement map or its SMT inclusion proof)
104735
+ * — so it is still readable once the cohort reaches Complete. Returns
104736
+ * undefined before aggregated data has been received.
104737
+ */
104738
+ getValidation(cohortId) {
104739
+ return this.#cohortStates.get(cohortId)?.validation;
104740
+ }
104601
104741
  #handleDistributeAggregatedData(message2) {
104602
104742
  const cohortId = message2.body?.cohortId;
104603
104743
  if (!cohortId) return;
@@ -104842,50 +104982,6 @@ a=end-of-candidates
104842
104982
  // src/core/aggregation/messages/index.ts
104843
104983
  init_shim();
104844
104984
 
104845
- // src/core/aggregation/messages/bodies.ts
104846
- init_shim();
104847
- var hasStr = (b, k) => !!b && typeof b[k] === "string";
104848
- var hasNum = (b, k) => !!b && typeof b[k] === "number";
104849
- var hasBool = (b, k) => !!b && typeof b[k] === "boolean";
104850
- var hasBytes = (b, k) => !!b && b[k] instanceof Uint8Array;
104851
- var hasBytesArray = (b, k) => {
104852
- const v = b ? b[k] : void 0;
104853
- return Array.isArray(v) && v.every((x) => x instanceof Uint8Array);
104854
- };
104855
- function isCohortAdvertMessage(m2) {
104856
- return m2.type === COHORT_ADVERT && hasStr(m2.body, "cohortId") && hasNum(m2.body, "cohortSize") && hasStr(m2.body, "beaconType") && hasStr(m2.body, "network") && hasBytes(m2.body, "communicationPk");
104857
- }
104858
- function isCohortOptInMessage(m2) {
104859
- return m2.type === COHORT_OPT_IN && hasStr(m2.body, "cohortId") && hasBytes(m2.body, "participantPk") && hasBytes(m2.body, "communicationPk");
104860
- }
104861
- function isCohortOptInAcceptMessage(m2) {
104862
- return m2.type === COHORT_OPT_IN_ACCEPT && hasStr(m2.body, "cohortId");
104863
- }
104864
- function isCohortReadyMessage(m2) {
104865
- return m2.type === COHORT_READY && hasStr(m2.body, "cohortId") && hasStr(m2.body, "beaconAddress") && hasBytesArray(m2.body, "cohortKeys");
104866
- }
104867
- function isSubmitUpdateMessage(m2) {
104868
- return m2.type === SUBMIT_UPDATE && hasStr(m2.body, "cohortId") && !!m2.body && typeof m2.body.signedUpdate === "object";
104869
- }
104870
- function isDistributeAggregatedDataMessage(m2) {
104871
- return m2.type === DISTRIBUTE_AGGREGATED_DATA && hasStr(m2.body, "cohortId") && hasStr(m2.body, "beaconType") && hasStr(m2.body, "signalBytesHex");
104872
- }
104873
- function isValidationAckMessage(m2) {
104874
- return m2.type === VALIDATION_ACK && hasStr(m2.body, "cohortId") && hasBool(m2.body, "approved");
104875
- }
104876
- function isAuthorizationRequestMessage(m2) {
104877
- return m2.type === AUTHORIZATION_REQUEST && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasStr(m2.body, "pendingTx") && hasStr(m2.body, "prevOutScriptHex") && hasStr(m2.body, "prevOutValue");
104878
- }
104879
- function isNonceContributionMessage(m2) {
104880
- return m2.type === NONCE_CONTRIBUTION && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "nonceContribution");
104881
- }
104882
- function isAggregatedNonceMessage(m2) {
104883
- return m2.type === AGGREGATED_NONCE && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "aggregatedNonce");
104884
- }
104885
- function isSignatureAuthorizationMessage(m2) {
104886
- return m2.type === SIGNATURE_AUTHORIZATION && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "partialSignature");
104887
- }
104888
-
104889
104985
  // src/core/aggregation/messages/guards.ts
104890
104986
  init_shim();
104891
104987
  var KEYGEN_VALUES = /* @__PURE__ */ new Set([
@@ -111518,35 +111614,22 @@ ${value2}`;
111518
111614
  session;
111519
111615
  #transport;
111520
111616
  #did;
111521
- #config;
111617
+ #defaultConfig;
111522
111618
  #onOptInReceived;
111523
111619
  #onReadyToFinalize;
111524
111620
  #onProvideTxData;
111525
111621
  #cohortTtlMs;
111526
111622
  #phaseTimeoutMs;
111527
111623
  #advertRepeatIntervalMs;
111528
- #cohortId;
111624
+ /** Per-cohort run state, keyed by cohortId. */
111625
+ #contexts = /* @__PURE__ */ new Map();
111529
111626
  #handlersRegistered = false;
111530
111627
  #stopped = false;
111531
- /**
111532
- * Guard against the async race where two concurrent #handleOptIn invocations
111533
- * both pass the `participants.length >= minParticipants` check before either
111534
- * mutates the cohort phase. Set synchronously before any `await` so subsequent
111535
- * handlers observe it on their next resumption.
111536
- */
111537
- #finalizing = false;
111538
- #resolveRun;
111539
- #rejectRun;
111540
- #cohortTtlTimer;
111541
- #phaseTimer;
111542
- #lastObservedPhase;
111543
- /** Stop handle for the repeating COHORT_ADVERT publish loop. */
111544
- #stopAdvertRepeat;
111545
111628
  constructor(options2) {
111546
111629
  super();
111547
111630
  this.#transport = options2.transport;
111548
111631
  this.#did = options2.did;
111549
- this.#config = options2.config;
111632
+ this.#defaultConfig = options2.config;
111550
111633
  this.#onOptInReceived = options2.onOptInReceived ?? (async () => ({ accepted: true }));
111551
111634
  this.#onReadyToFinalize = options2.onReadyToFinalize ?? (async ({ acceptedCount, minRequired }) => ({
111552
111635
  finalize: acceptedCount >= minRequired
@@ -111564,55 +111647,126 @@ ${value2}`;
111564
111647
  maxUpdateSizeBytes: options2.maxUpdateSizeBytes
111565
111648
  });
111566
111649
  }
111650
+ /** Resolve the {@link RunContext} an inbound message belongs to, by cohortId. */
111651
+ #contextFor(msg) {
111652
+ const cohortId = msg.body?.cohortId;
111653
+ if (!cohortId) return void 0;
111654
+ return this.#contexts.get(cohortId);
111655
+ }
111567
111656
  /**
111568
- * Drain any silent rejections the state machine recorded during the most
111569
- * recent receive() and surface them as `message-rejected` events. Safe to
111570
- * call even before a cohortId is assigned.
111657
+ * Drain any silent rejections the state machine recorded for a cohort during
111658
+ * the most recent receive() and surface them as `message-rejected` events.
111571
111659
  */
111572
- #drainRejections() {
111573
- if (!this.#cohortId) return;
111574
- for (const r2 of this.session.drainRejections(this.#cohortId)) {
111575
- this.emit("message-rejected", { cohortId: this.#cohortId, ...r2 });
111660
+ #drainRejections(ctx) {
111661
+ for (const r2 of this.session.drainRejections(ctx.cohortId)) {
111662
+ this.emit("message-rejected", { cohortId: ctx.cohortId, ...r2 });
111576
111663
  }
111577
111664
  }
111578
111665
  /**
111579
- * Run the protocol to completion. Resolves with the final aggregation result
111580
- * (signature + signed transaction) once signing is complete.
111666
+ * Advertise a new cohort and begin driving it to completion. Callable many
111667
+ * times on one runner; each cohort runs concurrently and independently.
111668
+ *
111669
+ * @param config Per-cohort conditions + network (see {@link CohortConfig}).
111670
+ * @returns The new cohort's id and a `completion` promise that resolves with
111671
+ * that cohort's {@link AggregationResult} (or rejects if it fails/stalls).
111672
+ * @throws If the runner has been stopped, or the config is invalid
111673
+ * (fail-fast via `createCohort`).
111674
+ */
111675
+ advertiseCohort(config) {
111676
+ if (this.#stopped) {
111677
+ throw new AggregationServiceError("Cannot advertise on a stopped runner.", "RUNNER_STOPPED", {});
111678
+ }
111679
+ this.#registerHandlers();
111680
+ const cohortId = this.session.createCohort(config);
111681
+ let resolve;
111682
+ let reject;
111683
+ const completion = new Promise((res, rej) => {
111684
+ resolve = res;
111685
+ reject = rej;
111686
+ });
111687
+ const ctx = {
111688
+ cohortId,
111689
+ config,
111690
+ resolve,
111691
+ reject,
111692
+ completion,
111693
+ finalizing: false,
111694
+ settled: false
111695
+ };
111696
+ this.#contexts.set(cohortId, ctx);
111697
+ try {
111698
+ this.#startTimers(ctx);
111699
+ const advertMsgs = this.session.advertise(cohortId);
111700
+ this.#onPhaseMaybeChanged(ctx);
111701
+ this.emit("cohort-advertised", { cohortId });
111702
+ if (this.#advertRepeatIntervalMs > 0) {
111703
+ this.#startAdvertRepeat(ctx, advertMsgs);
111704
+ } else {
111705
+ this.#sendAll(advertMsgs).catch((err) => this.#failCohort(ctx, err));
111706
+ }
111707
+ } catch (err) {
111708
+ this.#failCohort(ctx, err);
111709
+ }
111710
+ return { cohortId, completion };
111711
+ }
111712
+ /**
111713
+ * Run a single cohort to completion using the `config` supplied in the
111714
+ * runner options. Thin convenience over {@link advertiseCohort} for the
111715
+ * single-cohort case (and the path {@link AggregationRunner.solo} rides).
111581
111716
  *
111582
111717
  * @returns {Promise<AggregationResult>} The final result with signature and signed tx.
111583
111718
  */
111584
111719
  run() {
111585
- return new Promise((resolve, reject) => {
111586
- this.#resolveRun = resolve;
111587
- this.#rejectRun = reject;
111588
- try {
111589
- this.#registerHandlers();
111590
- this.#cohortId = this.session.createCohort(this.#config);
111591
- this.#startTimers();
111592
- const advertMsgs = this.session.advertise(this.#cohortId);
111593
- this.#onPhaseMaybeChanged();
111594
- this.emit("cohort-advertised", { cohortId: this.#cohortId });
111595
- if (this.#advertRepeatIntervalMs > 0) {
111596
- this.#startAdvertRepeat(advertMsgs);
111597
- } else {
111598
- this.#sendAll(advertMsgs).catch((err) => this.#fail(err));
111599
- }
111600
- } catch (err) {
111601
- this.#fail(err);
111720
+ if (!this.#defaultConfig) {
111721
+ return Promise.reject(new AggregationServiceError(
111722
+ "run() requires `config` in the runner options; use advertiseCohort(config) to drive cohorts explicitly.",
111723
+ "MISSING_COHORT_CONFIG",
111724
+ {}
111725
+ ));
111726
+ }
111727
+ try {
111728
+ return this.advertiseCohort(this.#defaultConfig).completion;
111729
+ } catch (err) {
111730
+ return Promise.reject(err);
111731
+ }
111732
+ }
111733
+ /**
111734
+ * Wait for every currently-outstanding cohort to settle and return the
111735
+ * successful results. Dynamic drain: cohorts advertised while this is pending
111736
+ * are included, and it resolves only once no cohorts remain. Failed cohorts
111737
+ * are surfaced via `error` / `cohort-failed` events and their rejected
111738
+ * `completion` promises; they are omitted from the returned array (this
111739
+ * method does not throw). Bound long-running cohorts with `cohortTtlMs` /
111740
+ * `phaseTimeoutMs` or this may never resolve.
111741
+ *
111742
+ * @returns {Promise<AggregationResult[]>} Results of the cohorts that completed.
111743
+ */
111744
+ async runAll() {
111745
+ const collected = /* @__PURE__ */ new Map();
111746
+ const onComplete = (result) => {
111747
+ collected.set(result.cohortId, result);
111748
+ };
111749
+ this.on("signing-complete", onComplete);
111750
+ try {
111751
+ while (this.#contexts.size > 0) {
111752
+ await Promise.allSettled([...this.#contexts.values()].map((c2) => c2.completion));
111602
111753
  }
111603
- });
111754
+ } finally {
111755
+ this.off("signing-complete", onComplete);
111756
+ }
111757
+ return [...collected.values()];
111604
111758
  }
111605
111759
  /**
111606
- * Begin publishing the cohort advert immediately and on a repeating interval
111607
- * until {@link #stopAdvertRepeating} is called. Each advert is broadcast
111608
- * (no recipient) via the transport's `publishRepeating` primitive.
111760
+ * Begin publishing a cohort's advert immediately and on a repeating interval
111761
+ * until the cohort's advert loop is stopped. Each advert is broadcast (no
111762
+ * recipient) via the transport's `publishRepeating` primitive.
111609
111763
  */
111610
- #startAdvertRepeat(advertMsgs) {
111764
+ #startAdvertRepeat(ctx, advertMsgs) {
111611
111765
  const stops = [];
111612
111766
  for (const msg of advertMsgs) {
111613
111767
  stops.push(this.#transport.publishRepeating(msg, this.#did, this.#advertRepeatIntervalMs));
111614
111768
  }
111615
- this.#stopAdvertRepeat = () => {
111769
+ ctx.stopAdvertRepeat = () => {
111616
111770
  for (const stop2 of stops) {
111617
111771
  try {
111618
111772
  stop2();
@@ -111621,61 +111775,118 @@ ${value2}`;
111621
111775
  }
111622
111776
  };
111623
111777
  }
111624
- /** Stop the advert republish loop. Idempotent. */
111625
- #stopAdvertRepeating() {
111626
- if (!this.#stopAdvertRepeat) return;
111627
- const stop2 = this.#stopAdvertRepeat;
111628
- this.#stopAdvertRepeat = void 0;
111778
+ /** Stop a cohort's advert republish loop. Idempotent. */
111779
+ #stopAdvertRepeating(ctx) {
111780
+ if (!ctx.stopAdvertRepeat) return;
111781
+ const stop2 = ctx.stopAdvertRepeat;
111782
+ ctx.stopAdvertRepeat = void 0;
111629
111783
  stop2();
111630
111784
  }
111631
- /** Schedule cohort TTL + phase timeout at the start of a run. */
111632
- #startTimers() {
111785
+ /** Schedule a cohort's TTL + phase timeout when it is advertised. */
111786
+ #startTimers(ctx) {
111633
111787
  if (this.#cohortTtlMs !== void 0) {
111634
- this.#cohortTtlTimer = setTimeout(() => {
111635
- const reason = `Cohort ${this.#cohortId ?? ""} exceeded TTL of ${this.#cohortTtlMs}ms`;
111636
- this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
111637
- this.#fail(new Error(reason));
111788
+ ctx.cohortTtlTimer = setTimeout(() => {
111789
+ const reason = `Cohort ${ctx.cohortId} exceeded TTL of ${this.#cohortTtlMs}ms`;
111790
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
111791
+ this.#failCohort(ctx, new Error(reason));
111638
111792
  }, this.#cohortTtlMs);
111639
111793
  }
111640
- this.#resetPhaseTimer();
111794
+ this.#resetPhaseTimer(ctx);
111641
111795
  }
111642
- /** Reset the per-phase stall timer. Called when a phase transition is observed. */
111643
- #resetPhaseTimer() {
111644
- if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
111645
- this.#phaseTimer = void 0;
111796
+ /** Reset a cohort's per-phase stall timer. Called when a phase transition is observed. */
111797
+ #resetPhaseTimer(ctx) {
111798
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
111799
+ ctx.phaseTimer = void 0;
111646
111800
  if (this.#phaseTimeoutMs === void 0) return;
111647
- this.#phaseTimer = setTimeout(() => {
111648
- const reason = `Cohort ${this.#cohortId ?? ""} stalled in phase ${this.#lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
111649
- this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
111650
- this.#fail(new Error(reason));
111801
+ ctx.phaseTimer = setTimeout(() => {
111802
+ const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
111803
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
111804
+ this.#failCohort(ctx, new Error(reason));
111651
111805
  }, this.#phaseTimeoutMs);
111652
111806
  }
111653
- /** Detect a phase change since the last observation and reset the phase timer. */
111654
- #onPhaseMaybeChanged() {
111655
- if (!this.#cohortId) return;
111656
- const phase = this.session.getCohortPhase(this.#cohortId);
111657
- if (phase !== this.#lastObservedPhase) {
111658
- this.#lastObservedPhase = phase;
111659
- this.#resetPhaseTimer();
111807
+ /** Detect a phase change for a cohort since the last observation and reset its phase timer. */
111808
+ #onPhaseMaybeChanged(ctx) {
111809
+ const phase = this.session.getCohortPhase(ctx.cohortId);
111810
+ if (phase !== ctx.lastObservedPhase) {
111811
+ ctx.lastObservedPhase = phase;
111812
+ this.#resetPhaseTimer(ctx);
111660
111813
  }
111661
111814
  }
111662
- /** Clear both timers. Called on successful completion, stop(), and #fail. */
111663
- #clearTimers() {
111664
- if (this.#cohortTtlTimer) clearTimeout(this.#cohortTtlTimer);
111665
- if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
111666
- this.#cohortTtlTimer = void 0;
111667
- this.#phaseTimer = void 0;
111815
+ /** Clear a cohort's timers. Called on completion, stop, and failure. */
111816
+ #clearTimers(ctx) {
111817
+ if (ctx.cohortTtlTimer) clearTimeout(ctx.cohortTtlTimer);
111818
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
111819
+ ctx.cohortTtlTimer = void 0;
111820
+ ctx.phaseTimer = void 0;
111821
+ }
111822
+ /**
111823
+ * Reclaim one cohort's runner-layer bookkeeping: stop its advert loop, clear
111824
+ * its timers, and drop its {@link RunContext}. Does NOT touch sibling cohorts
111825
+ * and does NOT detach the shared transport handlers. Leaves the cohort in the
111826
+ * state machine; whether that cohort's `session` state is also removed is the
111827
+ * caller's choice (see {@link #completeCohort} vs {@link #failCohort}).
111828
+ */
111829
+ #disposeCohort(ctx) {
111830
+ this.#stopAdvertRepeating(ctx);
111831
+ this.#clearTimers(ctx);
111832
+ this.#contexts.delete(ctx.cohortId);
111833
+ }
111834
+ /**
111835
+ * Settle one cohort successfully. Reclaims the runner context but leaves the
111836
+ * completed cohort in `session` so callers can read its beaconAddress / cohort
111837
+ * via `session.getCohort(result.cohortId)`; reclaim it with
111838
+ * `session.removeCohort(cohortId)` when done. Idempotent via `ctx.settled`.
111839
+ */
111840
+ #completeCohort(ctx, result) {
111841
+ if (ctx.settled) return;
111842
+ ctx.settled = true;
111843
+ this.#disposeCohort(ctx);
111844
+ this.emit("signing-complete", result);
111845
+ ctx.resolve(result);
111846
+ }
111847
+ /**
111848
+ * Fail one cohort. Reclaims its runner context, drops its now-dead state from
111849
+ * the state machine, and rejects only its completion; siblings keep running
111850
+ * and the shared transport handlers stay registered. Idempotent via
111851
+ * `ctx.settled`.
111852
+ */
111853
+ #failCohort(ctx, err) {
111854
+ if (ctx.settled) return;
111855
+ ctx.settled = true;
111856
+ this.#disposeCohort(ctx);
111857
+ this.session.removeCohort(ctx.cohortId);
111858
+ this.emit("error", err);
111859
+ ctx.reject(err);
111860
+ }
111861
+ /**
111862
+ * Stop a single cohort early without affecting the rest of the runner. Drops
111863
+ * the cohort's state machine state; its `completion` promise rejects with a
111864
+ * stopped error.
111865
+ */
111866
+ stopCohort(cohortId) {
111867
+ const ctx = this.#contexts.get(cohortId);
111868
+ if (!ctx || ctx.settled) return;
111869
+ ctx.settled = true;
111870
+ this.#disposeCohort(ctx);
111871
+ this.session.removeCohort(cohortId);
111872
+ ctx.reject(new AggregationServiceError(`Cohort ${cohortId} stopped.`, "COHORT_STOPPED", { cohortId }));
111668
111873
  }
111669
111874
  /**
111670
- * Stop the runner early. Marks the runner stopped and detaches transport
111671
- * handlers so a restart or a new runner doesn't inherit stale dispatch.
111875
+ * Stop the whole runner. Fails every outstanding cohort, then detaches the
111876
+ * shared transport handlers so a restart or a new runner doesn't inherit
111877
+ * stale dispatch. Safe to call repeatedly.
111672
111878
  */
111673
111879
  stop() {
111674
111880
  this.#stopped = true;
111675
- this.#stopAdvertRepeating();
111676
- this.#clearTimers();
111881
+ for (const ctx of [...this.#contexts.values()]) {
111882
+ if (ctx.settled) continue;
111883
+ ctx.settled = true;
111884
+ this.#disposeCohort(ctx);
111885
+ this.session.removeCohort(ctx.cohortId);
111886
+ ctx.reject(new AggregationServiceError("Service runner stopped.", "RUNNER_STOPPED", { cohortId: ctx.cohortId }));
111887
+ }
111888
+ this.#contexts.clear();
111677
111889
  this.#unregisterHandlers();
111678
- if (this.#cohortId) this.session.removeCohort(this.#cohortId);
111679
111890
  }
111680
111891
  /** Message types this runner listens for on the transport. */
111681
111892
  static #HANDLED_MESSAGE_TYPES = [
@@ -111686,7 +111897,10 @@ ${value2}`;
111686
111897
  SIGNATURE_AUTHORIZATION
111687
111898
  ];
111688
111899
  /**
111689
- * Internal: handler registration with the transport. Idempotent.
111900
+ * Internal: handler registration with the transport. Idempotent. Handlers
111901
+ * are DID-scoped and cohort-agnostic — one registration serves every cohort
111902
+ * this runner drives; demux to the right {@link RunContext} happens in each
111903
+ * handler via the inbound message's cohortId.
111690
111904
  */
111691
111905
  #registerHandlers() {
111692
111906
  if (this.#handlersRegistered) return;
@@ -111707,23 +111921,25 @@ ${value2}`;
111707
111921
  }
111708
111922
  /**
111709
111923
  * Internal: message handlers for each protocol step. Each handler:
111710
- * 1) feeds the message into the state machine via session.receive()
111711
- * 2) emits a high-level event for external observers
111712
- * 3) checks if the new state triggers any automatic next steps, and if so:
111924
+ * 1) resolves the cohort the message belongs to (by cohortId); ignores it if unknown
111925
+ * 2) feeds the message into the state machine via session.receive()
111926
+ * 3) emits a high-level event (carrying cohortId) for external observers
111927
+ * 4) checks if the new state triggers any automatic next steps, and if so:
111713
111928
  * a) calls the appropriate decision callback(s)
111714
111929
  * b) sends any resulting messages from the state machine
111930
+ * Errors fail only the owning cohort. A stopped runner ignores messages.
111715
111931
  * @param {BaseMessage} msg - The incoming message to handle.
111716
111932
  * @returns {Promise<void>} Resolves when handling is complete.
111717
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111718
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111719
111933
  */
111720
111934
  async #handleOptIn(msg) {
111721
111935
  if (this.#stopped) return;
111936
+ const ctx = this.#contextFor(msg);
111937
+ if (!ctx) return;
111722
111938
  try {
111723
111939
  this.session.receive(msg);
111724
- this.#drainRejections();
111725
- this.#onPhaseMaybeChanged();
111726
- const optIn = this.session.pendingOptIns(this.#cohortId).get(msg.from);
111940
+ this.#drainRejections(ctx);
111941
+ this.#onPhaseMaybeChanged(ctx);
111942
+ const optIn = this.session.pendingOptIns(ctx.cohortId).get(msg.from);
111727
111943
  if (!optIn) return;
111728
111944
  this.emit("opt-in-received", optIn);
111729
111945
  if (optIn.communicationPk) {
@@ -111731,29 +111947,34 @@ ${value2}`;
111731
111947
  }
111732
111948
  const decision = await this.#onOptInReceived(optIn);
111733
111949
  if (!decision.accepted) return;
111734
- await this.#sendAll(this.session.acceptParticipant(this.#cohortId, msg.from));
111735
- this.emit("participant-accepted", { participantDid: msg.from });
111736
- const cohort = this.session.getCohort(this.#cohortId);
111737
- if (cohort.participants.length >= this.#config.minParticipants && !this.#finalizing) {
111738
- this.#finalizing = true;
111950
+ const maxParticipants = ctx.config.maxParticipants;
111951
+ const cohortNow = this.session.getCohort(ctx.cohortId);
111952
+ if (maxParticipants !== void 0 && cohortNow && cohortNow.participants.length >= maxParticipants) {
111953
+ return;
111954
+ }
111955
+ await this.#sendAll(this.session.acceptParticipant(ctx.cohortId, msg.from));
111956
+ this.emit("participant-accepted", { cohortId: ctx.cohortId, participantDid: msg.from });
111957
+ const cohort = this.session.getCohort(ctx.cohortId);
111958
+ if (cohort.participants.length >= ctx.config.minParticipants && !ctx.finalizing) {
111959
+ ctx.finalizing = true;
111739
111960
  const finalizeDecision = await this.#onReadyToFinalize({
111740
111961
  acceptedCount: cohort.participants.length,
111741
- minRequired: this.#config.minParticipants
111962
+ minRequired: ctx.config.minParticipants
111742
111963
  });
111743
111964
  if (!finalizeDecision.finalize) {
111744
- this.#finalizing = false;
111965
+ ctx.finalizing = false;
111745
111966
  return;
111746
111967
  }
111747
- const readyMsgs = this.session.finalizeKeygen(this.#cohortId);
111748
- this.#stopAdvertRepeating();
111968
+ const readyMsgs = this.session.finalizeKeygen(ctx.cohortId);
111969
+ this.#stopAdvertRepeating(ctx);
111749
111970
  this.emit("keygen-complete", {
111750
- cohortId: this.#cohortId,
111971
+ cohortId: ctx.cohortId,
111751
111972
  beaconAddress: cohort.beaconAddress
111752
111973
  });
111753
111974
  await this.#sendAll(readyMsgs);
111754
111975
  }
111755
111976
  } catch (err) {
111756
- this.#fail(err);
111977
+ this.#failCohort(ctx, err);
111757
111978
  }
111758
111979
  }
111759
111980
  /**
@@ -111761,23 +111982,23 @@ ${value2}`;
111761
111982
  * and distributes the data for validation.
111762
111983
  * @param {BaseMessage} msg - The incoming message to handle.
111763
111984
  * @returns {Promise<void>} Resolves when handling is complete.
111764
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111765
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111766
111985
  */
111767
111986
  async #handleSubmitUpdate(msg) {
111768
111987
  if (this.#stopped) return;
111988
+ const ctx = this.#contextFor(msg);
111989
+ if (!ctx) return;
111769
111990
  try {
111770
111991
  this.session.receive(msg);
111771
- this.#drainRejections();
111772
- this.#onPhaseMaybeChanged();
111773
- this.emit("update-received", { participantDid: msg.from });
111774
- if (this.session.getCohortPhase(this.#cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
111775
- const distributeMsgs = this.session.buildAndDistribute(this.#cohortId);
111776
- this.emit("data-distributed", { cohortId: this.#cohortId });
111992
+ this.#drainRejections(ctx);
111993
+ this.#onPhaseMaybeChanged(ctx);
111994
+ this.emit("update-received", { cohortId: ctx.cohortId, participantDid: msg.from });
111995
+ if (this.session.getCohortPhase(ctx.cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
111996
+ const distributeMsgs = this.session.buildAndDistribute(ctx.cohortId);
111997
+ this.emit("data-distributed", { cohortId: ctx.cohortId });
111777
111998
  await this.#sendAll(distributeMsgs);
111778
111999
  }
111779
112000
  } catch (err) {
111780
- this.#fail(err);
112001
+ this.#failCohort(ctx, err);
111781
112002
  }
111782
112003
  }
111783
112004
  /**
@@ -111785,111 +112006,95 @@ ${value2}`;
111785
112006
  * automatically requests tx data and starts signing.
111786
112007
  * @param {BaseMessage} msg - The incoming message to handle.
111787
112008
  * @returns {Promise<void>} Resolves when handling is complete.
111788
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111789
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111790
112009
  */
111791
112010
  async #handleValidationAck(msg) {
111792
112011
  if (this.#stopped) return;
112012
+ const ctx = this.#contextFor(msg);
112013
+ if (!ctx) return;
111793
112014
  try {
111794
112015
  this.session.receive(msg);
111795
- this.#drainRejections();
111796
- this.#onPhaseMaybeChanged();
112016
+ this.#drainRejections(ctx);
112017
+ this.#onPhaseMaybeChanged(ctx);
111797
112018
  const approved = !!msg.body?.approved;
111798
- this.emit("validation-received", { participantDid: msg.from, approved });
111799
- const phase = this.session.getCohortPhase(this.#cohortId);
112019
+ this.emit("validation-received", { cohortId: ctx.cohortId, participantDid: msg.from, approved });
112020
+ const phase = this.session.getCohortPhase(ctx.cohortId);
111800
112021
  if (phase === "Failed" /* Failed */) {
111801
112022
  const reason = `Validation rejected by participant ${msg.from}`;
111802
- this.emit("cohort-failed", { cohortId: this.#cohortId, reason });
111803
- this.#fail(new Error(reason));
112023
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
112024
+ this.#failCohort(ctx, new Error(reason));
111804
112025
  return;
111805
112026
  }
111806
112027
  if (phase === "Validated" /* Validated */) {
111807
- const cohort = this.session.getCohort(this.#cohortId);
112028
+ const cohort = this.session.getCohort(ctx.cohortId);
111808
112029
  const txData = await this.#onProvideTxData({
111809
- cohortId: this.#cohortId,
112030
+ cohortId: ctx.cohortId,
111810
112031
  beaconAddress: cohort.beaconAddress,
111811
112032
  signalBytes: cohort.signalBytes
111812
112033
  });
111813
- const authMsgs = this.session.startSigning(this.#cohortId, txData);
111814
- const sessionId = this.session.getSigningSessionId(this.#cohortId) ?? "";
111815
- this.emit("signing-started", { sessionId });
112034
+ const authMsgs = this.session.startSigning(ctx.cohortId, txData);
112035
+ const sessionId = this.session.getSigningSessionId(ctx.cohortId) ?? "";
112036
+ this.emit("signing-started", { cohortId: ctx.cohortId, sessionId });
111816
112037
  await this.#sendAll(authMsgs);
111817
112038
  }
111818
112039
  } catch (err) {
111819
- this.#fail(err);
112040
+ this.#failCohort(ctx, err);
111820
112041
  }
111821
112042
  }
111822
112043
  /**
111823
- * Handler for receiving nonce contributions and signature authorizations. When all nonces or
111824
- * signatures are received,
112044
+ * Handler for receiving nonce contributions. When all nonces are received, sends the aggregated
112045
+ * nonce back to the cohort.
111825
112046
  * @param {BaseMessage} msg - The incoming message to handle.
111826
112047
  * @returns {Promise<void>} Resolves when handling is complete.
111827
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111828
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111829
112048
  */
111830
112049
  async #handleNonceContribution(msg) {
111831
112050
  if (this.#stopped) return;
112051
+ const ctx = this.#contextFor(msg);
112052
+ if (!ctx) return;
111832
112053
  try {
111833
112054
  this.session.receive(msg);
111834
- this.#drainRejections();
111835
- this.#onPhaseMaybeChanged();
111836
- this.emit("nonce-received", { participantDid: msg.from });
111837
- if (this.session.getCohortPhase(this.#cohortId) === "NoncesCollected" /* NoncesCollected */) {
111838
- await this.#sendAll(this.session.sendAggregatedNonce(this.#cohortId));
112055
+ this.#drainRejections(ctx);
112056
+ this.#onPhaseMaybeChanged(ctx);
112057
+ this.emit("nonce-received", { cohortId: ctx.cohortId, participantDid: msg.from });
112058
+ if (this.session.getCohortPhase(ctx.cohortId) === "NoncesCollected" /* NoncesCollected */) {
112059
+ await this.#sendAll(this.session.sendAggregatedNonce(ctx.cohortId));
111839
112060
  }
111840
112061
  } catch (err) {
111841
- this.#fail(err);
112062
+ this.#failCohort(ctx, err);
111842
112063
  }
111843
112064
  }
111844
112065
  /**
111845
112066
  * Handler for receiving signature authorizations. When all partial signatures are received, the
111846
- * session automatically completes and the final result is emitted and the run() promise is resolved.
112067
+ * session automatically completes; the final result is emitted and the cohort's completion
112068
+ * promise resolves.
111847
112069
  * @param {BaseMessage} msg - The incoming message to handle.
111848
112070
  * @returns {Promise<void>} Resolves when handling is complete.
111849
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111850
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111851
112071
  */
111852
112072
  async #handleSignatureAuthorization(msg) {
111853
112073
  if (this.#stopped) return;
112074
+ const ctx = this.#contextFor(msg);
112075
+ if (!ctx) return;
111854
112076
  try {
111855
112077
  this.session.receive(msg);
111856
- this.#drainRejections();
111857
- this.#onPhaseMaybeChanged();
111858
- const result = this.session.getResult(this.#cohortId);
112078
+ this.#drainRejections(ctx);
112079
+ this.#onPhaseMaybeChanged(ctx);
112080
+ const result = this.session.getResult(ctx.cohortId);
111859
112081
  if (result) {
111860
- this.#clearTimers();
111861
- this.#unregisterHandlers();
111862
- this.emit("signing-complete", result);
111863
- this.#resolveRun?.(result);
112082
+ this.#completeCohort(ctx, result);
111864
112083
  }
111865
112084
  } catch (err) {
111866
- this.#fail(err);
112085
+ this.#failCohort(ctx, err);
111867
112086
  }
111868
112087
  }
111869
112088
  /**
111870
112089
  * Internal: helper to send all messages sequentially. Catches and propagates errors.
111871
112090
  * @param {BaseMessage[]} msgs - The messages to send.
111872
112091
  * @returns {Promise<void>} Resolves when all messages have been sent.
111873
- * @throws {Error} If sending any message fails, the error is emitted and the run promise is
111874
- * rejected.
111875
112092
  */
111876
112093
  async #sendAll(msgs) {
111877
112094
  for (const m2 of msgs) {
111878
112095
  await this.#transport.sendMessage(m2, this.#did, m2.to);
111879
112096
  }
111880
112097
  }
111881
- /**
111882
- * Internal: helper to handle errors. Emits an 'error' event and rejects the run promise.
111883
- * @param {Error} err - The error to handle.
111884
- */
111885
- #fail(err) {
111886
- this.#stopAdvertRepeating();
111887
- this.#clearTimers();
111888
- this.#unregisterHandlers();
111889
- if (this.#cohortId) this.session.removeCohort(this.#cohortId);
111890
- this.emit("error", err);
111891
- this.#rejectRun?.(err);
111892
- }
111893
112098
  };
111894
112099
 
111895
112100
  // src/core/aggregation/runner/participant-runner.ts
@@ -111950,7 +112155,8 @@ ${value2}`;
111950
112155
  }
111951
112156
  /**
111952
112157
  * Single-shot helper: start, join the first cohort that passes `shouldJoin`,
111953
- * drive it to completion, and resolve. Convenient for tests and demos.
112158
+ * drive it to completion, and resolve. Convenient for tests and demos. The
112159
+ * single-cohort special case of {@link joinMatching} (count = 1).
111954
112160
  */
111955
112161
  static async joinFirst(options2) {
111956
112162
  return new Promise((resolve, reject) => {
@@ -111963,6 +112169,37 @@ ${value2}`;
111963
112169
  runner.start().catch(reject);
111964
112170
  });
111965
112171
  }
112172
+ /**
112173
+ * Multi-cohort helper: start, join EVERY cohort whose advert passes
112174
+ * `shouldJoin`, drive each to completion in parallel, and resolve once
112175
+ * `count` cohorts have completed (the runner stops at that point). The
112176
+ * N-cohort generalization of {@link joinFirst}, for a participant that joins
112177
+ * several cohorts advertised by one service.
112178
+ *
112179
+ * For an open-ended, long-lived subscriber (no fixed count), construct an
112180
+ * {@link AggregationParticipantRunner} directly, set `shouldJoin`, call
112181
+ * `start()`, and listen for `cohort-complete` — the runner already drives
112182
+ * any number of cohorts concurrently.
112183
+ *
112184
+ * @param options Participant runner options (set `shouldJoin` to select cohorts).
112185
+ * @param count Number of completed cohorts to collect before resolving.
112186
+ * @returns The {@link CohortCompleteInfo} for each completed cohort, in completion order.
112187
+ */
112188
+ static async joinMatching(options2, count) {
112189
+ return new Promise((resolve, reject) => {
112190
+ const runner = new _AggregationParticipantRunner(options2);
112191
+ const completed = [];
112192
+ runner.on("cohort-complete", (info) => {
112193
+ completed.push(info);
112194
+ if (completed.length >= count) {
112195
+ runner.stop();
112196
+ resolve(completed);
112197
+ }
112198
+ });
112199
+ runner.on("error", reject);
112200
+ runner.start().catch(reject);
112201
+ });
112202
+ }
111966
112203
  /**
111967
112204
  * Internal: handler registration with the transport. Idempotent and safe to call multiple times,
111968
112205
  * but only registers handlers once.
@@ -112111,7 +112348,7 @@ ${value2}`;
112111
112348
  if (this.session.getCohortPhase(cohortId) === "Complete" /* Complete */) {
112112
112349
  const info = this.session.joinedCohorts.get(cohortId);
112113
112350
  if (info) {
112114
- const validation = this.session.pendingValidations.get(cohortId);
112351
+ const validation = this.session.getValidation(cohortId);
112115
112352
  this.emit("cohort-complete", {
112116
112353
  cohortId,
112117
112354
  beaconAddress: info.beaconAddress,