@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.mjs CHANGED
@@ -103245,7 +103245,7 @@ var AggregationCohort = class {
103245
103245
  validationRejections = /* @__PURE__ */ new Set();
103246
103246
  constructor({ id, minParticipants, serviceDid, network, beaconType }) {
103247
103247
  this.id = id || crypto.randomUUID();
103248
- this.minParticipants = minParticipants || 2;
103248
+ this.minParticipants = minParticipants ?? 2;
103249
103249
  this.serviceDid = serviceDid || "";
103250
103250
  this.network = network;
103251
103251
  this.beaconType = beaconType || "CASBeacon";
@@ -103411,6 +103411,57 @@ var AggregationCohort = class {
103411
103411
  }
103412
103412
  };
103413
103413
 
103414
+ // src/core/aggregation/conditions.ts
103415
+ init_shim();
103416
+ var KNOWN_BEACON_TYPES = ["CASBeacon", "SMTBeacon"];
103417
+ function checkPair(problems, label, min, max) {
103418
+ if (min !== void 0 && (!Number.isInteger(min) || min < 0)) {
103419
+ problems.push(`min${label} must be an integer >= 0`);
103420
+ }
103421
+ if (max !== void 0 && (!Number.isInteger(max) || max < 0)) {
103422
+ problems.push(`max${label} must be an integer >= 0`);
103423
+ }
103424
+ if (min !== void 0 && max !== void 0 && Number.isInteger(min) && Number.isInteger(max) && max < min) {
103425
+ problems.push(`max${label} must be >= min${label}`);
103426
+ }
103427
+ }
103428
+ function checkCost(problems, label, cost) {
103429
+ if (cost === void 0) return;
103430
+ if (typeof cost.amount !== "number" || !Number.isFinite(cost.amount) || cost.amount < 0) {
103431
+ problems.push(`${label}.amount must be a finite number >= 0`);
103432
+ }
103433
+ if (typeof cost.unit !== "string" || cost.unit.length === 0) {
103434
+ problems.push(`${label}.unit must be a non-empty string`);
103435
+ }
103436
+ if (cost.basis !== void 0 && cost.basis !== "per-did" && cost.basis !== "per-participant") {
103437
+ problems.push(`${label}.basis must be 'per-did' or 'per-participant'`);
103438
+ }
103439
+ }
103440
+ function validateCohortConditions(c2) {
103441
+ const problems = [];
103442
+ if (!KNOWN_BEACON_TYPES.includes(c2.beaconType)) {
103443
+ problems.push(`beaconType must be one of ${KNOWN_BEACON_TYPES.join(", ")}`);
103444
+ }
103445
+ if (!Number.isInteger(c2.minParticipants) || c2.minParticipants < 1) {
103446
+ problems.push("minParticipants must be an integer >= 1");
103447
+ }
103448
+ if (c2.maxParticipants !== void 0) {
103449
+ if (!Number.isInteger(c2.maxParticipants) || c2.maxParticipants < 1) {
103450
+ problems.push("maxParticipants must be an integer >= 1");
103451
+ } else if (Number.isInteger(c2.minParticipants) && c2.maxParticipants < c2.minParticipants) {
103452
+ problems.push("maxParticipants must be >= minParticipants");
103453
+ }
103454
+ }
103455
+ checkPair(problems, "DidsPerParticipant", c2.minDidsPerParticipant, c2.maxDidsPerParticipant);
103456
+ checkPair(problems, "SecondsBetweenAnnouncements", c2.minSecondsBetweenAnnouncements, c2.maxSecondsBetweenAnnouncements);
103457
+ if (c2.pendingUpdateTrigger !== void 0 && (!Number.isInteger(c2.pendingUpdateTrigger) || c2.pendingUpdateTrigger < 1)) {
103458
+ problems.push("pendingUpdateTrigger must be an integer >= 1");
103459
+ }
103460
+ checkCost(problems, "costOfEnrollment", c2.costOfEnrollment);
103461
+ checkCost(problems, "costPerAnnouncement", c2.costPerAnnouncement);
103462
+ return problems;
103463
+ }
103464
+
103414
103465
  // src/core/aggregation/messages/base.ts
103415
103466
  init_shim();
103416
103467
  var AGGREGATION_WIRE_VERSION = 1;
@@ -103845,6 +103896,14 @@ var AggregationService = class {
103845
103896
  * Cohort starts in `Created` phase — call `advertise()` to broadcast.
103846
103897
  */
103847
103898
  createCohort(config) {
103899
+ const problems = validateCohortConditions(config);
103900
+ if (problems.length > 0) {
103901
+ throw new AggregationServiceError(
103902
+ `Invalid cohort conditions: ${problems.join("; ")}`,
103903
+ "INVALID_COHORT_CONDITIONS",
103904
+ { problems }
103905
+ );
103906
+ }
103848
103907
  const cohort = new AggregationCohort({
103849
103908
  serviceDid: this.did,
103850
103909
  minParticipants: config.minParticipants,
@@ -103877,13 +103936,13 @@ var AggregationService = class {
103877
103936
  { cohortId, phase: state.phase }
103878
103937
  );
103879
103938
  }
103939
+ const { network, ...conditions } = state.config;
103880
103940
  const message2 = createCohortAdvertMessage({
103881
103941
  from: this.did,
103882
103942
  cohortId,
103883
- cohortSize: state.config.minParticipants,
103884
- beaconType: state.config.beaconType,
103885
- network: state.config.network,
103886
- communicationPk: this.publicKey.compressed
103943
+ network,
103944
+ communicationPk: this.publicKey.compressed,
103945
+ ...conditions
103887
103946
  });
103888
103947
  state.phase = "Advertised" /* Advertised */;
103889
103948
  return [message2];
@@ -103942,6 +104001,14 @@ var AggregationService = class {
103942
104001
  { cohortId, participantDid }
103943
104002
  );
103944
104003
  }
104004
+ const maxParticipants = state.config.maxParticipants;
104005
+ if (maxParticipants !== void 0 && state.acceptedParticipants.size >= maxParticipants) {
104006
+ throw new AggregationServiceError(
104007
+ `Cohort ${cohortId} is full: ${maxParticipants} participants already accepted.`,
104008
+ "COHORT_FULL",
104009
+ { cohortId, maxParticipants }
104010
+ );
104011
+ }
103945
104012
  state.acceptedParticipants.add(participantDid);
103946
104013
  state.cohort.participants.push(participantDid);
103947
104014
  state.cohort.participantKeys.set(participantDid, optIn.participantPk);
@@ -103975,6 +104042,14 @@ var AggregationService = class {
103975
104042
  { cohortId }
103976
104043
  );
103977
104044
  }
104045
+ const maxParticipants = state.config.maxParticipants;
104046
+ if (maxParticipants !== void 0 && state.acceptedParticipants.size > maxParticipants) {
104047
+ throw new AggregationServiceError(
104048
+ `Cohort ${cohortId} has ${state.acceptedParticipants.size} accepted participants, exceeds max ${maxParticipants}.`,
104049
+ "TOO_MANY_PARTICIPANTS",
104050
+ { cohortId, maxParticipants }
104051
+ );
104052
+ }
103978
104053
  const beaconAddress = state.cohort.computeBeaconAddress();
103979
104054
  state.phase = "CohortSet" /* CohortSet */;
103980
104055
  const messages2 = [];
@@ -104292,6 +104367,59 @@ var AggregationService = class {
104292
104367
  // src/core/aggregation/participant.ts
104293
104368
  init_shim();
104294
104369
  init_utils();
104370
+
104371
+ // src/core/aggregation/messages/bodies.ts
104372
+ init_shim();
104373
+ var hasStr = (b, k) => !!b && typeof b[k] === "string";
104374
+ var hasIntMin = (b, k, min) => {
104375
+ const v = b ? b[k] : void 0;
104376
+ return typeof v === "number" && Number.isInteger(v) && v >= min;
104377
+ };
104378
+ var optIntMin = (b, k, min) => {
104379
+ const v = b ? b[k] : void 0;
104380
+ return v === void 0 || typeof v === "number" && Number.isInteger(v) && v >= min;
104381
+ };
104382
+ var hasBool = (b, k) => !!b && typeof b[k] === "boolean";
104383
+ var hasBytes = (b, k) => !!b && b[k] instanceof Uint8Array;
104384
+ var hasBytesArray = (b, k) => {
104385
+ const v = b ? b[k] : void 0;
104386
+ return Array.isArray(v) && v.every((x) => x instanceof Uint8Array);
104387
+ };
104388
+ function isCohortAdvertMessage(m2) {
104389
+ 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");
104390
+ }
104391
+ function isCohortOptInMessage(m2) {
104392
+ return m2.type === COHORT_OPT_IN && hasStr(m2.body, "cohortId") && hasBytes(m2.body, "participantPk") && hasBytes(m2.body, "communicationPk");
104393
+ }
104394
+ function isCohortOptInAcceptMessage(m2) {
104395
+ return m2.type === COHORT_OPT_IN_ACCEPT && hasStr(m2.body, "cohortId");
104396
+ }
104397
+ function isCohortReadyMessage(m2) {
104398
+ return m2.type === COHORT_READY && hasStr(m2.body, "cohortId") && hasStr(m2.body, "beaconAddress") && hasBytesArray(m2.body, "cohortKeys");
104399
+ }
104400
+ function isSubmitUpdateMessage(m2) {
104401
+ return m2.type === SUBMIT_UPDATE && hasStr(m2.body, "cohortId") && !!m2.body && typeof m2.body.signedUpdate === "object";
104402
+ }
104403
+ function isDistributeAggregatedDataMessage(m2) {
104404
+ return m2.type === DISTRIBUTE_AGGREGATED_DATA && hasStr(m2.body, "cohortId") && hasStr(m2.body, "beaconType") && hasStr(m2.body, "signalBytesHex");
104405
+ }
104406
+ function isValidationAckMessage(m2) {
104407
+ return m2.type === VALIDATION_ACK && hasStr(m2.body, "cohortId") && hasBool(m2.body, "approved");
104408
+ }
104409
+ function isAuthorizationRequestMessage(m2) {
104410
+ 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");
104411
+ }
104412
+ function isNonceContributionMessage(m2) {
104413
+ return m2.type === NONCE_CONTRIBUTION && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "nonceContribution");
104414
+ }
104415
+ function isAggregatedNonceMessage(m2) {
104416
+ return m2.type === AGGREGATED_NONCE && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "aggregatedNonce");
104417
+ }
104418
+ function isSignatureAuthorizationMessage(m2) {
104419
+ return m2.type === SIGNATURE_AUTHORIZATION && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "partialSignature");
104420
+ }
104421
+
104422
+ // src/core/aggregation/participant.ts
104295
104423
  var AggregationParticipant = class {
104296
104424
  did;
104297
104425
  /** MuSig2 signing capability. The raw secret never lives as a field here. */
@@ -104349,16 +104477,15 @@ var AggregationParticipant = class {
104349
104477
  return map3;
104350
104478
  }
104351
104479
  #handleCohortAdvert(message2) {
104352
- const cohortId = message2.body?.cohortId;
104353
- if (!cohortId) return;
104480
+ if (!isCohortAdvertMessage(message2)) return;
104481
+ const { cohortId, network, communicationPk, ...conditions } = message2.body;
104354
104482
  if (this.#cohortStates.has(cohortId)) return;
104355
104483
  const advert = {
104356
104484
  cohortId,
104357
104485
  serviceDid: message2.from,
104358
- cohortSize: message2.body?.cohortSize ?? 0,
104359
- network: message2.body?.network ?? "",
104360
- beaconType: message2.body?.beaconType ?? "CASBeacon",
104361
- serviceCommunicationPk: message2.body?.communicationPk ?? new Uint8Array()
104486
+ network,
104487
+ serviceCommunicationPk: communicationPk,
104488
+ ...conditions
104362
104489
  };
104363
104490
  this.#cohortStates.set(cohortId, {
104364
104491
  phase: "Discovered" /* Discovered */,
@@ -104383,7 +104510,7 @@ var AggregationParticipant = class {
104383
104510
  const cohort = new AggregationCohort({
104384
104511
  id: cohortId,
104385
104512
  serviceDid: state.serviceDid,
104386
- minParticipants: state.advert.cohortSize,
104513
+ minParticipants: state.advert.minParticipants,
104387
104514
  network: state.advert.network,
104388
104515
  beaconType: state.advert.beaconType
104389
104516
  });
@@ -104462,6 +104589,17 @@ var AggregationParticipant = class {
104462
104589
  }
104463
104590
  return map3;
104464
104591
  }
104592
+ /**
104593
+ * The validated aggregated data retained for a cohort, regardless of phase.
104594
+ * Unlike {@link pendingValidations} (which lists only cohorts still awaiting
104595
+ * the validate decision), this returns the stored validation — including the
104596
+ * participant's sidecar (the CAS Announcement map or its SMT inclusion proof)
104597
+ * — so it is still readable once the cohort reaches Complete. Returns
104598
+ * undefined before aggregated data has been received.
104599
+ */
104600
+ getValidation(cohortId) {
104601
+ return this.#cohortStates.get(cohortId)?.validation;
104602
+ }
104465
104603
  #handleDistributeAggregatedData(message2) {
104466
104604
  const cohortId = message2.body?.cohortId;
104467
104605
  if (!cohortId) return;
@@ -104706,50 +104844,6 @@ var SILENT_LOGGER = {
104706
104844
  // src/core/aggregation/messages/index.ts
104707
104845
  init_shim();
104708
104846
 
104709
- // src/core/aggregation/messages/bodies.ts
104710
- init_shim();
104711
- var hasStr = (b, k) => !!b && typeof b[k] === "string";
104712
- var hasNum = (b, k) => !!b && typeof b[k] === "number";
104713
- var hasBool = (b, k) => !!b && typeof b[k] === "boolean";
104714
- var hasBytes = (b, k) => !!b && b[k] instanceof Uint8Array;
104715
- var hasBytesArray = (b, k) => {
104716
- const v = b ? b[k] : void 0;
104717
- return Array.isArray(v) && v.every((x) => x instanceof Uint8Array);
104718
- };
104719
- function isCohortAdvertMessage(m2) {
104720
- 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");
104721
- }
104722
- function isCohortOptInMessage(m2) {
104723
- return m2.type === COHORT_OPT_IN && hasStr(m2.body, "cohortId") && hasBytes(m2.body, "participantPk") && hasBytes(m2.body, "communicationPk");
104724
- }
104725
- function isCohortOptInAcceptMessage(m2) {
104726
- return m2.type === COHORT_OPT_IN_ACCEPT && hasStr(m2.body, "cohortId");
104727
- }
104728
- function isCohortReadyMessage(m2) {
104729
- return m2.type === COHORT_READY && hasStr(m2.body, "cohortId") && hasStr(m2.body, "beaconAddress") && hasBytesArray(m2.body, "cohortKeys");
104730
- }
104731
- function isSubmitUpdateMessage(m2) {
104732
- return m2.type === SUBMIT_UPDATE && hasStr(m2.body, "cohortId") && !!m2.body && typeof m2.body.signedUpdate === "object";
104733
- }
104734
- function isDistributeAggregatedDataMessage(m2) {
104735
- return m2.type === DISTRIBUTE_AGGREGATED_DATA && hasStr(m2.body, "cohortId") && hasStr(m2.body, "beaconType") && hasStr(m2.body, "signalBytesHex");
104736
- }
104737
- function isValidationAckMessage(m2) {
104738
- return m2.type === VALIDATION_ACK && hasStr(m2.body, "cohortId") && hasBool(m2.body, "approved");
104739
- }
104740
- function isAuthorizationRequestMessage(m2) {
104741
- 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");
104742
- }
104743
- function isNonceContributionMessage(m2) {
104744
- return m2.type === NONCE_CONTRIBUTION && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "nonceContribution");
104745
- }
104746
- function isAggregatedNonceMessage(m2) {
104747
- return m2.type === AGGREGATED_NONCE && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "aggregatedNonce");
104748
- }
104749
- function isSignatureAuthorizationMessage(m2) {
104750
- return m2.type === SIGNATURE_AUTHORIZATION && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "partialSignature");
104751
- }
104752
-
104753
104847
  // src/core/aggregation/messages/guards.ts
104754
104848
  init_shim();
104755
104849
  var KEYGEN_VALUES = /* @__PURE__ */ new Set([
@@ -111382,35 +111476,22 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111382
111476
  session;
111383
111477
  #transport;
111384
111478
  #did;
111385
- #config;
111479
+ #defaultConfig;
111386
111480
  #onOptInReceived;
111387
111481
  #onReadyToFinalize;
111388
111482
  #onProvideTxData;
111389
111483
  #cohortTtlMs;
111390
111484
  #phaseTimeoutMs;
111391
111485
  #advertRepeatIntervalMs;
111392
- #cohortId;
111486
+ /** Per-cohort run state, keyed by cohortId. */
111487
+ #contexts = /* @__PURE__ */ new Map();
111393
111488
  #handlersRegistered = false;
111394
111489
  #stopped = false;
111395
- /**
111396
- * Guard against the async race where two concurrent #handleOptIn invocations
111397
- * both pass the `participants.length >= minParticipants` check before either
111398
- * mutates the cohort phase. Set synchronously before any `await` so subsequent
111399
- * handlers observe it on their next resumption.
111400
- */
111401
- #finalizing = false;
111402
- #resolveRun;
111403
- #rejectRun;
111404
- #cohortTtlTimer;
111405
- #phaseTimer;
111406
- #lastObservedPhase;
111407
- /** Stop handle for the repeating COHORT_ADVERT publish loop. */
111408
- #stopAdvertRepeat;
111409
111490
  constructor(options2) {
111410
111491
  super();
111411
111492
  this.#transport = options2.transport;
111412
111493
  this.#did = options2.did;
111413
- this.#config = options2.config;
111494
+ this.#defaultConfig = options2.config;
111414
111495
  this.#onOptInReceived = options2.onOptInReceived ?? (async () => ({ accepted: true }));
111415
111496
  this.#onReadyToFinalize = options2.onReadyToFinalize ?? (async ({ acceptedCount, minRequired }) => ({
111416
111497
  finalize: acceptedCount >= minRequired
@@ -111428,55 +111509,126 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111428
111509
  maxUpdateSizeBytes: options2.maxUpdateSizeBytes
111429
111510
  });
111430
111511
  }
111512
+ /** Resolve the {@link RunContext} an inbound message belongs to, by cohortId. */
111513
+ #contextFor(msg) {
111514
+ const cohortId = msg.body?.cohortId;
111515
+ if (!cohortId) return void 0;
111516
+ return this.#contexts.get(cohortId);
111517
+ }
111431
111518
  /**
111432
- * Drain any silent rejections the state machine recorded during the most
111433
- * recent receive() and surface them as `message-rejected` events. Safe to
111434
- * call even before a cohortId is assigned.
111519
+ * Drain any silent rejections the state machine recorded for a cohort during
111520
+ * the most recent receive() and surface them as `message-rejected` events.
111435
111521
  */
111436
- #drainRejections() {
111437
- if (!this.#cohortId) return;
111438
- for (const r2 of this.session.drainRejections(this.#cohortId)) {
111439
- this.emit("message-rejected", { cohortId: this.#cohortId, ...r2 });
111522
+ #drainRejections(ctx) {
111523
+ for (const r2 of this.session.drainRejections(ctx.cohortId)) {
111524
+ this.emit("message-rejected", { cohortId: ctx.cohortId, ...r2 });
111440
111525
  }
111441
111526
  }
111442
111527
  /**
111443
- * Run the protocol to completion. Resolves with the final aggregation result
111444
- * (signature + signed transaction) once signing is complete.
111528
+ * Advertise a new cohort and begin driving it to completion. Callable many
111529
+ * times on one runner; each cohort runs concurrently and independently.
111530
+ *
111531
+ * @param config Per-cohort conditions + network (see {@link CohortConfig}).
111532
+ * @returns The new cohort's id and a `completion` promise that resolves with
111533
+ * that cohort's {@link AggregationResult} (or rejects if it fails/stalls).
111534
+ * @throws If the runner has been stopped, or the config is invalid
111535
+ * (fail-fast via `createCohort`).
111536
+ */
111537
+ advertiseCohort(config) {
111538
+ if (this.#stopped) {
111539
+ throw new AggregationServiceError("Cannot advertise on a stopped runner.", "RUNNER_STOPPED", {});
111540
+ }
111541
+ this.#registerHandlers();
111542
+ const cohortId = this.session.createCohort(config);
111543
+ let resolve;
111544
+ let reject;
111545
+ const completion = new Promise((res, rej) => {
111546
+ resolve = res;
111547
+ reject = rej;
111548
+ });
111549
+ const ctx = {
111550
+ cohortId,
111551
+ config,
111552
+ resolve,
111553
+ reject,
111554
+ completion,
111555
+ finalizing: false,
111556
+ settled: false
111557
+ };
111558
+ this.#contexts.set(cohortId, ctx);
111559
+ try {
111560
+ this.#startTimers(ctx);
111561
+ const advertMsgs = this.session.advertise(cohortId);
111562
+ this.#onPhaseMaybeChanged(ctx);
111563
+ this.emit("cohort-advertised", { cohortId });
111564
+ if (this.#advertRepeatIntervalMs > 0) {
111565
+ this.#startAdvertRepeat(ctx, advertMsgs);
111566
+ } else {
111567
+ this.#sendAll(advertMsgs).catch((err) => this.#failCohort(ctx, err));
111568
+ }
111569
+ } catch (err) {
111570
+ this.#failCohort(ctx, err);
111571
+ }
111572
+ return { cohortId, completion };
111573
+ }
111574
+ /**
111575
+ * Run a single cohort to completion using the `config` supplied in the
111576
+ * runner options. Thin convenience over {@link advertiseCohort} for the
111577
+ * single-cohort case (and the path {@link AggregationRunner.solo} rides).
111445
111578
  *
111446
111579
  * @returns {Promise<AggregationResult>} The final result with signature and signed tx.
111447
111580
  */
111448
111581
  run() {
111449
- return new Promise((resolve, reject) => {
111450
- this.#resolveRun = resolve;
111451
- this.#rejectRun = reject;
111452
- try {
111453
- this.#registerHandlers();
111454
- this.#cohortId = this.session.createCohort(this.#config);
111455
- this.#startTimers();
111456
- const advertMsgs = this.session.advertise(this.#cohortId);
111457
- this.#onPhaseMaybeChanged();
111458
- this.emit("cohort-advertised", { cohortId: this.#cohortId });
111459
- if (this.#advertRepeatIntervalMs > 0) {
111460
- this.#startAdvertRepeat(advertMsgs);
111461
- } else {
111462
- this.#sendAll(advertMsgs).catch((err) => this.#fail(err));
111463
- }
111464
- } catch (err) {
111465
- this.#fail(err);
111582
+ if (!this.#defaultConfig) {
111583
+ return Promise.reject(new AggregationServiceError(
111584
+ "run() requires `config` in the runner options; use advertiseCohort(config) to drive cohorts explicitly.",
111585
+ "MISSING_COHORT_CONFIG",
111586
+ {}
111587
+ ));
111588
+ }
111589
+ try {
111590
+ return this.advertiseCohort(this.#defaultConfig).completion;
111591
+ } catch (err) {
111592
+ return Promise.reject(err);
111593
+ }
111594
+ }
111595
+ /**
111596
+ * Wait for every currently-outstanding cohort to settle and return the
111597
+ * successful results. Dynamic drain: cohorts advertised while this is pending
111598
+ * are included, and it resolves only once no cohorts remain. Failed cohorts
111599
+ * are surfaced via `error` / `cohort-failed` events and their rejected
111600
+ * `completion` promises; they are omitted from the returned array (this
111601
+ * method does not throw). Bound long-running cohorts with `cohortTtlMs` /
111602
+ * `phaseTimeoutMs` or this may never resolve.
111603
+ *
111604
+ * @returns {Promise<AggregationResult[]>} Results of the cohorts that completed.
111605
+ */
111606
+ async runAll() {
111607
+ const collected = /* @__PURE__ */ new Map();
111608
+ const onComplete = (result) => {
111609
+ collected.set(result.cohortId, result);
111610
+ };
111611
+ this.on("signing-complete", onComplete);
111612
+ try {
111613
+ while (this.#contexts.size > 0) {
111614
+ await Promise.allSettled([...this.#contexts.values()].map((c2) => c2.completion));
111466
111615
  }
111467
- });
111616
+ } finally {
111617
+ this.off("signing-complete", onComplete);
111618
+ }
111619
+ return [...collected.values()];
111468
111620
  }
111469
111621
  /**
111470
- * Begin publishing the cohort advert immediately and on a repeating interval
111471
- * until {@link #stopAdvertRepeating} is called. Each advert is broadcast
111472
- * (no recipient) via the transport's `publishRepeating` primitive.
111622
+ * Begin publishing a cohort's advert immediately and on a repeating interval
111623
+ * until the cohort's advert loop is stopped. Each advert is broadcast (no
111624
+ * recipient) via the transport's `publishRepeating` primitive.
111473
111625
  */
111474
- #startAdvertRepeat(advertMsgs) {
111626
+ #startAdvertRepeat(ctx, advertMsgs) {
111475
111627
  const stops = [];
111476
111628
  for (const msg of advertMsgs) {
111477
111629
  stops.push(this.#transport.publishRepeating(msg, this.#did, this.#advertRepeatIntervalMs));
111478
111630
  }
111479
- this.#stopAdvertRepeat = () => {
111631
+ ctx.stopAdvertRepeat = () => {
111480
111632
  for (const stop2 of stops) {
111481
111633
  try {
111482
111634
  stop2();
@@ -111485,61 +111637,118 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111485
111637
  }
111486
111638
  };
111487
111639
  }
111488
- /** Stop the advert republish loop. Idempotent. */
111489
- #stopAdvertRepeating() {
111490
- if (!this.#stopAdvertRepeat) return;
111491
- const stop2 = this.#stopAdvertRepeat;
111492
- this.#stopAdvertRepeat = void 0;
111640
+ /** Stop a cohort's advert republish loop. Idempotent. */
111641
+ #stopAdvertRepeating(ctx) {
111642
+ if (!ctx.stopAdvertRepeat) return;
111643
+ const stop2 = ctx.stopAdvertRepeat;
111644
+ ctx.stopAdvertRepeat = void 0;
111493
111645
  stop2();
111494
111646
  }
111495
- /** Schedule cohort TTL + phase timeout at the start of a run. */
111496
- #startTimers() {
111647
+ /** Schedule a cohort's TTL + phase timeout when it is advertised. */
111648
+ #startTimers(ctx) {
111497
111649
  if (this.#cohortTtlMs !== void 0) {
111498
- this.#cohortTtlTimer = setTimeout(() => {
111499
- const reason = `Cohort ${this.#cohortId ?? ""} exceeded TTL of ${this.#cohortTtlMs}ms`;
111500
- this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
111501
- this.#fail(new Error(reason));
111650
+ ctx.cohortTtlTimer = setTimeout(() => {
111651
+ const reason = `Cohort ${ctx.cohortId} exceeded TTL of ${this.#cohortTtlMs}ms`;
111652
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
111653
+ this.#failCohort(ctx, new Error(reason));
111502
111654
  }, this.#cohortTtlMs);
111503
111655
  }
111504
- this.#resetPhaseTimer();
111656
+ this.#resetPhaseTimer(ctx);
111505
111657
  }
111506
- /** Reset the per-phase stall timer. Called when a phase transition is observed. */
111507
- #resetPhaseTimer() {
111508
- if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
111509
- this.#phaseTimer = void 0;
111658
+ /** Reset a cohort's per-phase stall timer. Called when a phase transition is observed. */
111659
+ #resetPhaseTimer(ctx) {
111660
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
111661
+ ctx.phaseTimer = void 0;
111510
111662
  if (this.#phaseTimeoutMs === void 0) return;
111511
- this.#phaseTimer = setTimeout(() => {
111512
- const reason = `Cohort ${this.#cohortId ?? ""} stalled in phase ${this.#lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
111513
- this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
111514
- this.#fail(new Error(reason));
111663
+ ctx.phaseTimer = setTimeout(() => {
111664
+ const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
111665
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
111666
+ this.#failCohort(ctx, new Error(reason));
111515
111667
  }, this.#phaseTimeoutMs);
111516
111668
  }
111517
- /** Detect a phase change since the last observation and reset the phase timer. */
111518
- #onPhaseMaybeChanged() {
111519
- if (!this.#cohortId) return;
111520
- const phase = this.session.getCohortPhase(this.#cohortId);
111521
- if (phase !== this.#lastObservedPhase) {
111522
- this.#lastObservedPhase = phase;
111523
- this.#resetPhaseTimer();
111669
+ /** Detect a phase change for a cohort since the last observation and reset its phase timer. */
111670
+ #onPhaseMaybeChanged(ctx) {
111671
+ const phase = this.session.getCohortPhase(ctx.cohortId);
111672
+ if (phase !== ctx.lastObservedPhase) {
111673
+ ctx.lastObservedPhase = phase;
111674
+ this.#resetPhaseTimer(ctx);
111524
111675
  }
111525
111676
  }
111526
- /** Clear both timers. Called on successful completion, stop(), and #fail. */
111527
- #clearTimers() {
111528
- if (this.#cohortTtlTimer) clearTimeout(this.#cohortTtlTimer);
111529
- if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
111530
- this.#cohortTtlTimer = void 0;
111531
- this.#phaseTimer = void 0;
111677
+ /** Clear a cohort's timers. Called on completion, stop, and failure. */
111678
+ #clearTimers(ctx) {
111679
+ if (ctx.cohortTtlTimer) clearTimeout(ctx.cohortTtlTimer);
111680
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
111681
+ ctx.cohortTtlTimer = void 0;
111682
+ ctx.phaseTimer = void 0;
111683
+ }
111684
+ /**
111685
+ * Reclaim one cohort's runner-layer bookkeeping: stop its advert loop, clear
111686
+ * its timers, and drop its {@link RunContext}. Does NOT touch sibling cohorts
111687
+ * and does NOT detach the shared transport handlers. Leaves the cohort in the
111688
+ * state machine; whether that cohort's `session` state is also removed is the
111689
+ * caller's choice (see {@link #completeCohort} vs {@link #failCohort}).
111690
+ */
111691
+ #disposeCohort(ctx) {
111692
+ this.#stopAdvertRepeating(ctx);
111693
+ this.#clearTimers(ctx);
111694
+ this.#contexts.delete(ctx.cohortId);
111695
+ }
111696
+ /**
111697
+ * Settle one cohort successfully. Reclaims the runner context but leaves the
111698
+ * completed cohort in `session` so callers can read its beaconAddress / cohort
111699
+ * via `session.getCohort(result.cohortId)`; reclaim it with
111700
+ * `session.removeCohort(cohortId)` when done. Idempotent via `ctx.settled`.
111701
+ */
111702
+ #completeCohort(ctx, result) {
111703
+ if (ctx.settled) return;
111704
+ ctx.settled = true;
111705
+ this.#disposeCohort(ctx);
111706
+ this.emit("signing-complete", result);
111707
+ ctx.resolve(result);
111708
+ }
111709
+ /**
111710
+ * Fail one cohort. Reclaims its runner context, drops its now-dead state from
111711
+ * the state machine, and rejects only its completion; siblings keep running
111712
+ * and the shared transport handlers stay registered. Idempotent via
111713
+ * `ctx.settled`.
111714
+ */
111715
+ #failCohort(ctx, err) {
111716
+ if (ctx.settled) return;
111717
+ ctx.settled = true;
111718
+ this.#disposeCohort(ctx);
111719
+ this.session.removeCohort(ctx.cohortId);
111720
+ this.emit("error", err);
111721
+ ctx.reject(err);
111722
+ }
111723
+ /**
111724
+ * Stop a single cohort early without affecting the rest of the runner. Drops
111725
+ * the cohort's state machine state; its `completion` promise rejects with a
111726
+ * stopped error.
111727
+ */
111728
+ stopCohort(cohortId) {
111729
+ const ctx = this.#contexts.get(cohortId);
111730
+ if (!ctx || ctx.settled) return;
111731
+ ctx.settled = true;
111732
+ this.#disposeCohort(ctx);
111733
+ this.session.removeCohort(cohortId);
111734
+ ctx.reject(new AggregationServiceError(`Cohort ${cohortId} stopped.`, "COHORT_STOPPED", { cohortId }));
111532
111735
  }
111533
111736
  /**
111534
- * Stop the runner early. Marks the runner stopped and detaches transport
111535
- * handlers so a restart or a new runner doesn't inherit stale dispatch.
111737
+ * Stop the whole runner. Fails every outstanding cohort, then detaches the
111738
+ * shared transport handlers so a restart or a new runner doesn't inherit
111739
+ * stale dispatch. Safe to call repeatedly.
111536
111740
  */
111537
111741
  stop() {
111538
111742
  this.#stopped = true;
111539
- this.#stopAdvertRepeating();
111540
- this.#clearTimers();
111743
+ for (const ctx of [...this.#contexts.values()]) {
111744
+ if (ctx.settled) continue;
111745
+ ctx.settled = true;
111746
+ this.#disposeCohort(ctx);
111747
+ this.session.removeCohort(ctx.cohortId);
111748
+ ctx.reject(new AggregationServiceError("Service runner stopped.", "RUNNER_STOPPED", { cohortId: ctx.cohortId }));
111749
+ }
111750
+ this.#contexts.clear();
111541
111751
  this.#unregisterHandlers();
111542
- if (this.#cohortId) this.session.removeCohort(this.#cohortId);
111543
111752
  }
111544
111753
  /** Message types this runner listens for on the transport. */
111545
111754
  static #HANDLED_MESSAGE_TYPES = [
@@ -111550,7 +111759,10 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111550
111759
  SIGNATURE_AUTHORIZATION
111551
111760
  ];
111552
111761
  /**
111553
- * Internal: handler registration with the transport. Idempotent.
111762
+ * Internal: handler registration with the transport. Idempotent. Handlers
111763
+ * are DID-scoped and cohort-agnostic — one registration serves every cohort
111764
+ * this runner drives; demux to the right {@link RunContext} happens in each
111765
+ * handler via the inbound message's cohortId.
111554
111766
  */
111555
111767
  #registerHandlers() {
111556
111768
  if (this.#handlersRegistered) return;
@@ -111571,23 +111783,25 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111571
111783
  }
111572
111784
  /**
111573
111785
  * Internal: message handlers for each protocol step. Each handler:
111574
- * 1) feeds the message into the state machine via session.receive()
111575
- * 2) emits a high-level event for external observers
111576
- * 3) checks if the new state triggers any automatic next steps, and if so:
111786
+ * 1) resolves the cohort the message belongs to (by cohortId); ignores it if unknown
111787
+ * 2) feeds the message into the state machine via session.receive()
111788
+ * 3) emits a high-level event (carrying cohortId) for external observers
111789
+ * 4) checks if the new state triggers any automatic next steps, and if so:
111577
111790
  * a) calls the appropriate decision callback(s)
111578
111791
  * b) sends any resulting messages from the state machine
111792
+ * Errors fail only the owning cohort. A stopped runner ignores messages.
111579
111793
  * @param {BaseMessage} msg - The incoming message to handle.
111580
111794
  * @returns {Promise<void>} Resolves when handling is complete.
111581
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111582
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111583
111795
  */
111584
111796
  async #handleOptIn(msg) {
111585
111797
  if (this.#stopped) return;
111798
+ const ctx = this.#contextFor(msg);
111799
+ if (!ctx) return;
111586
111800
  try {
111587
111801
  this.session.receive(msg);
111588
- this.#drainRejections();
111589
- this.#onPhaseMaybeChanged();
111590
- const optIn = this.session.pendingOptIns(this.#cohortId).get(msg.from);
111802
+ this.#drainRejections(ctx);
111803
+ this.#onPhaseMaybeChanged(ctx);
111804
+ const optIn = this.session.pendingOptIns(ctx.cohortId).get(msg.from);
111591
111805
  if (!optIn) return;
111592
111806
  this.emit("opt-in-received", optIn);
111593
111807
  if (optIn.communicationPk) {
@@ -111595,29 +111809,34 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111595
111809
  }
111596
111810
  const decision = await this.#onOptInReceived(optIn);
111597
111811
  if (!decision.accepted) return;
111598
- await this.#sendAll(this.session.acceptParticipant(this.#cohortId, msg.from));
111599
- this.emit("participant-accepted", { participantDid: msg.from });
111600
- const cohort = this.session.getCohort(this.#cohortId);
111601
- if (cohort.participants.length >= this.#config.minParticipants && !this.#finalizing) {
111602
- this.#finalizing = true;
111812
+ const maxParticipants = ctx.config.maxParticipants;
111813
+ const cohortNow = this.session.getCohort(ctx.cohortId);
111814
+ if (maxParticipants !== void 0 && cohortNow && cohortNow.participants.length >= maxParticipants) {
111815
+ return;
111816
+ }
111817
+ await this.#sendAll(this.session.acceptParticipant(ctx.cohortId, msg.from));
111818
+ this.emit("participant-accepted", { cohortId: ctx.cohortId, participantDid: msg.from });
111819
+ const cohort = this.session.getCohort(ctx.cohortId);
111820
+ if (cohort.participants.length >= ctx.config.minParticipants && !ctx.finalizing) {
111821
+ ctx.finalizing = true;
111603
111822
  const finalizeDecision = await this.#onReadyToFinalize({
111604
111823
  acceptedCount: cohort.participants.length,
111605
- minRequired: this.#config.minParticipants
111824
+ minRequired: ctx.config.minParticipants
111606
111825
  });
111607
111826
  if (!finalizeDecision.finalize) {
111608
- this.#finalizing = false;
111827
+ ctx.finalizing = false;
111609
111828
  return;
111610
111829
  }
111611
- const readyMsgs = this.session.finalizeKeygen(this.#cohortId);
111612
- this.#stopAdvertRepeating();
111830
+ const readyMsgs = this.session.finalizeKeygen(ctx.cohortId);
111831
+ this.#stopAdvertRepeating(ctx);
111613
111832
  this.emit("keygen-complete", {
111614
- cohortId: this.#cohortId,
111833
+ cohortId: ctx.cohortId,
111615
111834
  beaconAddress: cohort.beaconAddress
111616
111835
  });
111617
111836
  await this.#sendAll(readyMsgs);
111618
111837
  }
111619
111838
  } catch (err) {
111620
- this.#fail(err);
111839
+ this.#failCohort(ctx, err);
111621
111840
  }
111622
111841
  }
111623
111842
  /**
@@ -111625,23 +111844,23 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111625
111844
  * and distributes the data for validation.
111626
111845
  * @param {BaseMessage} msg - The incoming message to handle.
111627
111846
  * @returns {Promise<void>} Resolves when handling is complete.
111628
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111629
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111630
111847
  */
111631
111848
  async #handleSubmitUpdate(msg) {
111632
111849
  if (this.#stopped) return;
111850
+ const ctx = this.#contextFor(msg);
111851
+ if (!ctx) return;
111633
111852
  try {
111634
111853
  this.session.receive(msg);
111635
- this.#drainRejections();
111636
- this.#onPhaseMaybeChanged();
111637
- this.emit("update-received", { participantDid: msg.from });
111638
- if (this.session.getCohortPhase(this.#cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
111639
- const distributeMsgs = this.session.buildAndDistribute(this.#cohortId);
111640
- this.emit("data-distributed", { cohortId: this.#cohortId });
111854
+ this.#drainRejections(ctx);
111855
+ this.#onPhaseMaybeChanged(ctx);
111856
+ this.emit("update-received", { cohortId: ctx.cohortId, participantDid: msg.from });
111857
+ if (this.session.getCohortPhase(ctx.cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
111858
+ const distributeMsgs = this.session.buildAndDistribute(ctx.cohortId);
111859
+ this.emit("data-distributed", { cohortId: ctx.cohortId });
111641
111860
  await this.#sendAll(distributeMsgs);
111642
111861
  }
111643
111862
  } catch (err) {
111644
- this.#fail(err);
111863
+ this.#failCohort(ctx, err);
111645
111864
  }
111646
111865
  }
111647
111866
  /**
@@ -111649,111 +111868,95 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
111649
111868
  * automatically requests tx data and starts signing.
111650
111869
  * @param {BaseMessage} msg - The incoming message to handle.
111651
111870
  * @returns {Promise<void>} Resolves when handling is complete.
111652
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111653
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111654
111871
  */
111655
111872
  async #handleValidationAck(msg) {
111656
111873
  if (this.#stopped) return;
111874
+ const ctx = this.#contextFor(msg);
111875
+ if (!ctx) return;
111657
111876
  try {
111658
111877
  this.session.receive(msg);
111659
- this.#drainRejections();
111660
- this.#onPhaseMaybeChanged();
111878
+ this.#drainRejections(ctx);
111879
+ this.#onPhaseMaybeChanged(ctx);
111661
111880
  const approved = !!msg.body?.approved;
111662
- this.emit("validation-received", { participantDid: msg.from, approved });
111663
- const phase = this.session.getCohortPhase(this.#cohortId);
111881
+ this.emit("validation-received", { cohortId: ctx.cohortId, participantDid: msg.from, approved });
111882
+ const phase = this.session.getCohortPhase(ctx.cohortId);
111664
111883
  if (phase === "Failed" /* Failed */) {
111665
111884
  const reason = `Validation rejected by participant ${msg.from}`;
111666
- this.emit("cohort-failed", { cohortId: this.#cohortId, reason });
111667
- this.#fail(new Error(reason));
111885
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
111886
+ this.#failCohort(ctx, new Error(reason));
111668
111887
  return;
111669
111888
  }
111670
111889
  if (phase === "Validated" /* Validated */) {
111671
- const cohort = this.session.getCohort(this.#cohortId);
111890
+ const cohort = this.session.getCohort(ctx.cohortId);
111672
111891
  const txData = await this.#onProvideTxData({
111673
- cohortId: this.#cohortId,
111892
+ cohortId: ctx.cohortId,
111674
111893
  beaconAddress: cohort.beaconAddress,
111675
111894
  signalBytes: cohort.signalBytes
111676
111895
  });
111677
- const authMsgs = this.session.startSigning(this.#cohortId, txData);
111678
- const sessionId = this.session.getSigningSessionId(this.#cohortId) ?? "";
111679
- this.emit("signing-started", { sessionId });
111896
+ const authMsgs = this.session.startSigning(ctx.cohortId, txData);
111897
+ const sessionId = this.session.getSigningSessionId(ctx.cohortId) ?? "";
111898
+ this.emit("signing-started", { cohortId: ctx.cohortId, sessionId });
111680
111899
  await this.#sendAll(authMsgs);
111681
111900
  }
111682
111901
  } catch (err) {
111683
- this.#fail(err);
111902
+ this.#failCohort(ctx, err);
111684
111903
  }
111685
111904
  }
111686
111905
  /**
111687
- * Handler for receiving nonce contributions and signature authorizations. When all nonces or
111688
- * signatures are received,
111906
+ * Handler for receiving nonce contributions. When all nonces are received, sends the aggregated
111907
+ * nonce back to the cohort.
111689
111908
  * @param {BaseMessage} msg - The incoming message to handle.
111690
111909
  * @returns {Promise<void>} Resolves when handling is complete.
111691
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111692
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111693
111910
  */
111694
111911
  async #handleNonceContribution(msg) {
111695
111912
  if (this.#stopped) return;
111913
+ const ctx = this.#contextFor(msg);
111914
+ if (!ctx) return;
111696
111915
  try {
111697
111916
  this.session.receive(msg);
111698
- this.#drainRejections();
111699
- this.#onPhaseMaybeChanged();
111700
- this.emit("nonce-received", { participantDid: msg.from });
111701
- if (this.session.getCohortPhase(this.#cohortId) === "NoncesCollected" /* NoncesCollected */) {
111702
- await this.#sendAll(this.session.sendAggregatedNonce(this.#cohortId));
111917
+ this.#drainRejections(ctx);
111918
+ this.#onPhaseMaybeChanged(ctx);
111919
+ this.emit("nonce-received", { cohortId: ctx.cohortId, participantDid: msg.from });
111920
+ if (this.session.getCohortPhase(ctx.cohortId) === "NoncesCollected" /* NoncesCollected */) {
111921
+ await this.#sendAll(this.session.sendAggregatedNonce(ctx.cohortId));
111703
111922
  }
111704
111923
  } catch (err) {
111705
- this.#fail(err);
111924
+ this.#failCohort(ctx, err);
111706
111925
  }
111707
111926
  }
111708
111927
  /**
111709
111928
  * Handler for receiving signature authorizations. When all partial signatures are received, the
111710
- * session automatically completes and the final result is emitted and the run() promise is resolved.
111929
+ * session automatically completes; the final result is emitted and the cohort's completion
111930
+ * promise resolves.
111711
111931
  * @param {BaseMessage} msg - The incoming message to handle.
111712
111932
  * @returns {Promise<void>} Resolves when handling is complete.
111713
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111714
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111715
111933
  */
111716
111934
  async #handleSignatureAuthorization(msg) {
111717
111935
  if (this.#stopped) return;
111936
+ const ctx = this.#contextFor(msg);
111937
+ if (!ctx) return;
111718
111938
  try {
111719
111939
  this.session.receive(msg);
111720
- this.#drainRejections();
111721
- this.#onPhaseMaybeChanged();
111722
- const result = this.session.getResult(this.#cohortId);
111940
+ this.#drainRejections(ctx);
111941
+ this.#onPhaseMaybeChanged(ctx);
111942
+ const result = this.session.getResult(ctx.cohortId);
111723
111943
  if (result) {
111724
- this.#clearTimers();
111725
- this.#unregisterHandlers();
111726
- this.emit("signing-complete", result);
111727
- this.#resolveRun?.(result);
111944
+ this.#completeCohort(ctx, result);
111728
111945
  }
111729
111946
  } catch (err) {
111730
- this.#fail(err);
111947
+ this.#failCohort(ctx, err);
111731
111948
  }
111732
111949
  }
111733
111950
  /**
111734
111951
  * Internal: helper to send all messages sequentially. Catches and propagates errors.
111735
111952
  * @param {BaseMessage[]} msgs - The messages to send.
111736
111953
  * @returns {Promise<void>} Resolves when all messages have been sent.
111737
- * @throws {Error} If sending any message fails, the error is emitted and the run promise is
111738
- * rejected.
111739
111954
  */
111740
111955
  async #sendAll(msgs) {
111741
111956
  for (const m2 of msgs) {
111742
111957
  await this.#transport.sendMessage(m2, this.#did, m2.to);
111743
111958
  }
111744
111959
  }
111745
- /**
111746
- * Internal: helper to handle errors. Emits an 'error' event and rejects the run promise.
111747
- * @param {Error} err - The error to handle.
111748
- */
111749
- #fail(err) {
111750
- this.#stopAdvertRepeating();
111751
- this.#clearTimers();
111752
- this.#unregisterHandlers();
111753
- if (this.#cohortId) this.session.removeCohort(this.#cohortId);
111754
- this.emit("error", err);
111755
- this.#rejectRun?.(err);
111756
- }
111757
111960
  };
111758
111961
 
111759
111962
  // src/core/aggregation/runner/participant-runner.ts
@@ -111814,7 +112017,8 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
111814
112017
  }
111815
112018
  /**
111816
112019
  * Single-shot helper: start, join the first cohort that passes `shouldJoin`,
111817
- * drive it to completion, and resolve. Convenient for tests and demos.
112020
+ * drive it to completion, and resolve. Convenient for tests and demos. The
112021
+ * single-cohort special case of {@link joinMatching} (count = 1).
111818
112022
  */
111819
112023
  static async joinFirst(options2) {
111820
112024
  return new Promise((resolve, reject) => {
@@ -111827,6 +112031,37 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
111827
112031
  runner.start().catch(reject);
111828
112032
  });
111829
112033
  }
112034
+ /**
112035
+ * Multi-cohort helper: start, join EVERY cohort whose advert passes
112036
+ * `shouldJoin`, drive each to completion in parallel, and resolve once
112037
+ * `count` cohorts have completed (the runner stops at that point). The
112038
+ * N-cohort generalization of {@link joinFirst}, for a participant that joins
112039
+ * several cohorts advertised by one service.
112040
+ *
112041
+ * For an open-ended, long-lived subscriber (no fixed count), construct an
112042
+ * {@link AggregationParticipantRunner} directly, set `shouldJoin`, call
112043
+ * `start()`, and listen for `cohort-complete` — the runner already drives
112044
+ * any number of cohorts concurrently.
112045
+ *
112046
+ * @param options Participant runner options (set `shouldJoin` to select cohorts).
112047
+ * @param count Number of completed cohorts to collect before resolving.
112048
+ * @returns The {@link CohortCompleteInfo} for each completed cohort, in completion order.
112049
+ */
112050
+ static async joinMatching(options2, count) {
112051
+ return new Promise((resolve, reject) => {
112052
+ const runner = new _AggregationParticipantRunner(options2);
112053
+ const completed = [];
112054
+ runner.on("cohort-complete", (info) => {
112055
+ completed.push(info);
112056
+ if (completed.length >= count) {
112057
+ runner.stop();
112058
+ resolve(completed);
112059
+ }
112060
+ });
112061
+ runner.on("error", reject);
112062
+ runner.start().catch(reject);
112063
+ });
112064
+ }
111830
112065
  /**
111831
112066
  * Internal: handler registration with the transport. Idempotent and safe to call multiple times,
111832
112067
  * but only registers handlers once.
@@ -111975,7 +112210,7 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
111975
112210
  if (this.session.getCohortPhase(cohortId) === "Complete" /* Complete */) {
111976
112211
  const info = this.session.joinedCohorts.get(cohortId);
111977
112212
  if (info) {
111978
- const validation = this.session.pendingValidations.get(cohortId);
112213
+ const validation = this.session.getValidation(cohortId);
111979
112214
  this.emit("cohort-complete", {
111980
112215
  cohortId,
111981
112216
  beaconAddress: info.beaconAddress,
@@ -129101,6 +129336,7 @@ export {
129101
129336
  InMemoryRateLimitStore,
129102
129337
  InMemoryTransport,
129103
129338
  InboxBuffer,
129339
+ KNOWN_BEACON_TYPES,
129104
129340
  KeyPairAggregationSigner,
129105
129341
  NONCE_CONTRIBUTION,
129106
129342
  NonceCache,
@@ -129173,6 +129409,7 @@ export {
129173
129409
  registerBeaconStrategy,
129174
129410
  reviveFromWire,
129175
129411
  signEnvelope,
129412
+ validateCohortConditions,
129176
129413
  verifyEnvelope,
129177
129414
  verifyRequestAuth
129178
129415
  };