@did-btcr2/aggregation 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +18107 -18096
- package/dist/browser.mjs +18107 -18096
- package/dist/cjs/core/index.js +2423 -0
- package/dist/cjs/index.js +3513 -3521
- package/dist/cjs/participant/index.js +2425 -0
- package/dist/cjs/service/index.js +3199 -0
- package/dist/esm/{runner/aggregation-runner.js → aggregation-runner.js} +4 -4
- package/dist/esm/aggregation-runner.js.map +1 -0
- package/dist/esm/core/beacon-strategy.js.map +1 -0
- package/dist/esm/core/cohort.js.map +1 -0
- package/dist/esm/core/conditions.js.map +1 -0
- package/dist/esm/core/errors.js.map +1 -0
- package/dist/esm/core/fallback-spend.js.map +1 -0
- package/dist/esm/core/index.js +23 -0
- package/dist/esm/core/index.js.map +1 -0
- package/dist/esm/core/logger.js.map +1 -0
- package/dist/esm/core/messages/base.js.map +1 -0
- package/dist/esm/core/messages/bodies.js.map +1 -0
- package/dist/esm/core/messages/constants.js.map +1 -0
- package/dist/esm/core/messages/factories.js.map +1 -0
- package/dist/esm/core/messages/guards.js.map +1 -0
- package/dist/esm/core/messages/index.js.map +1 -0
- package/dist/esm/core/phases.js.map +1 -0
- package/dist/esm/core/recovery-policy.js.map +1 -0
- package/dist/esm/core/recovery-spend.js.map +1 -0
- package/dist/esm/core/signer.js.map +1 -0
- package/dist/esm/core/signing-session.js.map +1 -0
- package/dist/esm/core/transport/didcomm.js.map +1 -0
- package/dist/esm/core/transport/error.js.map +1 -0
- package/dist/esm/core/transport/http/envelope.js.map +1 -0
- package/dist/esm/core/transport/http/errors.js.map +1 -0
- package/dist/esm/core/transport/http/index.js +9 -0
- package/dist/esm/core/transport/http/index.js.map +1 -0
- package/dist/esm/core/transport/http/protocol.js.map +1 -0
- package/dist/esm/core/transport/http/request-auth.js.map +1 -0
- package/dist/esm/core/transport/in-memory.js.map +1 -0
- package/dist/esm/core/transport/index.js +12 -0
- package/dist/esm/core/transport/index.js.map +1 -0
- package/dist/esm/core/transport/nostr.js.map +1 -0
- package/dist/esm/core/transport/transport.js.map +1 -0
- package/dist/esm/core/typed-emitter.js.map +1 -0
- package/dist/esm/index.js +13 -20
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/participant/events.js.map +1 -0
- package/dist/esm/{transport/http/client.js → participant/http-client.js} +6 -6
- package/dist/esm/participant/http-client.js.map +1 -0
- package/dist/esm/participant/index.js +13 -0
- package/dist/esm/participant/index.js.map +1 -0
- package/dist/esm/{runner → participant}/participant-runner.js +5 -5
- package/dist/esm/participant/participant-runner.js.map +1 -0
- package/dist/esm/{participant.js → participant/participant.js} +11 -11
- package/dist/esm/participant/participant.js.map +1 -0
- package/dist/esm/participant/sse-stream.js.map +1 -0
- package/dist/esm/service/events.js +2 -0
- package/dist/esm/{runner → service}/events.js.map +1 -1
- package/dist/esm/{transport/http/server.js → service/http-server.js} +6 -6
- package/dist/esm/service/http-server.js.map +1 -0
- package/dist/esm/service/inbox-buffer.js.map +1 -0
- package/dist/esm/service/index.js +16 -0
- package/dist/esm/service/index.js.map +1 -0
- package/dist/esm/service/nonce-cache.js.map +1 -0
- package/dist/esm/service/rate-limiter.js.map +1 -0
- package/dist/esm/{runner → service}/service-runner.js +5 -5
- package/dist/esm/service/service-runner.js.map +1 -0
- package/dist/esm/{service.js → service/service.js} +12 -12
- package/dist/esm/service/service.js.map +1 -0
- package/dist/esm/service/sse-writer.js.map +1 -0
- package/dist/esm/{transport/factory.js → transport-factory.js} +5 -5
- package/dist/esm/transport-factory.js.map +1 -0
- package/dist/types/{runner/aggregation-runner.d.ts → aggregation-runner.d.ts} +3 -3
- package/dist/types/aggregation-runner.d.ts.map +1 -0
- package/dist/types/{beacon-strategy.d.ts → core/beacon-strategy.d.ts} +2 -2
- package/dist/types/core/beacon-strategy.d.ts.map +1 -0
- package/dist/types/{cohort.d.ts → core/cohort.d.ts} +3 -3
- package/dist/types/core/cohort.d.ts.map +1 -0
- package/dist/types/core/conditions.d.ts.map +1 -0
- package/dist/types/core/errors.d.ts.map +1 -0
- package/dist/types/core/fallback-spend.d.ts.map +1 -0
- package/dist/types/core/index.d.ts +15 -0
- package/dist/types/core/index.d.ts.map +1 -0
- package/dist/types/core/logger.d.ts.map +1 -0
- package/dist/types/core/messages/base.d.ts.map +1 -0
- package/dist/types/core/messages/bodies.d.ts.map +1 -0
- package/dist/types/core/messages/constants.d.ts.map +1 -0
- package/dist/types/core/messages/factories.d.ts.map +1 -0
- package/dist/types/core/messages/guards.d.ts.map +1 -0
- package/dist/types/core/messages/index.d.ts.map +1 -0
- package/dist/types/core/phases.d.ts.map +1 -0
- package/dist/types/core/recovery-policy.d.ts.map +1 -0
- package/dist/types/core/recovery-spend.d.ts.map +1 -0
- package/dist/types/core/signer.d.ts.map +1 -0
- package/dist/types/core/signing-session.d.ts.map +1 -0
- package/dist/types/core/transport/didcomm.d.ts.map +1 -0
- package/dist/types/core/transport/error.d.ts.map +1 -0
- package/dist/types/core/transport/http/envelope.d.ts.map +1 -0
- package/dist/types/core/transport/http/errors.d.ts.map +1 -0
- package/dist/types/core/transport/http/index.d.ts +5 -0
- package/dist/types/core/transport/http/index.d.ts.map +1 -0
- package/dist/types/core/transport/http/protocol.d.ts.map +1 -0
- package/dist/types/core/transport/http/request-auth.d.ts.map +1 -0
- package/dist/types/core/transport/in-memory.d.ts.map +1 -0
- package/{src/transport/index.ts → dist/types/core/transport/index.d.ts} +1 -1
- package/dist/types/core/transport/index.d.ts.map +1 -0
- package/dist/types/core/transport/nostr.d.ts.map +1 -0
- package/dist/types/core/transport/transport.d.ts.map +1 -0
- package/dist/types/core/typed-emitter.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -16
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/{runner → participant}/events.d.ts +1 -76
- package/dist/types/participant/events.d.ts.map +1 -0
- package/dist/types/{transport/http/client.d.ts → participant/http-client.d.ts} +4 -4
- package/dist/types/participant/http-client.d.ts.map +1 -0
- package/dist/types/participant/index.d.ts +6 -0
- package/dist/types/participant/index.d.ts.map +1 -0
- package/dist/types/{runner → participant}/participant-runner.d.ts +6 -6
- package/dist/types/participant/participant-runner.d.ts.map +1 -0
- package/dist/types/{participant.d.ts → participant/participant.d.ts} +6 -6
- package/dist/types/participant/participant.d.ts.map +1 -0
- package/dist/types/participant/sse-stream.d.ts.map +1 -0
- package/dist/types/service/events.d.ts +76 -0
- package/dist/types/service/events.d.ts.map +1 -0
- package/dist/types/{transport/http/server.d.ts → service/http-server.d.ts} +4 -4
- package/dist/types/{transport/http/server.d.ts.map → service/http-server.d.ts.map} +1 -1
- package/dist/types/service/inbox-buffer.d.ts.map +1 -0
- package/dist/types/service/index.d.ts +9 -0
- package/dist/types/service/index.d.ts.map +1 -0
- package/dist/types/service/nonce-cache.d.ts.map +1 -0
- package/dist/types/service/rate-limiter.d.ts.map +1 -0
- package/dist/types/{runner → service}/service-runner.d.ts +4 -4
- package/dist/types/service/service-runner.d.ts.map +1 -0
- package/dist/types/{service.d.ts → service/service.d.ts} +6 -6
- package/dist/types/service/service.d.ts.map +1 -0
- package/dist/types/service/sse-writer.d.ts.map +1 -0
- package/dist/types/{transport/factory.d.ts → transport-factory.d.ts} +5 -5
- package/dist/types/transport-factory.d.ts.map +1 -0
- package/package.json +23 -6
- package/src/{runner/aggregation-runner.ts → aggregation-runner.ts} +7 -7
- package/src/{beacon-strategy.ts → core/beacon-strategy.ts} +2 -2
- package/src/{cohort.ts → core/cohort.ts} +3 -3
- package/src/core/index.ts +22 -0
- package/src/core/transport/http/index.ts +8 -0
- package/src/core/transport/index.ts +11 -0
- package/src/index.ts +13 -23
- package/src/{runner → participant}/events.ts +1 -59
- package/src/{transport/http/client.ts → participant/http-client.ts} +8 -8
- package/src/participant/index.ts +12 -0
- package/src/{runner → participant}/participant-runner.ts +10 -10
- package/src/{participant.ts → participant/participant.ts} +18 -18
- package/src/service/events.ts +58 -0
- package/src/{transport/http/server.ts → service/http-server.ts} +8 -8
- package/src/service/index.ts +15 -0
- package/src/{runner → service}/service-runner.ts +9 -9
- package/src/{service.ts → service/service.ts} +19 -19
- package/src/{transport/factory.ts → transport-factory.ts} +8 -8
- package/dist/esm/beacon-strategy.js.map +0 -1
- package/dist/esm/cohort.js.map +0 -1
- package/dist/esm/conditions.js.map +0 -1
- package/dist/esm/errors.js.map +0 -1
- package/dist/esm/fallback-spend.js.map +0 -1
- package/dist/esm/logger.js.map +0 -1
- package/dist/esm/messages/base.js.map +0 -1
- package/dist/esm/messages/bodies.js.map +0 -1
- package/dist/esm/messages/constants.js.map +0 -1
- package/dist/esm/messages/factories.js.map +0 -1
- package/dist/esm/messages/guards.js.map +0 -1
- package/dist/esm/messages/index.js.map +0 -1
- package/dist/esm/participant.js.map +0 -1
- package/dist/esm/phases.js.map +0 -1
- package/dist/esm/recovery-policy.js.map +0 -1
- package/dist/esm/recovery-spend.js.map +0 -1
- package/dist/esm/runner/aggregation-runner.js.map +0 -1
- package/dist/esm/runner/index.js +0 -6
- package/dist/esm/runner/index.js.map +0 -1
- package/dist/esm/runner/participant-runner.js.map +0 -1
- package/dist/esm/runner/service-runner.js.map +0 -1
- package/dist/esm/runner/typed-emitter.js.map +0 -1
- package/dist/esm/service.js.map +0 -1
- package/dist/esm/signer.js.map +0 -1
- package/dist/esm/signing-session.js.map +0 -1
- package/dist/esm/transport/didcomm.js.map +0 -1
- package/dist/esm/transport/error.js.map +0 -1
- package/dist/esm/transport/factory.js.map +0 -1
- package/dist/esm/transport/http/client.js.map +0 -1
- package/dist/esm/transport/http/envelope.js.map +0 -1
- package/dist/esm/transport/http/errors.js.map +0 -1
- package/dist/esm/transport/http/inbox-buffer.js.map +0 -1
- package/dist/esm/transport/http/index.js +0 -12
- package/dist/esm/transport/http/index.js.map +0 -1
- package/dist/esm/transport/http/nonce-cache.js.map +0 -1
- package/dist/esm/transport/http/protocol.js.map +0 -1
- package/dist/esm/transport/http/rate-limiter.js.map +0 -1
- package/dist/esm/transport/http/request-auth.js.map +0 -1
- package/dist/esm/transport/http/server.js.map +0 -1
- package/dist/esm/transport/http/sse-stream.js.map +0 -1
- package/dist/esm/transport/http/sse-writer.js.map +0 -1
- package/dist/esm/transport/in-memory.js.map +0 -1
- package/dist/esm/transport/index.js +0 -8
- package/dist/esm/transport/index.js.map +0 -1
- package/dist/esm/transport/nostr.js.map +0 -1
- package/dist/esm/transport/transport.js.map +0 -1
- package/dist/types/beacon-strategy.d.ts.map +0 -1
- package/dist/types/cohort.d.ts.map +0 -1
- package/dist/types/conditions.d.ts.map +0 -1
- package/dist/types/errors.d.ts.map +0 -1
- package/dist/types/fallback-spend.d.ts.map +0 -1
- package/dist/types/logger.d.ts.map +0 -1
- package/dist/types/messages/base.d.ts.map +0 -1
- package/dist/types/messages/bodies.d.ts.map +0 -1
- package/dist/types/messages/constants.d.ts.map +0 -1
- package/dist/types/messages/factories.d.ts.map +0 -1
- package/dist/types/messages/guards.d.ts.map +0 -1
- package/dist/types/messages/index.d.ts.map +0 -1
- package/dist/types/participant.d.ts.map +0 -1
- package/dist/types/phases.d.ts.map +0 -1
- package/dist/types/recovery-policy.d.ts.map +0 -1
- package/dist/types/recovery-spend.d.ts.map +0 -1
- package/dist/types/runner/aggregation-runner.d.ts.map +0 -1
- package/dist/types/runner/events.d.ts.map +0 -1
- package/dist/types/runner/index.d.ts +0 -6
- package/dist/types/runner/index.d.ts.map +0 -1
- package/dist/types/runner/participant-runner.d.ts.map +0 -1
- package/dist/types/runner/service-runner.d.ts.map +0 -1
- package/dist/types/runner/typed-emitter.d.ts.map +0 -1
- package/dist/types/service.d.ts.map +0 -1
- package/dist/types/signer.d.ts.map +0 -1
- package/dist/types/signing-session.d.ts.map +0 -1
- package/dist/types/transport/didcomm.d.ts.map +0 -1
- package/dist/types/transport/error.d.ts.map +0 -1
- package/dist/types/transport/factory.d.ts.map +0 -1
- package/dist/types/transport/http/client.d.ts.map +0 -1
- package/dist/types/transport/http/envelope.d.ts.map +0 -1
- package/dist/types/transport/http/errors.d.ts.map +0 -1
- package/dist/types/transport/http/inbox-buffer.d.ts.map +0 -1
- package/dist/types/transport/http/index.d.ts +0 -12
- package/dist/types/transport/http/index.d.ts.map +0 -1
- package/dist/types/transport/http/nonce-cache.d.ts.map +0 -1
- package/dist/types/transport/http/protocol.d.ts.map +0 -1
- package/dist/types/transport/http/rate-limiter.d.ts.map +0 -1
- package/dist/types/transport/http/request-auth.d.ts.map +0 -1
- package/dist/types/transport/http/sse-stream.d.ts.map +0 -1
- package/dist/types/transport/http/sse-writer.d.ts.map +0 -1
- package/dist/types/transport/in-memory.d.ts.map +0 -1
- package/dist/types/transport/index.d.ts +0 -8
- package/dist/types/transport/index.d.ts.map +0 -1
- package/dist/types/transport/nostr.d.ts.map +0 -1
- package/dist/types/transport/transport.d.ts.map +0 -1
- package/src/runner/index.ts +0 -5
- package/src/transport/http/index.ts +0 -11
- /package/dist/esm/{beacon-strategy.js → core/beacon-strategy.js} +0 -0
- /package/dist/esm/{cohort.js → core/cohort.js} +0 -0
- /package/dist/esm/{conditions.js → core/conditions.js} +0 -0
- /package/dist/esm/{errors.js → core/errors.js} +0 -0
- /package/dist/esm/{fallback-spend.js → core/fallback-spend.js} +0 -0
- /package/dist/esm/{logger.js → core/logger.js} +0 -0
- /package/dist/esm/{messages → core/messages}/base.js +0 -0
- /package/dist/esm/{messages → core/messages}/bodies.js +0 -0
- /package/dist/esm/{messages → core/messages}/constants.js +0 -0
- /package/dist/esm/{messages → core/messages}/factories.js +0 -0
- /package/dist/esm/{messages → core/messages}/guards.js +0 -0
- /package/dist/esm/{messages → core/messages}/index.js +0 -0
- /package/dist/esm/{phases.js → core/phases.js} +0 -0
- /package/dist/esm/{recovery-policy.js → core/recovery-policy.js} +0 -0
- /package/dist/esm/{recovery-spend.js → core/recovery-spend.js} +0 -0
- /package/dist/esm/{signer.js → core/signer.js} +0 -0
- /package/dist/esm/{signing-session.js → core/signing-session.js} +0 -0
- /package/dist/esm/{transport → core/transport}/didcomm.js +0 -0
- /package/dist/esm/{transport → core/transport}/error.js +0 -0
- /package/dist/esm/{transport → core/transport}/http/envelope.js +0 -0
- /package/dist/esm/{transport → core/transport}/http/errors.js +0 -0
- /package/dist/esm/{transport → core/transport}/http/protocol.js +0 -0
- /package/dist/esm/{transport → core/transport}/http/request-auth.js +0 -0
- /package/dist/esm/{transport → core/transport}/in-memory.js +0 -0
- /package/dist/esm/{transport → core/transport}/nostr.js +0 -0
- /package/dist/esm/{transport → core/transport}/transport.js +0 -0
- /package/dist/esm/{runner → core}/typed-emitter.js +0 -0
- /package/dist/esm/{runner → participant}/events.js +0 -0
- /package/dist/esm/{transport/http → participant}/sse-stream.js +0 -0
- /package/dist/esm/{transport/http → service}/inbox-buffer.js +0 -0
- /package/dist/esm/{transport/http → service}/nonce-cache.js +0 -0
- /package/dist/esm/{transport/http → service}/rate-limiter.js +0 -0
- /package/dist/esm/{transport/http → service}/sse-writer.js +0 -0
- /package/dist/types/{conditions.d.ts → core/conditions.d.ts} +0 -0
- /package/dist/types/{errors.d.ts → core/errors.d.ts} +0 -0
- /package/dist/types/{fallback-spend.d.ts → core/fallback-spend.d.ts} +0 -0
- /package/dist/types/{logger.d.ts → core/logger.d.ts} +0 -0
- /package/dist/types/{messages → core/messages}/base.d.ts +0 -0
- /package/dist/types/{messages → core/messages}/bodies.d.ts +0 -0
- /package/dist/types/{messages → core/messages}/constants.d.ts +0 -0
- /package/dist/types/{messages → core/messages}/factories.d.ts +0 -0
- /package/dist/types/{messages → core/messages}/guards.d.ts +0 -0
- /package/dist/types/{messages → core/messages}/index.d.ts +0 -0
- /package/dist/types/{phases.d.ts → core/phases.d.ts} +0 -0
- /package/dist/types/{recovery-policy.d.ts → core/recovery-policy.d.ts} +0 -0
- /package/dist/types/{recovery-spend.d.ts → core/recovery-spend.d.ts} +0 -0
- /package/dist/types/{signer.d.ts → core/signer.d.ts} +0 -0
- /package/dist/types/{signing-session.d.ts → core/signing-session.d.ts} +0 -0
- /package/dist/types/{transport → core/transport}/didcomm.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/error.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/http/envelope.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/http/errors.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/http/protocol.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/http/request-auth.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/in-memory.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/nostr.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/transport.d.ts +0 -0
- /package/dist/types/{runner → core}/typed-emitter.d.ts +0 -0
- /package/dist/types/{transport/http → participant}/sse-stream.d.ts +0 -0
- /package/dist/types/{transport/http → service}/inbox-buffer.d.ts +0 -0
- /package/dist/types/{transport/http → service}/nonce-cache.d.ts +0 -0
- /package/dist/types/{transport/http → service}/rate-limiter.d.ts +0 -0
- /package/dist/types/{transport/http → service}/sse-writer.d.ts +0 -0
- /package/src/{conditions.ts → core/conditions.ts} +0 -0
- /package/src/{errors.ts → core/errors.ts} +0 -0
- /package/src/{fallback-spend.ts → core/fallback-spend.ts} +0 -0
- /package/src/{logger.ts → core/logger.ts} +0 -0
- /package/src/{messages → core/messages}/base.ts +0 -0
- /package/src/{messages → core/messages}/bodies.ts +0 -0
- /package/src/{messages → core/messages}/constants.ts +0 -0
- /package/src/{messages → core/messages}/factories.ts +0 -0
- /package/src/{messages → core/messages}/guards.ts +0 -0
- /package/src/{messages → core/messages}/index.ts +0 -0
- /package/src/{phases.ts → core/phases.ts} +0 -0
- /package/src/{recovery-policy.ts → core/recovery-policy.ts} +0 -0
- /package/src/{recovery-spend.ts → core/recovery-spend.ts} +0 -0
- /package/src/{signer.ts → core/signer.ts} +0 -0
- /package/src/{signing-session.ts → core/signing-session.ts} +0 -0
- /package/src/{transport → core/transport}/didcomm.ts +0 -0
- /package/src/{transport → core/transport}/error.ts +0 -0
- /package/src/{transport → core/transport}/http/envelope.ts +0 -0
- /package/src/{transport → core/transport}/http/errors.ts +0 -0
- /package/src/{transport → core/transport}/http/protocol.ts +0 -0
- /package/src/{transport → core/transport}/http/request-auth.ts +0 -0
- /package/src/{transport → core/transport}/in-memory.ts +0 -0
- /package/src/{transport → core/transport}/nostr.ts +0 -0
- /package/src/{transport → core/transport}/transport.ts +0 -0
- /package/src/{runner → core}/typed-emitter.ts +0 -0
- /package/src/{transport/http → participant}/sse-stream.ts +0 -0
- /package/src/{transport/http → service}/inbox-buffer.ts +0 -0
- /package/src/{transport/http → service}/nonce-cache.ts +0 -0
- /package/src/{transport/http → service}/rate-limiter.ts +0 -0
- /package/src/{transport/http → service}/sse-writer.ts +0 -0
|
@@ -0,0 +1,2425 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/participant/index.ts
|
|
31
|
+
var participant_exports = {};
|
|
32
|
+
__export(participant_exports, {
|
|
33
|
+
AggregationParticipant: () => AggregationParticipant,
|
|
34
|
+
AggregationParticipantRunner: () => AggregationParticipantRunner,
|
|
35
|
+
HttpClientTransport: () => HttpClientTransport,
|
|
36
|
+
defaultReconnectBackoff: () => defaultReconnectBackoff,
|
|
37
|
+
parseSseStream: () => parseSseStream
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(participant_exports);
|
|
40
|
+
|
|
41
|
+
// src/participant/participant.ts
|
|
42
|
+
var import_common4 = require("@did-btcr2/common");
|
|
43
|
+
var import_secp256k14 = require("@noble/curves/secp256k1.js");
|
|
44
|
+
var import_utils3 = require("@noble/hashes/utils");
|
|
45
|
+
var import_btc_signer5 = require("@scure/btc-signer");
|
|
46
|
+
|
|
47
|
+
// src/core/beacon-strategy.ts
|
|
48
|
+
var import_common = require("@did-btcr2/common");
|
|
49
|
+
var import_smt = require("@did-btcr2/smt");
|
|
50
|
+
var CAS_STRATEGY = {
|
|
51
|
+
type: "CASBeacon",
|
|
52
|
+
buildAggregatedData(cohort) {
|
|
53
|
+
cohort.buildCASAnnouncement();
|
|
54
|
+
},
|
|
55
|
+
getDistributePayload(cohort) {
|
|
56
|
+
return { casAnnouncement: cohort.casAnnouncement };
|
|
57
|
+
},
|
|
58
|
+
validateParticipantView({ participantDid, included, expectedHash, body }) {
|
|
59
|
+
const casAnnouncement = body.casAnnouncement;
|
|
60
|
+
if (!casAnnouncement) return { matches: false };
|
|
61
|
+
if (!included) {
|
|
62
|
+
return { matches: !(participantDid in casAnnouncement), casAnnouncement };
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
matches: casAnnouncement[participantDid] === expectedHash,
|
|
66
|
+
casAnnouncement
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var SMT_STRATEGY = {
|
|
71
|
+
type: "SMTBeacon",
|
|
72
|
+
buildAggregatedData(cohort) {
|
|
73
|
+
cohort.buildSMTTree();
|
|
74
|
+
},
|
|
75
|
+
getDistributePayload(cohort, participantDid) {
|
|
76
|
+
const proof = cohort.smtProofs?.get(participantDid);
|
|
77
|
+
return { smtProof: proof };
|
|
78
|
+
},
|
|
79
|
+
validateParticipantView({ participantDid, included, submittedUpdate, body }) {
|
|
80
|
+
const smtProof = body.smtProof;
|
|
81
|
+
const index = (0, import_smt.didToIndex)(participantDid);
|
|
82
|
+
if (!included) {
|
|
83
|
+
if (!smtProof?.nonce || smtProof?.updateId) return { matches: false, smtProof };
|
|
84
|
+
const candidateHash2 = (0, import_smt.blockHash)((0, import_smt.blockHash)((0, import_smt.base64UrlToHash)(smtProof.nonce)));
|
|
85
|
+
return { matches: (0, import_smt.verifySerializedProof)(smtProof, index, candidateHash2), smtProof };
|
|
86
|
+
}
|
|
87
|
+
if (!smtProof?.updateId || !smtProof?.nonce || !submittedUpdate) return { matches: false, smtProof };
|
|
88
|
+
const canonicalBytes = new TextEncoder().encode((0, import_common.canonicalize)(submittedUpdate));
|
|
89
|
+
const expectedUpdateId = (0, import_smt.hashToBase64Url)((0, import_smt.blockHash)(canonicalBytes));
|
|
90
|
+
if (smtProof.updateId !== expectedUpdateId) {
|
|
91
|
+
return { matches: false, smtProof };
|
|
92
|
+
}
|
|
93
|
+
const candidateHash = (0, import_smt.blockHash)((0, import_smt.blockHash)((0, import_smt.base64UrlToHash)(smtProof.nonce)), (0, import_smt.base64UrlToHash)(smtProof.updateId));
|
|
94
|
+
return {
|
|
95
|
+
matches: (0, import_smt.verifySerializedProof)(smtProof, index, candidateHash),
|
|
96
|
+
smtProof
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
var STRATEGIES = /* @__PURE__ */ new Map([
|
|
101
|
+
[CAS_STRATEGY.type, CAS_STRATEGY],
|
|
102
|
+
[SMT_STRATEGY.type, SMT_STRATEGY]
|
|
103
|
+
]);
|
|
104
|
+
function getBeaconStrategy(type) {
|
|
105
|
+
return STRATEGIES.get(type);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/core/cohort.ts
|
|
109
|
+
var import_bitcoin = require("@did-btcr2/bitcoin");
|
|
110
|
+
var import_common3 = require("@did-btcr2/common");
|
|
111
|
+
var import_smt2 = require("@did-btcr2/smt");
|
|
112
|
+
var import_secp256k12 = require("@noble/curves/secp256k1.js");
|
|
113
|
+
var import_utils2 = require("@noble/hashes/utils");
|
|
114
|
+
var import_btc_signer2 = require("@scure/btc-signer");
|
|
115
|
+
var import_musig22 = require("@scure/btc-signer/musig2");
|
|
116
|
+
|
|
117
|
+
// src/core/errors.ts
|
|
118
|
+
var import_common2 = require("@did-btcr2/common");
|
|
119
|
+
var AggregationParticipantError = class extends import_common2.MethodError {
|
|
120
|
+
constructor(message, type = "AggregationParticipantError", data) {
|
|
121
|
+
super(message, type, data);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
var AggregationCohortError = class extends import_common2.MethodError {
|
|
125
|
+
constructor(message, type = "AggregationCohortError", data) {
|
|
126
|
+
super(message, type, data);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var SigningSessionError = class extends import_common2.MethodError {
|
|
130
|
+
constructor(message, type = "SigningSessionError", data) {
|
|
131
|
+
super(message, type, data);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// src/core/recovery-policy.ts
|
|
136
|
+
var import_secp256k1 = require("@noble/curves/secp256k1.js");
|
|
137
|
+
var import_utils = require("@noble/hashes/utils");
|
|
138
|
+
var import_btc_signer = require("@scure/btc-signer");
|
|
139
|
+
var import_musig2 = require("@scure/btc-signer/musig2");
|
|
140
|
+
var DEFAULT_FUNDING_MODEL = "operator-funded";
|
|
141
|
+
var MAX_RECOVERY_SEQUENCE = 65535;
|
|
142
|
+
var TAPROOT_LEAF_VERSION = 192;
|
|
143
|
+
function resolveFallbackThreshold(advertised, n) {
|
|
144
|
+
return advertised ?? Math.max(1, n - 1);
|
|
145
|
+
}
|
|
146
|
+
function assertFallbackParams({ cohortKeys, fallbackThreshold }) {
|
|
147
|
+
const n = cohortKeys.length;
|
|
148
|
+
if (n === 0) {
|
|
149
|
+
throw new AggregationCohortError(
|
|
150
|
+
"Cannot build fallback leaf: no cohort keys.",
|
|
151
|
+
"NO_COHORT_KEYS"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
for (const key of cohortKeys) {
|
|
155
|
+
if (key.length !== 33) {
|
|
156
|
+
throw new AggregationCohortError(
|
|
157
|
+
`Cohort key must be a 33-byte compressed public key, got ${key.length} bytes.`,
|
|
158
|
+
"INVALID_COHORT_KEY",
|
|
159
|
+
{ length: key.length }
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!Number.isInteger(fallbackThreshold) || fallbackThreshold < 1 || fallbackThreshold > n) {
|
|
164
|
+
throw new AggregationCohortError(
|
|
165
|
+
`Fallback threshold must be an integer in [1, ${n}] (k-of-n), got ${fallbackThreshold}.`,
|
|
166
|
+
"INVALID_FALLBACK_THRESHOLD",
|
|
167
|
+
{ fallbackThreshold, n }
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function buildFallbackLeaf(params) {
|
|
172
|
+
assertFallbackParams(params);
|
|
173
|
+
const xOnlyKeys = (0, import_musig2.sortKeys)(params.cohortKeys).map((k) => k.slice(1));
|
|
174
|
+
return (0, import_btc_signer.p2tr_ms)(params.fallbackThreshold, xOnlyKeys).script;
|
|
175
|
+
}
|
|
176
|
+
function assertRecoveryParams({ recoveryKey, recoverySequence }) {
|
|
177
|
+
if (recoveryKey.length !== 32) {
|
|
178
|
+
throw new AggregationCohortError(
|
|
179
|
+
`Recovery key must be a 32-byte x-only public key, got ${recoveryKey.length} bytes.`,
|
|
180
|
+
"INVALID_RECOVERY_KEY"
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (!Number.isInteger(recoverySequence) || recoverySequence < 1 || recoverySequence > MAX_RECOVERY_SEQUENCE) {
|
|
184
|
+
throw new AggregationCohortError(
|
|
185
|
+
`Recovery sequence must be a block-based BIP-68 relative timelock in [1, ${MAX_RECOVERY_SEQUENCE}], got ${recoverySequence}.`,
|
|
186
|
+
"INVALID_RECOVERY_SEQUENCE"
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function buildRecoveryScript(params) {
|
|
191
|
+
assertRecoveryParams(params);
|
|
192
|
+
return import_btc_signer.Script.encode([
|
|
193
|
+
params.recoverySequence,
|
|
194
|
+
"CHECKSEQUENCEVERIFY",
|
|
195
|
+
"DROP",
|
|
196
|
+
params.recoveryKey,
|
|
197
|
+
"CHECKSIG"
|
|
198
|
+
]);
|
|
199
|
+
}
|
|
200
|
+
function buildRecoveryLeaves(fundingModel, params) {
|
|
201
|
+
switch (fundingModel) {
|
|
202
|
+
case "operator-funded":
|
|
203
|
+
return [
|
|
204
|
+
{ script: buildFallbackLeaf(params) },
|
|
205
|
+
{ script: buildRecoveryScript(params) }
|
|
206
|
+
];
|
|
207
|
+
case "participant-funded":
|
|
208
|
+
throw new AggregationCohortError(
|
|
209
|
+
"Funding model 'participant-funded' is reserved and not yet implemented.",
|
|
210
|
+
"UNSUPPORTED_FUNDING_MODEL",
|
|
211
|
+
{ fundingModel }
|
|
212
|
+
);
|
|
213
|
+
default:
|
|
214
|
+
throw new AggregationCohortError(
|
|
215
|
+
`Unknown funding model: ${fundingModel}.`,
|
|
216
|
+
"UNKNOWN_FUNDING_MODEL",
|
|
217
|
+
{ fundingModel }
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/core/cohort.ts
|
|
223
|
+
var AggregationCohort = class {
|
|
224
|
+
/** Unique identifier for the cohort. */
|
|
225
|
+
id;
|
|
226
|
+
/** DID of the Aggregation Service managing this cohort. */
|
|
227
|
+
serviceDid;
|
|
228
|
+
/** Minimum number of participants required to finalize the cohort. */
|
|
229
|
+
minParticipants;
|
|
230
|
+
/** Network on which the cohort operates (mainnet, mutinynet, etc.). */
|
|
231
|
+
network;
|
|
232
|
+
/** Type of beacon used in the cohort: 'CASBeacon' or 'SMTBeacon'. */
|
|
233
|
+
beaconType;
|
|
234
|
+
/** List of participant DIDs that have been accepted into the cohort. */
|
|
235
|
+
participants = [];
|
|
236
|
+
/**
|
|
237
|
+
* Mapping from participant DID to their compressed secp256k1 public key.
|
|
238
|
+
* Distinct from {@link cohortKeys} (which is sorted per BIP-327): this lets
|
|
239
|
+
* callers look up a participant's key without knowing their position in the
|
|
240
|
+
* sorted array. Populated by the service at `acceptParticipant` time.
|
|
241
|
+
*/
|
|
242
|
+
participantKeys = /* @__PURE__ */ new Map();
|
|
243
|
+
/** Sorted list of cohort participants' compressed public keys. */
|
|
244
|
+
#cohortKeys = [];
|
|
245
|
+
/**
|
|
246
|
+
* BIP-341 TapTweak scalar: `taggedHash("TapTweak", internalPubkey || tapMerkleRoot)`.
|
|
247
|
+
* The beacon output is an internal key (the MuSig2 aggregate) plus a script
|
|
248
|
+
* tree (the recovery leaves), so the tweak commits to the tree's Merkle root.
|
|
249
|
+
* The MuSig2 signing session applies this as an x-only tweak; a value that does
|
|
250
|
+
* not match the root the funded address was derived from silently yields an
|
|
251
|
+
* invalid key-path signature.
|
|
252
|
+
*/
|
|
253
|
+
tapTweak = new Uint8Array();
|
|
254
|
+
/** The n-of-n MuSig2 aggregate internal key, x-only (32 bytes), set by computeBeaconAddress(). */
|
|
255
|
+
internalKey = new Uint8Array();
|
|
256
|
+
/** BIP-341 Taproot Merkle root of the recovery script tree, set by computeBeaconAddress(). */
|
|
257
|
+
tapMerkleRoot = new Uint8Array();
|
|
258
|
+
/** Operator recovery key, x-only (32 bytes). Used to build the CSV recovery leaf. */
|
|
259
|
+
recoveryKey;
|
|
260
|
+
/** Relative-timelock (BIP-68 nSequence) before the recovery leaf is spendable. */
|
|
261
|
+
recoverySequence;
|
|
262
|
+
/** Funding model governing the recovery leaves (default 'operator-funded'). */
|
|
263
|
+
fundingModel;
|
|
264
|
+
/**
|
|
265
|
+
* Advertised k of the k-of-n fallback leaf, or undefined to default to n-1 at
|
|
266
|
+
* address computation. Read the resolved value via {@link effectiveFallbackThreshold}.
|
|
267
|
+
*/
|
|
268
|
+
fallbackThreshold;
|
|
269
|
+
/** The Taproot beacon address: key path is the MuSig2 aggregate, script path is fallback + recovery. */
|
|
270
|
+
beaconAddress = "";
|
|
271
|
+
/** Pending DID updates submitted by participants, keyed by DID. */
|
|
272
|
+
pendingUpdates = /* @__PURE__ */ new Map();
|
|
273
|
+
/**
|
|
274
|
+
* Participant DIDs that explicitly declined to submit an update this round
|
|
275
|
+
* (cooperative non-inclusion). A decliner is absent from the CAS Announcement
|
|
276
|
+
* Map and carries a non-inclusion leaf in the SMT, yet still signs. Kept
|
|
277
|
+
* disjoint from {@link pendingUpdates} so CAS correctness holds by construction.
|
|
278
|
+
*/
|
|
279
|
+
nonIncluded = /* @__PURE__ */ new Set();
|
|
280
|
+
/** CAS Beacon Announcement Map (DID to updateHash), set by buildCASAnnouncement(). */
|
|
281
|
+
casAnnouncement;
|
|
282
|
+
/** Per-participant SMT proofs, set by buildSMTTree(). */
|
|
283
|
+
smtProofs;
|
|
284
|
+
/** Signal bytes (32 bytes) for OP_RETURN: SHA-256 of CAS announcement OR SMT root. */
|
|
285
|
+
signalBytes;
|
|
286
|
+
/** Set of participant DIDs that have approved the aggregated data. */
|
|
287
|
+
validationAcks = /* @__PURE__ */ new Set();
|
|
288
|
+
/** Set of participant DIDs that have rejected the aggregated data. */
|
|
289
|
+
validationRejections = /* @__PURE__ */ new Set();
|
|
290
|
+
constructor({ id, minParticipants, serviceDid, network, beaconType, recoveryKey, recoverySequence, fundingModel, fallbackThreshold }) {
|
|
291
|
+
this.id = id || crypto.randomUUID();
|
|
292
|
+
this.minParticipants = minParticipants ?? 2;
|
|
293
|
+
this.serviceDid = serviceDid || "";
|
|
294
|
+
this.network = network;
|
|
295
|
+
this.beaconType = beaconType || "CASBeacon";
|
|
296
|
+
this.recoveryKey = recoveryKey;
|
|
297
|
+
this.recoverySequence = recoverySequence;
|
|
298
|
+
this.fundingModel = fundingModel ?? DEFAULT_FUNDING_MODEL;
|
|
299
|
+
this.fallbackThreshold = fallbackThreshold;
|
|
300
|
+
}
|
|
301
|
+
/** Sorted cohort keys (sorted on assignment per BIP-327). */
|
|
302
|
+
get cohortKeys() {
|
|
303
|
+
return this.#cohortKeys;
|
|
304
|
+
}
|
|
305
|
+
set cohortKeys(keys) {
|
|
306
|
+
this.#cohortKeys = (0, import_musig22.sortKeys)(keys);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* The resolved k of the k-of-n fallback leaf: the advertised
|
|
310
|
+
* {@link fallbackThreshold}, or n-1 (floored at 1) when unadvertised, where n is
|
|
311
|
+
* the current cohort size. This is the value the beacon address commits to and
|
|
312
|
+
* the spend builders must reproduce. Returns 0 before any cohort keys are set.
|
|
313
|
+
*/
|
|
314
|
+
get effectiveFallbackThreshold() {
|
|
315
|
+
if (this.#cohortKeys.length === 0) return 0;
|
|
316
|
+
return resolveFallbackThreshold(this.fallbackThreshold, this.#cohortKeys.length);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Computes the Taproot beacon address from the cohort keys and recovery params.
|
|
320
|
+
*
|
|
321
|
+
* The output's key path is the n-of-n MuSig2 aggregate of the cohort keys; its
|
|
322
|
+
* script path is the recovery tree (a CSV recovery leaf so a missing signer
|
|
323
|
+
* cannot permanently lock the UTXO). Sets `internalKey`, `tapMerkleRoot`, and
|
|
324
|
+
* the `tapTweak` the MuSig2 session must apply for the key-path spend.
|
|
325
|
+
*
|
|
326
|
+
* The tweak is derived from the Merkle root the address was built with (read
|
|
327
|
+
* back from the payment), never recomputed by hand, so the MuSig2 key-path
|
|
328
|
+
* signature is guaranteed to validate against the funded address.
|
|
329
|
+
*/
|
|
330
|
+
computeBeaconAddress() {
|
|
331
|
+
if (this.#cohortKeys.length === 0) {
|
|
332
|
+
throw new AggregationCohortError(
|
|
333
|
+
"Cannot compute beacon address: no cohort keys.",
|
|
334
|
+
"NO_COHORT_KEYS",
|
|
335
|
+
{ cohortId: this.id }
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (!this.recoveryKey || this.recoveryKey.length === 0 || this.recoverySequence === void 0) {
|
|
339
|
+
throw new AggregationCohortError(
|
|
340
|
+
"Cannot compute beacon address: missing recovery key or sequence.",
|
|
341
|
+
"NO_RECOVERY_PARAMS",
|
|
342
|
+
{ cohortId: this.id }
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const keyAggContext = (0, import_musig22.keyAggregate)(this.#cohortKeys);
|
|
346
|
+
const aggPubkey = (0, import_musig22.keyAggExport)(keyAggContext);
|
|
347
|
+
const leaves = buildRecoveryLeaves(this.fundingModel, {
|
|
348
|
+
recoveryKey: this.recoveryKey,
|
|
349
|
+
recoverySequence: this.recoverySequence,
|
|
350
|
+
cohortKeys: this.#cohortKeys,
|
|
351
|
+
fallbackThreshold: resolveFallbackThreshold(this.fallbackThreshold, this.#cohortKeys.length)
|
|
352
|
+
});
|
|
353
|
+
const payment = (0, import_btc_signer2.p2tr)(aggPubkey, leaves, (0, import_bitcoin.getNetwork)(this.network), true);
|
|
354
|
+
this.internalKey = aggPubkey;
|
|
355
|
+
this.tapMerkleRoot = payment.tapMerkleRoot;
|
|
356
|
+
this.tapTweak = import_secp256k12.schnorr.utils.taggedHash("TapTweak", (0, import_utils2.concatBytes)(aggPubkey, payment.tapMerkleRoot));
|
|
357
|
+
if (!payment.address) {
|
|
358
|
+
throw new AggregationCohortError(
|
|
359
|
+
"Failed to compute Taproot address",
|
|
360
|
+
"BEACON_ADDRESS_ERROR",
|
|
361
|
+
{ cohortId: this.id }
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
this.beaconAddress = payment.address;
|
|
365
|
+
return payment.address;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Validates that the participant's key is in the cohort and the beacon address
|
|
369
|
+
* matches the locally-computed one. Used by participants to verify cohort ready
|
|
370
|
+
* messages from the service.
|
|
371
|
+
*/
|
|
372
|
+
validateMembership(participantPkHex, cohortKeysHex, expectedBeaconAddress) {
|
|
373
|
+
if (!cohortKeysHex.includes(participantPkHex)) {
|
|
374
|
+
throw new AggregationCohortError(
|
|
375
|
+
`Participant key not found in cohort ${this.id}.`,
|
|
376
|
+
"COHORT_VALIDATION_ERROR",
|
|
377
|
+
{ cohortId: this.id, participantPkHex }
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
this.cohortKeys = cohortKeysHex.map((k) => (0, import_utils2.hexToBytes)(k));
|
|
381
|
+
const computed = this.computeBeaconAddress();
|
|
382
|
+
if (computed !== expectedBeaconAddress) {
|
|
383
|
+
throw new AggregationCohortError(
|
|
384
|
+
`Computed beacon address ${computed} does not match expected ${expectedBeaconAddress}.`,
|
|
385
|
+
"BEACON_ADDRESS_MISMATCH",
|
|
386
|
+
{ cohortId: this.id, computed, expected: expectedBeaconAddress }
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Returns the position of a participant's public key in the sorted
|
|
392
|
+
* {@link cohortKeys} array, or -1 if the participant is not in the cohort.
|
|
393
|
+
* Required by MuSig2 partial-sig verification which indexes by signer position.
|
|
394
|
+
*/
|
|
395
|
+
indexOfParticipant(did) {
|
|
396
|
+
const pk = this.participantKeys.get(did);
|
|
397
|
+
if (!pk) return -1;
|
|
398
|
+
return this.#cohortKeys.findIndex(
|
|
399
|
+
(k) => k.length === pk.length && k.every((b, i) => b === pk[i])
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
addUpdate(participantDid, signedUpdate) {
|
|
403
|
+
if (!this.participants.includes(participantDid)) {
|
|
404
|
+
throw new AggregationCohortError(
|
|
405
|
+
`Participant ${participantDid} is not in cohort ${this.id}.`,
|
|
406
|
+
"UNKNOWN_PARTICIPANT",
|
|
407
|
+
{ cohortId: this.id, participantDid }
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
this.pendingUpdates.set(participantDid, signedUpdate);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Record that a participant declined to submit an update this round
|
|
414
|
+
* (cooperative non-inclusion). The member stays in the cohort and still signs.
|
|
415
|
+
*/
|
|
416
|
+
addNonInclusion(participantDid) {
|
|
417
|
+
if (!this.participants.includes(participantDid)) {
|
|
418
|
+
throw new AggregationCohortError(
|
|
419
|
+
`Participant ${participantDid} is not in cohort ${this.id}.`,
|
|
420
|
+
"UNKNOWN_PARTICIPANT",
|
|
421
|
+
{ cohortId: this.id, participantDid }
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
if (this.pendingUpdates.has(participantDid)) {
|
|
425
|
+
throw new AggregationCohortError(
|
|
426
|
+
`Participant ${participantDid} already submitted an update; cannot also decline.`,
|
|
427
|
+
"CONFLICTING_RESPONSE",
|
|
428
|
+
{ cohortId: this.id, participantDid }
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
this.nonIncluded.add(participantDid);
|
|
432
|
+
}
|
|
433
|
+
hasAllUpdates() {
|
|
434
|
+
return this.pendingUpdates.size === this.participants.length;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* True when every participant has responded for this round, either with an
|
|
438
|
+
* update or with an explicit non-inclusion. This is the aggregation gate when
|
|
439
|
+
* non-inclusion is in play; it generalizes {@link hasAllUpdates} the same way
|
|
440
|
+
* {@link hasAllValidationResponses} generalizes a unanimous ack.
|
|
441
|
+
*/
|
|
442
|
+
hasAllResponses() {
|
|
443
|
+
return this.pendingUpdates.size + this.nonIncluded.size === this.participants.length;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Builds a CAS Announcement Map from collected updates.
|
|
447
|
+
* Maps each participant DID to base64url canonical hash of their signed update.
|
|
448
|
+
* Computes signal bytes as SHA-256 of canonicalized announcement.
|
|
449
|
+
*
|
|
450
|
+
* Members who declined (cooperative non-inclusion) are naturally absent from
|
|
451
|
+
* the map: the body iterates {@link pendingUpdates}, which never holds a
|
|
452
|
+
* decliner. Absence from the map is exactly the CAS non-inclusion signal.
|
|
453
|
+
*/
|
|
454
|
+
buildCASAnnouncement() {
|
|
455
|
+
if (!this.hasAllResponses()) {
|
|
456
|
+
throw new AggregationCohortError(
|
|
457
|
+
"Cannot build CAS Announcement: not all participants have responded.",
|
|
458
|
+
"INCOMPLETE_RESPONSES",
|
|
459
|
+
{ cohortId: this.id, updates: this.pendingUpdates.size, declined: this.nonIncluded.size, total: this.participants.length }
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
const announcement = {};
|
|
463
|
+
for (const [did, signedUpdate] of this.pendingUpdates) {
|
|
464
|
+
announcement[did] = (0, import_common3.canonicalHash)(signedUpdate);
|
|
465
|
+
}
|
|
466
|
+
this.casAnnouncement = announcement;
|
|
467
|
+
this.signalBytes = (0, import_common3.hash)((0, import_common3.canonicalize)(announcement));
|
|
468
|
+
return announcement;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Builds an SMT tree with one leaf per participant.
|
|
472
|
+
*
|
|
473
|
+
* A member who submitted an update gets an inclusion leaf
|
|
474
|
+
* (SHA-256(SHA-256(nonce) || SHA-256(update))); a member who declined gets a
|
|
475
|
+
* non-inclusion leaf (SHA-256(SHA-256(nonce)), the `signedUpdate` entry field
|
|
476
|
+
* omitted). The cohort mints each member's nonce and returns it inside that
|
|
477
|
+
* member's serialized proof, so a decliner can self-validate its own
|
|
478
|
+
* non-inclusion slot and the resolver can recompute the leaf. Stores
|
|
479
|
+
* per-participant proofs and the SMT root as signalBytes.
|
|
480
|
+
*/
|
|
481
|
+
buildSMTTree() {
|
|
482
|
+
if (!this.hasAllResponses()) {
|
|
483
|
+
throw new AggregationCohortError(
|
|
484
|
+
"Cannot build SMT tree: not all participants have responded.",
|
|
485
|
+
"INCOMPLETE_RESPONSES",
|
|
486
|
+
{ cohortId: this.id, updates: this.pendingUpdates.size, declined: this.nonIncluded.size, total: this.participants.length }
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
const tree = new import_smt2.BTCR2MerkleTree();
|
|
490
|
+
const entries = [];
|
|
491
|
+
const encoder = new TextEncoder();
|
|
492
|
+
for (const did of this.participants) {
|
|
493
|
+
const nonce = (0, import_utils2.randomBytes)(32);
|
|
494
|
+
const signedUpdate = this.pendingUpdates.get(did);
|
|
495
|
+
if (signedUpdate) {
|
|
496
|
+
entries.push({ did, nonce, signedUpdate: encoder.encode((0, import_common3.canonicalize)(signedUpdate)) });
|
|
497
|
+
} else {
|
|
498
|
+
entries.push({ did, nonce });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
tree.addEntries(entries);
|
|
502
|
+
tree.finalize();
|
|
503
|
+
this.signalBytes = tree.rootHash;
|
|
504
|
+
this.smtProofs = /* @__PURE__ */ new Map();
|
|
505
|
+
for (const did of this.participants) {
|
|
506
|
+
this.smtProofs.set(did, tree.proof(did));
|
|
507
|
+
}
|
|
508
|
+
return this.smtProofs;
|
|
509
|
+
}
|
|
510
|
+
addValidation(participantDid, approved) {
|
|
511
|
+
if (!this.participants.includes(participantDid)) {
|
|
512
|
+
throw new AggregationCohortError(
|
|
513
|
+
`Unknown participant ${participantDid} in cohort ${this.id}.`,
|
|
514
|
+
"UNKNOWN_PARTICIPANT",
|
|
515
|
+
{ cohortId: this.id, participantDid }
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
if (approved) {
|
|
519
|
+
this.validationAcks.add(participantDid);
|
|
520
|
+
} else {
|
|
521
|
+
this.validationRejections.add(participantDid);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* True when every participant has either approved or rejected the aggregated data.
|
|
526
|
+
*/
|
|
527
|
+
hasAllValidationResponses() {
|
|
528
|
+
return this.validationAcks.size + this.validationRejections.size === this.participants.length;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* True when all participants approved. Note: differs from {@link hasAllValidationResponses},
|
|
532
|
+
* this returns false if any participant rejected, even if all responses are in.
|
|
533
|
+
*/
|
|
534
|
+
isFullyValidated() {
|
|
535
|
+
return this.validationRejections.size === 0 && this.validationAcks.size === this.participants.length;
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// src/core/fallback-spend.ts
|
|
540
|
+
var import_bitcoin2 = require("@did-btcr2/bitcoin");
|
|
541
|
+
var import_secp256k13 = require("@noble/curves/secp256k1.js");
|
|
542
|
+
var import_btc_signer3 = require("@scure/btc-signer");
|
|
543
|
+
var import_musig23 = require("@scure/btc-signer/musig2");
|
|
544
|
+
function fallbackSighash(tx, inputIndex, prevOutScript, prevOutValue, fallbackLeafScript) {
|
|
545
|
+
return tx.preimageWitnessV1(
|
|
546
|
+
inputIndex,
|
|
547
|
+
[prevOutScript],
|
|
548
|
+
import_btc_signer3.SigHash.DEFAULT,
|
|
549
|
+
[prevOutValue],
|
|
550
|
+
void 0,
|
|
551
|
+
fallbackLeafScript,
|
|
552
|
+
TAPROOT_LEAF_VERSION
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/core/conditions.ts
|
|
557
|
+
var KNOWN_FUNDING_MODELS = ["operator-funded", "participant-funded"];
|
|
558
|
+
|
|
559
|
+
// src/core/messages/constants.ts
|
|
560
|
+
var AGGREGATION_MESSAGE_PREFIX = "https://btcr2.dev/aggregation";
|
|
561
|
+
var COHORT_ADVERT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_advert`;
|
|
562
|
+
var COHORT_OPT_IN = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in`;
|
|
563
|
+
var COHORT_OPT_IN_ACCEPT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in_accept`;
|
|
564
|
+
var COHORT_READY = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_ready`;
|
|
565
|
+
var SUBMIT_UPDATE = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_update`;
|
|
566
|
+
var SUBMIT_NONINCLUDED = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_nonincluded`;
|
|
567
|
+
var DISTRIBUTE_AGGREGATED_DATA = `${AGGREGATION_MESSAGE_PREFIX}/update/distribute_aggregated_data`;
|
|
568
|
+
var VALIDATION_ACK = `${AGGREGATION_MESSAGE_PREFIX}/update/validation_ack`;
|
|
569
|
+
var AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/authorization_request`;
|
|
570
|
+
var NONCE_CONTRIBUTION = `${AGGREGATION_MESSAGE_PREFIX}/sign/nonce_contribution`;
|
|
571
|
+
var AGGREGATED_NONCE = `${AGGREGATION_MESSAGE_PREFIX}/sign/aggregated_nonce`;
|
|
572
|
+
var SIGNATURE_AUTHORIZATION = `${AGGREGATION_MESSAGE_PREFIX}/sign/signature_authorization`;
|
|
573
|
+
var FALLBACK_AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/fallback_authorization_request`;
|
|
574
|
+
var FALLBACK_SIGNATURE = `${AGGREGATION_MESSAGE_PREFIX}/sign/fallback_signature`;
|
|
575
|
+
|
|
576
|
+
// src/core/messages/bodies.ts
|
|
577
|
+
var hasStr = (b, k) => !!b && typeof b[k] === "string";
|
|
578
|
+
var hasIntMin = (b, k, min) => {
|
|
579
|
+
const v = b ? b[k] : void 0;
|
|
580
|
+
return typeof v === "number" && Number.isInteger(v) && v >= min;
|
|
581
|
+
};
|
|
582
|
+
var optIntMin = (b, k, min) => {
|
|
583
|
+
const v = b ? b[k] : void 0;
|
|
584
|
+
return v === void 0 || typeof v === "number" && Number.isInteger(v) && v >= min;
|
|
585
|
+
};
|
|
586
|
+
var intInRange = (b, k, min, max) => {
|
|
587
|
+
const v = b ? b[k] : void 0;
|
|
588
|
+
return typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
|
|
589
|
+
};
|
|
590
|
+
var optStrOneOf = (b, k, allowed) => {
|
|
591
|
+
const v = b ? b[k] : void 0;
|
|
592
|
+
return v === void 0 || typeof v === "string" && allowed.includes(v);
|
|
593
|
+
};
|
|
594
|
+
var hasBytes = (b, k) => !!b && b[k] instanceof Uint8Array;
|
|
595
|
+
function isCohortAdvertMessage(m) {
|
|
596
|
+
return m.type === COHORT_ADVERT && hasStr(m.body, "cohortId") && hasIntMin(m.body, "minParticipants", 1) && optIntMin(m.body, "maxParticipants", 1) && hasStr(m.body, "beaconType") && hasStr(m.body, "network") && hasStr(m.body, "recoveryKey") && intInRange(m.body, "recoverySequence", 1, MAX_RECOVERY_SEQUENCE) && optStrOneOf(m.body, "fundingModel", KNOWN_FUNDING_MODELS) && optIntMin(m.body, "fallbackThreshold", 1) && hasBytes(m.body, "communicationPk");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/core/messages/base.ts
|
|
600
|
+
var AGGREGATION_WIRE_VERSION = 1;
|
|
601
|
+
var BaseMessage = class {
|
|
602
|
+
type;
|
|
603
|
+
version;
|
|
604
|
+
to;
|
|
605
|
+
from;
|
|
606
|
+
body;
|
|
607
|
+
constructor({ type, version, to, from, body }) {
|
|
608
|
+
this.type = type;
|
|
609
|
+
this.version = version ?? AGGREGATION_WIRE_VERSION;
|
|
610
|
+
this.to = to;
|
|
611
|
+
this.from = from;
|
|
612
|
+
this.body = body;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Converts a BaseMessage to a JSON object.
|
|
616
|
+
* @returns {Base} The JSON representation of the BaseMessage.
|
|
617
|
+
*/
|
|
618
|
+
toJSON() {
|
|
619
|
+
return {
|
|
620
|
+
type: this.type,
|
|
621
|
+
version: this.version,
|
|
622
|
+
to: this.to,
|
|
623
|
+
from: this.from,
|
|
624
|
+
body: this.body
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// src/core/messages/factories.ts
|
|
630
|
+
function createCohortOptInMessage(fields) {
|
|
631
|
+
const { from, to, ...body } = fields;
|
|
632
|
+
return new BaseMessage({ type: COHORT_OPT_IN, from, to, body });
|
|
633
|
+
}
|
|
634
|
+
function createSubmitUpdateMessage(fields) {
|
|
635
|
+
const { from, to, ...body } = fields;
|
|
636
|
+
return new BaseMessage({ type: SUBMIT_UPDATE, from, to, body });
|
|
637
|
+
}
|
|
638
|
+
function createSubmitNonIncludedMessage(fields) {
|
|
639
|
+
const { from, to, ...body } = fields;
|
|
640
|
+
return new BaseMessage({ type: SUBMIT_NONINCLUDED, from, to, body });
|
|
641
|
+
}
|
|
642
|
+
function createValidationAckMessage(fields) {
|
|
643
|
+
const { from, to, ...body } = fields;
|
|
644
|
+
return new BaseMessage({ type: VALIDATION_ACK, from, to, body });
|
|
645
|
+
}
|
|
646
|
+
function createNonceContributionMessage(fields) {
|
|
647
|
+
const { from, to, ...body } = fields;
|
|
648
|
+
return new BaseMessage({ type: NONCE_CONTRIBUTION, from, to, body });
|
|
649
|
+
}
|
|
650
|
+
function createSignatureAuthorizationMessage(fields) {
|
|
651
|
+
const { from, to, ...body } = fields;
|
|
652
|
+
return new BaseMessage({ type: SIGNATURE_AUTHORIZATION, from, to, body });
|
|
653
|
+
}
|
|
654
|
+
function createFallbackSignatureMessage(fields) {
|
|
655
|
+
const { from, to, ...body } = fields;
|
|
656
|
+
return new BaseMessage({ type: FALLBACK_SIGNATURE, from, to, body });
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/core/signing-session.ts
|
|
660
|
+
var import_keypair = require("@did-btcr2/keypair");
|
|
661
|
+
var import_btc_signer4 = require("@scure/btc-signer");
|
|
662
|
+
var musig2 = __toESM(require("@scure/btc-signer/musig2"), 1);
|
|
663
|
+
var BeaconSigningSession = class {
|
|
664
|
+
/** Unique identifier for this signing session. */
|
|
665
|
+
id;
|
|
666
|
+
/** The cohort this session signs for. */
|
|
667
|
+
cohort;
|
|
668
|
+
/** The Bitcoin transaction being signed. */
|
|
669
|
+
pendingTx;
|
|
670
|
+
/** Previous output scripts for Taproot sighash computation (BIP-341). */
|
|
671
|
+
prevOutScripts;
|
|
672
|
+
/** Previous output values for Taproot sighash computation. */
|
|
673
|
+
prevOutValues;
|
|
674
|
+
/** Map of participant publicKey-hex to public nonce contribution. */
|
|
675
|
+
nonceContributions = /* @__PURE__ */ new Map();
|
|
676
|
+
/** Aggregated MuSig2 nonce (66 bytes). */
|
|
677
|
+
aggregatedNonce;
|
|
678
|
+
/** Map of participant DID to partial signature. */
|
|
679
|
+
partialSignatures = /* @__PURE__ */ new Map();
|
|
680
|
+
/** Final 64-byte Schnorr signature. */
|
|
681
|
+
signature;
|
|
682
|
+
/** Current signing session phase. */
|
|
683
|
+
phase;
|
|
684
|
+
/**
|
|
685
|
+
* Participant's MuSig2 secret nonce, held only on the participant side
|
|
686
|
+
* between {@link generateNonceContribution} and {@link generatePartialSignature}.
|
|
687
|
+
* Private and cleared on every terminal path (success, failure, or teardown
|
|
688
|
+
* via {@link clearSecrets}) so a spent or abandoned nonce cannot be reused or
|
|
689
|
+
* serialized.
|
|
690
|
+
*/
|
|
691
|
+
#secretNonce;
|
|
692
|
+
constructor({ id, cohort, pendingTx, prevOutScripts, prevOutValues }) {
|
|
693
|
+
this.id = id || crypto.randomUUID();
|
|
694
|
+
this.cohort = cohort;
|
|
695
|
+
this.pendingTx = pendingTx;
|
|
696
|
+
this.prevOutScripts = prevOutScripts || [];
|
|
697
|
+
this.prevOutValues = prevOutValues || [];
|
|
698
|
+
this.phase = "AwaitingNonceContributions" /* AwaitingNonceContributions */;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Computes the Taproot sighash (BIP-341) for the first input.
|
|
702
|
+
*/
|
|
703
|
+
get sigHash() {
|
|
704
|
+
if (!this.prevOutScripts.length || !this.prevOutValues.length) {
|
|
705
|
+
throw new SigningSessionError(
|
|
706
|
+
"Cannot compute sighash: missing prevOutScripts or prevOutValues.",
|
|
707
|
+
"SIGHASH_ERROR"
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
return this.pendingTx.preimageWitnessV1(
|
|
711
|
+
0,
|
|
712
|
+
this.prevOutScripts,
|
|
713
|
+
import_btc_signer4.SigHash.DEFAULT,
|
|
714
|
+
this.prevOutValues
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
addNonceContribution(participantDid, nonceContribution) {
|
|
718
|
+
if (this.phase !== "AwaitingNonceContributions" /* AwaitingNonceContributions */) {
|
|
719
|
+
throw new SigningSessionError(
|
|
720
|
+
`Nonce contributions not expected. Current phase: ${this.phase}`,
|
|
721
|
+
"INVALID_PHASE",
|
|
722
|
+
{ phase: this.phase }
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
if (!this.cohort.participants.includes(participantDid)) {
|
|
726
|
+
throw new SigningSessionError(
|
|
727
|
+
`Participant ${participantDid} is not a member of cohort ${this.cohort.id}.`,
|
|
728
|
+
"UNKNOWN_PARTICIPANT",
|
|
729
|
+
{ cohortId: this.cohort.id, participantDid }
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
if (nonceContribution.length !== 66) {
|
|
733
|
+
throw new SigningSessionError(
|
|
734
|
+
`Invalid nonce contribution: expected 66 bytes, got ${nonceContribution.length}.`,
|
|
735
|
+
"INVALID_NONCE_LENGTH"
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
if (this.nonceContributions.has(participantDid)) {
|
|
739
|
+
throw new SigningSessionError(
|
|
740
|
+
`Duplicate nonce contribution from ${participantDid}.`,
|
|
741
|
+
"DUPLICATE_NONCE"
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
this.nonceContributions.set(participantDid, nonceContribution);
|
|
745
|
+
if (this.nonceContributions.size === this.cohort.participants.length) {
|
|
746
|
+
this.phase = "NonceContributionsReceived" /* NonceContributionsReceived */;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
generateAggregatedNonce() {
|
|
750
|
+
if (this.phase !== "NonceContributionsReceived" /* NonceContributionsReceived */) {
|
|
751
|
+
throw new SigningSessionError(
|
|
752
|
+
`Cannot aggregate nonces: phase is ${this.phase}, expected NonceContributionsReceived.`,
|
|
753
|
+
"INVALID_PHASE"
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
this.aggregatedNonce = musig2.nonceAggregate([...this.nonceContributions.values()]);
|
|
757
|
+
this.phase = "AwaitingPartialSignatures" /* AwaitingPartialSignatures */;
|
|
758
|
+
return this.aggregatedNonce;
|
|
759
|
+
}
|
|
760
|
+
addPartialSignature(participantDid, partialSig) {
|
|
761
|
+
if (this.phase !== "AwaitingPartialSignatures" /* AwaitingPartialSignatures */) {
|
|
762
|
+
throw new SigningSessionError(
|
|
763
|
+
`Partial signatures not expected. Current phase: ${this.phase}`,
|
|
764
|
+
"INVALID_PHASE"
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
if (!this.cohort.participants.includes(participantDid)) {
|
|
768
|
+
throw new SigningSessionError(
|
|
769
|
+
`Participant ${participantDid} is not a member of cohort ${this.cohort.id}.`,
|
|
770
|
+
"UNKNOWN_PARTICIPANT",
|
|
771
|
+
{ cohortId: this.cohort.id, participantDid }
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
if (this.partialSignatures.has(participantDid)) {
|
|
775
|
+
throw new SigningSessionError(
|
|
776
|
+
`Duplicate partial signature from ${participantDid}.`,
|
|
777
|
+
"DUPLICATE_PARTIAL_SIG"
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
this.partialSignatures.set(participantDid, partialSig);
|
|
781
|
+
if (this.partialSignatures.size === this.cohort.participants.length) {
|
|
782
|
+
this.phase = "PartialSignaturesReceived" /* PartialSignaturesReceived */;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
generateFinalSignature() {
|
|
786
|
+
if (this.phase !== "PartialSignaturesReceived" /* PartialSignaturesReceived */) {
|
|
787
|
+
throw new SigningSessionError(
|
|
788
|
+
`Cannot generate final signature: phase is ${this.phase}.`,
|
|
789
|
+
"INVALID_PHASE"
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
if (!this.aggregatedNonce) {
|
|
793
|
+
throw new SigningSessionError("Aggregated nonce missing.", "MISSING_AGGREGATED_NONCE");
|
|
794
|
+
}
|
|
795
|
+
const session = new musig2.Session(
|
|
796
|
+
this.aggregatedNonce,
|
|
797
|
+
this.cohort.cohortKeys,
|
|
798
|
+
this.sigHash,
|
|
799
|
+
[this.cohort.tapTweak],
|
|
800
|
+
[true]
|
|
801
|
+
);
|
|
802
|
+
const pubNoncesByIndex = new Array(this.cohort.cohortKeys.length);
|
|
803
|
+
for (const [did, nonce] of this.nonceContributions) {
|
|
804
|
+
const idx = this.cohort.indexOfParticipant(did);
|
|
805
|
+
if (idx < 0) {
|
|
806
|
+
throw new SigningSessionError(
|
|
807
|
+
`Cannot verify nonce from ${did}: participant key missing from cohort.`,
|
|
808
|
+
"UNKNOWN_PARTICIPANT_KEY",
|
|
809
|
+
{ participantDid: did }
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
pubNoncesByIndex[idx] = nonce;
|
|
813
|
+
}
|
|
814
|
+
for (const [did, partialSig] of this.partialSignatures) {
|
|
815
|
+
const idx = this.cohort.indexOfParticipant(did);
|
|
816
|
+
if (idx < 0) {
|
|
817
|
+
throw new SigningSessionError(
|
|
818
|
+
`Cannot verify partial signature from ${did}: participant key missing from cohort.`,
|
|
819
|
+
"UNKNOWN_PARTICIPANT_KEY",
|
|
820
|
+
{ participantDid: did }
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const ok = session.partialSigVerify(partialSig, pubNoncesByIndex, idx);
|
|
824
|
+
if (!ok) {
|
|
825
|
+
throw new SigningSessionError(
|
|
826
|
+
`Bad partial signature from ${did}.`,
|
|
827
|
+
"BAD_PARTIAL_SIG",
|
|
828
|
+
{ participantDid: did, index: idx }
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
this.signature = session.partialSigAgg([...this.partialSignatures.values()]);
|
|
833
|
+
this.phase = "Complete" /* Complete */;
|
|
834
|
+
return this.signature;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Generates a fresh MuSig2 nonce contribution for the participant.
|
|
838
|
+
* Stores the secret nonce internally for use in `generatePartialSignature()`.
|
|
839
|
+
*/
|
|
840
|
+
generateNonceContribution(participantPublicKey, participantSecretKey) {
|
|
841
|
+
const aggPublicKey = musig2.keyAggExport(musig2.keyAggregate(this.cohort.cohortKeys));
|
|
842
|
+
const nonces = musig2.nonceGen(participantPublicKey, participantSecretKey, aggPublicKey);
|
|
843
|
+
this.#secretNonce = nonces.secret;
|
|
844
|
+
return nonces.public;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Generates a partial signature using the participant's secret key + secret nonce.
|
|
848
|
+
* Requires the aggregated nonce to have been set first (via the service).
|
|
849
|
+
*
|
|
850
|
+
* Clears the stored secret nonce after use on every path (success or throw)
|
|
851
|
+
* via {@link clearSecrets}. JS cannot truly erase memory (GC may relocate
|
|
852
|
+
* buffers), but overwriting the bytes shortens the exposure window and
|
|
853
|
+
* prevents accidental reuse or serialization of a spent nonce - reuse of a
|
|
854
|
+
* MuSig2 nonce leaks the secret key.
|
|
855
|
+
*/
|
|
856
|
+
generatePartialSignature(participantSecretKey) {
|
|
857
|
+
if (!this.aggregatedNonce) {
|
|
858
|
+
throw new SigningSessionError("Aggregated nonce not available.", "MISSING_AGGREGATED_NONCE");
|
|
859
|
+
}
|
|
860
|
+
if (!this.#secretNonce) {
|
|
861
|
+
throw new SigningSessionError("Secret nonce not available - generateNonceContribution() must be called first.", "MISSING_SECRET_NONCE");
|
|
862
|
+
}
|
|
863
|
+
const session = new musig2.Session(
|
|
864
|
+
this.aggregatedNonce,
|
|
865
|
+
this.cohort.cohortKeys,
|
|
866
|
+
this.sigHash,
|
|
867
|
+
[this.cohort.tapTweak],
|
|
868
|
+
[true]
|
|
869
|
+
);
|
|
870
|
+
try {
|
|
871
|
+
return session.sign(this.#secretNonce, participantSecretKey);
|
|
872
|
+
} finally {
|
|
873
|
+
this.clearSecrets();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Zeroize any retained secret nonce. Safe to call repeatedly and on any path
|
|
878
|
+
* (completion, failure, or teardown of an abandoned session). Callers that
|
|
879
|
+
* drop a session before it reaches a partial signature should invoke this so
|
|
880
|
+
* the secret nonce does not linger on the live object.
|
|
881
|
+
*/
|
|
882
|
+
clearSecrets() {
|
|
883
|
+
if (this.#secretNonce) {
|
|
884
|
+
(0, import_keypair.wipe)(this.#secretNonce);
|
|
885
|
+
this.#secretNonce = void 0;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
isComplete() {
|
|
889
|
+
return this.phase === "Complete" /* Complete */;
|
|
890
|
+
}
|
|
891
|
+
isFailed() {
|
|
892
|
+
return this.phase === "Failed" /* Failed */;
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
// src/participant/participant.ts
|
|
897
|
+
function txEmbedsSignal(tx, signalHex) {
|
|
898
|
+
let expected;
|
|
899
|
+
try {
|
|
900
|
+
expected = (0, import_utils3.hexToBytes)(signalHex);
|
|
901
|
+
} catch {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
if (expected.length === 0) return false;
|
|
905
|
+
for (let i = 0; i < tx.outputsLength; i++) {
|
|
906
|
+
const script = tx.getOutput(i)?.script;
|
|
907
|
+
if (!script) continue;
|
|
908
|
+
let decoded;
|
|
909
|
+
try {
|
|
910
|
+
decoded = import_btc_signer5.Script.decode(script);
|
|
911
|
+
} catch {
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
if (decoded.length === 2 && decoded[0] === "RETURN" && decoded[1] instanceof Uint8Array) {
|
|
915
|
+
const payload = decoded[1];
|
|
916
|
+
if (payload.length === expected.length && payload.every((b, j) => b === expected[j])) return true;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
var AggregationParticipant = class {
|
|
922
|
+
did;
|
|
923
|
+
/** MuSig2 signing capability. The raw secret never lives as a field here. */
|
|
924
|
+
#signer;
|
|
925
|
+
/** Per-cohort state, keyed by cohortId. */
|
|
926
|
+
#cohortStates = /* @__PURE__ */ new Map();
|
|
927
|
+
constructor({ did, signer }) {
|
|
928
|
+
this.did = did;
|
|
929
|
+
this.#signer = signer;
|
|
930
|
+
}
|
|
931
|
+
/** The participant's compressed (33-byte) MuSig2 public key. Not secret. */
|
|
932
|
+
get publicKey() {
|
|
933
|
+
return this.#signer.publicKey;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Process an incoming message. Updates internal state but never produces
|
|
937
|
+
* outgoing messages: those come exclusively from action methods.
|
|
938
|
+
*/
|
|
939
|
+
receive(message) {
|
|
940
|
+
if (message.version === void 0 || message.version !== AGGREGATION_WIRE_VERSION) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const type = message.type;
|
|
944
|
+
switch (type) {
|
|
945
|
+
case COHORT_ADVERT:
|
|
946
|
+
this.#handleCohortAdvert(message);
|
|
947
|
+
break;
|
|
948
|
+
case COHORT_OPT_IN_ACCEPT:
|
|
949
|
+
this.#handleOptInAccept(message);
|
|
950
|
+
break;
|
|
951
|
+
case COHORT_READY:
|
|
952
|
+
this.#handleCohortReady(message);
|
|
953
|
+
break;
|
|
954
|
+
case DISTRIBUTE_AGGREGATED_DATA:
|
|
955
|
+
this.#handleDistributeAggregatedData(message);
|
|
956
|
+
break;
|
|
957
|
+
case AUTHORIZATION_REQUEST:
|
|
958
|
+
this.#handleAuthorizationRequest(message);
|
|
959
|
+
break;
|
|
960
|
+
case AGGREGATED_NONCE:
|
|
961
|
+
this.#handleAggregatedNonce(message);
|
|
962
|
+
break;
|
|
963
|
+
case FALLBACK_AUTHORIZATION_REQUEST:
|
|
964
|
+
this.#handleFallbackAuthorizationRequest(message);
|
|
965
|
+
break;
|
|
966
|
+
default:
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
/** Cohorts the participant has discovered but not yet joined. */
|
|
971
|
+
get discoveredCohorts() {
|
|
972
|
+
const map = /* @__PURE__ */ new Map();
|
|
973
|
+
for (const [id, state] of this.#cohortStates) {
|
|
974
|
+
if (state.phase === "Discovered" /* Discovered */ && state.advert) {
|
|
975
|
+
map.set(id, state.advert);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return map;
|
|
979
|
+
}
|
|
980
|
+
#handleCohortAdvert(message) {
|
|
981
|
+
if (!isCohortAdvertMessage(message)) return;
|
|
982
|
+
const { cohortId, network, communicationPk, ...conditions } = message.body;
|
|
983
|
+
if (this.#cohortStates.has(cohortId)) return;
|
|
984
|
+
const advert = {
|
|
985
|
+
cohortId,
|
|
986
|
+
serviceDid: message.from,
|
|
987
|
+
network,
|
|
988
|
+
serviceCommunicationPk: communicationPk,
|
|
989
|
+
...conditions
|
|
990
|
+
};
|
|
991
|
+
this.#cohortStates.set(cohortId, {
|
|
992
|
+
phase: "Discovered" /* Discovered */,
|
|
993
|
+
cohortId,
|
|
994
|
+
serviceDid: message.from,
|
|
995
|
+
advert
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* User action: join a discovered cohort.
|
|
1000
|
+
* Returns the opt-in message to send.
|
|
1001
|
+
*/
|
|
1002
|
+
joinCohort(cohortId) {
|
|
1003
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1004
|
+
if (!state || state.phase !== "Discovered" /* Discovered */) {
|
|
1005
|
+
throw new AggregationParticipantError(
|
|
1006
|
+
`Cannot join cohort ${cohortId}: not in Discovered phase.`,
|
|
1007
|
+
"INVALID_PHASE",
|
|
1008
|
+
{ cohortId, phase: state?.phase }
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
const advert = state.advert;
|
|
1012
|
+
const cohort = new AggregationCohort({
|
|
1013
|
+
id: cohortId,
|
|
1014
|
+
serviceDid: state.serviceDid,
|
|
1015
|
+
minParticipants: advert.minParticipants,
|
|
1016
|
+
network: advert.network,
|
|
1017
|
+
beaconType: advert.beaconType,
|
|
1018
|
+
recoveryKey: advert.recoveryKey ? (0, import_utils3.hexToBytes)(advert.recoveryKey) : void 0,
|
|
1019
|
+
recoverySequence: advert.recoverySequence,
|
|
1020
|
+
fundingModel: advert.fundingModel,
|
|
1021
|
+
fallbackThreshold: advert.fallbackThreshold
|
|
1022
|
+
});
|
|
1023
|
+
state.cohort = cohort;
|
|
1024
|
+
state.phase = "OptedIn" /* OptedIn */;
|
|
1025
|
+
const optInMessage = createCohortOptInMessage({
|
|
1026
|
+
from: this.did,
|
|
1027
|
+
to: state.serviceDid,
|
|
1028
|
+
cohortId,
|
|
1029
|
+
participantPk: this.publicKey,
|
|
1030
|
+
communicationPk: this.publicKey
|
|
1031
|
+
});
|
|
1032
|
+
return [optInMessage];
|
|
1033
|
+
}
|
|
1034
|
+
#handleOptInAccept(message) {
|
|
1035
|
+
void message;
|
|
1036
|
+
}
|
|
1037
|
+
/** Cohorts that have been finalized: beacon address available. */
|
|
1038
|
+
get joinedCohorts() {
|
|
1039
|
+
const map = /* @__PURE__ */ new Map();
|
|
1040
|
+
for (const [id, state] of this.#cohortStates) {
|
|
1041
|
+
if (state.cohort && state.cohort.beaconAddress) {
|
|
1042
|
+
map.set(id, {
|
|
1043
|
+
cohortId: id,
|
|
1044
|
+
serviceDid: state.serviceDid,
|
|
1045
|
+
beaconAddress: state.cohort.beaconAddress,
|
|
1046
|
+
cohortKeys: state.cohort.cohortKeys
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return map;
|
|
1051
|
+
}
|
|
1052
|
+
#handleCohortReady(message) {
|
|
1053
|
+
const cohortId = message.body?.cohortId;
|
|
1054
|
+
if (!cohortId) return;
|
|
1055
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1056
|
+
if (!state || !state.cohort) return;
|
|
1057
|
+
if (state.phase !== "OptedIn" /* OptedIn */) return;
|
|
1058
|
+
const beaconAddress = message.body?.beaconAddress;
|
|
1059
|
+
const cohortKeys = message.body?.cohortKeys;
|
|
1060
|
+
if (!beaconAddress || !cohortKeys) return;
|
|
1061
|
+
const participantPkHex = (0, import_utils3.bytesToHex)(this.publicKey);
|
|
1062
|
+
const cohortKeysHex = cohortKeys.map((k) => (0, import_utils3.bytesToHex)(new Uint8Array(k)));
|
|
1063
|
+
state.cohort.validateMembership(participantPkHex, cohortKeysHex, beaconAddress);
|
|
1064
|
+
state.phase = "CohortReady" /* CohortReady */;
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* User action: submit a signed BTCR2 update for inclusion in the cohort's
|
|
1068
|
+
* aggregated signal.
|
|
1069
|
+
*/
|
|
1070
|
+
submitUpdate(cohortId, signedUpdate) {
|
|
1071
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1072
|
+
if (!state || state.phase !== "CohortReady" /* CohortReady */) {
|
|
1073
|
+
throw new AggregationParticipantError(
|
|
1074
|
+
`Cannot submit update to cohort ${cohortId}: not in CohortReady phase.`,
|
|
1075
|
+
"INVALID_PHASE",
|
|
1076
|
+
{ cohortId, phase: state?.phase }
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
state.submittedUpdate = signedUpdate;
|
|
1080
|
+
state.included = true;
|
|
1081
|
+
state.phase = "UpdateSubmitted" /* UpdateSubmitted */;
|
|
1082
|
+
const message = createSubmitUpdateMessage({
|
|
1083
|
+
from: this.did,
|
|
1084
|
+
to: state.serviceDid,
|
|
1085
|
+
cohortId,
|
|
1086
|
+
signedUpdate
|
|
1087
|
+
});
|
|
1088
|
+
return [message];
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* User action: decline to submit an update this round (cooperative
|
|
1092
|
+
* non-inclusion). The member stays in the cohort and still signs; it will be
|
|
1093
|
+
* absent from the CAS Announcement Map, or carry a non-inclusion leaf in the
|
|
1094
|
+
* SMT. Returns the SUBMIT_NONINCLUDED message to send.
|
|
1095
|
+
*/
|
|
1096
|
+
declineUpdate(cohortId) {
|
|
1097
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1098
|
+
if (!state || state.phase !== "CohortReady" /* CohortReady */) {
|
|
1099
|
+
throw new AggregationParticipantError(
|
|
1100
|
+
`Cannot decline in cohort ${cohortId}: not in CohortReady phase.`,
|
|
1101
|
+
"INVALID_PHASE",
|
|
1102
|
+
{ cohortId, phase: state?.phase }
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
state.included = false;
|
|
1106
|
+
state.phase = "NonIncluded" /* NonIncluded */;
|
|
1107
|
+
const message = createSubmitNonIncludedMessage({
|
|
1108
|
+
from: this.did,
|
|
1109
|
+
to: state.serviceDid,
|
|
1110
|
+
cohortId
|
|
1111
|
+
});
|
|
1112
|
+
return [message];
|
|
1113
|
+
}
|
|
1114
|
+
/** Aggregated data awaiting user validation. */
|
|
1115
|
+
get pendingValidations() {
|
|
1116
|
+
const map = /* @__PURE__ */ new Map();
|
|
1117
|
+
for (const [id, state] of this.#cohortStates) {
|
|
1118
|
+
if (state.phase === "AwaitingValidation" /* AwaitingValidation */ && state.validation) {
|
|
1119
|
+
map.set(id, state.validation);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return map;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* The validated aggregated data retained for a cohort, regardless of phase.
|
|
1126
|
+
* Unlike {@link pendingValidations} (which lists only cohorts still awaiting
|
|
1127
|
+
* the validate decision), this returns the stored validation, including the
|
|
1128
|
+
* participant's sidecar (the CAS Announcement map or its SMT inclusion proof),
|
|
1129
|
+
* so it is still readable once the cohort reaches Complete. Returns
|
|
1130
|
+
* undefined before aggregated data has been received.
|
|
1131
|
+
*/
|
|
1132
|
+
getValidation(cohortId) {
|
|
1133
|
+
return this.#cohortStates.get(cohortId)?.validation;
|
|
1134
|
+
}
|
|
1135
|
+
#handleDistributeAggregatedData(message) {
|
|
1136
|
+
const cohortId = message.body?.cohortId;
|
|
1137
|
+
if (!cohortId) return;
|
|
1138
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1139
|
+
if (!state || state.phase !== "UpdateSubmitted" /* UpdateSubmitted */ && state.phase !== "NonIncluded" /* NonIncluded */) return;
|
|
1140
|
+
const declined = state.included === false;
|
|
1141
|
+
if (!declined && !state.submittedUpdate) return;
|
|
1142
|
+
const beaconType = message.body?.beaconType;
|
|
1143
|
+
if (!beaconType) return;
|
|
1144
|
+
const strategy = getBeaconStrategy(beaconType);
|
|
1145
|
+
if (!strategy) return;
|
|
1146
|
+
const signalBytesHex = message.body?.signalBytesHex ?? "";
|
|
1147
|
+
const expectedHash = declined ? "" : (0, import_common4.canonicalHash)(state.submittedUpdate);
|
|
1148
|
+
const result = strategy.validateParticipantView({
|
|
1149
|
+
participantDid: this.did,
|
|
1150
|
+
included: !declined,
|
|
1151
|
+
submittedUpdate: declined ? void 0 : state.submittedUpdate,
|
|
1152
|
+
expectedHash: declined ? void 0 : expectedHash,
|
|
1153
|
+
body: message.body
|
|
1154
|
+
});
|
|
1155
|
+
state.validation = {
|
|
1156
|
+
cohortId,
|
|
1157
|
+
beaconType,
|
|
1158
|
+
signalBytesHex,
|
|
1159
|
+
expectedHash,
|
|
1160
|
+
matches: result.matches,
|
|
1161
|
+
casAnnouncement: result.casAnnouncement,
|
|
1162
|
+
smtProof: result.smtProof,
|
|
1163
|
+
included: !declined
|
|
1164
|
+
};
|
|
1165
|
+
state.phase = "AwaitingValidation" /* AwaitingValidation */;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* User action: approve aggregated data and send validation ack.
|
|
1169
|
+
*/
|
|
1170
|
+
approveValidation(cohortId) {
|
|
1171
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1172
|
+
if (!state || state.phase !== "AwaitingValidation" /* AwaitingValidation */) {
|
|
1173
|
+
throw new AggregationParticipantError(
|
|
1174
|
+
`Cannot approve validation for cohort ${cohortId}: not in AwaitingValidation phase.`,
|
|
1175
|
+
"INVALID_PHASE",
|
|
1176
|
+
{ cohortId, phase: state?.phase }
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
state.phase = "ValidationSent" /* ValidationSent */;
|
|
1180
|
+
return [createValidationAckMessage({
|
|
1181
|
+
from: this.did,
|
|
1182
|
+
to: state.serviceDid,
|
|
1183
|
+
cohortId,
|
|
1184
|
+
approved: true
|
|
1185
|
+
})];
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* User action: reject aggregated data and send rejection ack.
|
|
1189
|
+
*/
|
|
1190
|
+
rejectValidation(cohortId) {
|
|
1191
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1192
|
+
if (!state || state.phase !== "AwaitingValidation" /* AwaitingValidation */) {
|
|
1193
|
+
throw new AggregationParticipantError(
|
|
1194
|
+
`Cannot reject validation for cohort ${cohortId}: not in AwaitingValidation phase.`,
|
|
1195
|
+
"INVALID_PHASE",
|
|
1196
|
+
{ cohortId, phase: state?.phase }
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
state.phase = "Failed" /* Failed */;
|
|
1200
|
+
return [createValidationAckMessage({
|
|
1201
|
+
from: this.did,
|
|
1202
|
+
to: state.serviceDid,
|
|
1203
|
+
cohortId,
|
|
1204
|
+
approved: false
|
|
1205
|
+
})];
|
|
1206
|
+
}
|
|
1207
|
+
/** Signing requests awaiting user approval. */
|
|
1208
|
+
get pendingSigningRequests() {
|
|
1209
|
+
const map = /* @__PURE__ */ new Map();
|
|
1210
|
+
for (const [id, state] of this.#cohortStates) {
|
|
1211
|
+
if (state.phase === "AwaitingSigning" /* AwaitingSigning */ && state.signingRequest) {
|
|
1212
|
+
map.set(id, state.signingRequest);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return map;
|
|
1216
|
+
}
|
|
1217
|
+
#handleAuthorizationRequest(message) {
|
|
1218
|
+
const cohortId = message.body?.cohortId;
|
|
1219
|
+
if (!cohortId) return;
|
|
1220
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1221
|
+
if (!state || !state.cohort) return;
|
|
1222
|
+
if (state.phase !== "ValidationSent" /* ValidationSent */) return;
|
|
1223
|
+
const sessionId = message.body?.sessionId;
|
|
1224
|
+
const pendingTxHex = message.body?.pendingTx;
|
|
1225
|
+
const prevOutScriptHex = message.body?.prevOutScriptHex;
|
|
1226
|
+
const prevOutValue = message.body?.prevOutValue;
|
|
1227
|
+
if (!sessionId || !pendingTxHex || !prevOutScriptHex || !prevOutValue) return;
|
|
1228
|
+
state.signingRequest = {
|
|
1229
|
+
cohortId,
|
|
1230
|
+
sessionId,
|
|
1231
|
+
pendingTxHex,
|
|
1232
|
+
prevOutScriptHex,
|
|
1233
|
+
prevOutValue
|
|
1234
|
+
};
|
|
1235
|
+
state.phase = "AwaitingSigning" /* AwaitingSigning */;
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Bind a signing approval to the announcement the member validated: a beacon
|
|
1239
|
+
* transaction MUST carry an OP_RETURN with the exact 32-byte signal stored when
|
|
1240
|
+
* the aggregated data was distributed. Both the optimistic nonce approval and
|
|
1241
|
+
* the fallback approval sign with SIGHASH_DEFAULT (committing to every output)
|
|
1242
|
+
* while the coordinator drives output selection, so without this check a
|
|
1243
|
+
* coordinator could anchor a different signal under the member's signature.
|
|
1244
|
+
*/
|
|
1245
|
+
#assertTxAnchorsValidatedSignal(cohortId, state, tx) {
|
|
1246
|
+
const signalHex = state.validation?.signalBytesHex;
|
|
1247
|
+
if (!signalHex) {
|
|
1248
|
+
throw new AggregationParticipantError(
|
|
1249
|
+
`Cohort ${cohortId} has no validated signal to bind the signature to.`,
|
|
1250
|
+
"MISSING_STATE",
|
|
1251
|
+
{ cohortId }
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
if (!txEmbedsSignal(tx, signalHex)) {
|
|
1255
|
+
throw new AggregationParticipantError(
|
|
1256
|
+
`Transaction for cohort ${cohortId} does not anchor the validated signal.`,
|
|
1257
|
+
"SIGNAL_MISMATCH",
|
|
1258
|
+
{ cohortId }
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* User action: approve signing and generate nonce contribution.
|
|
1264
|
+
*/
|
|
1265
|
+
approveNonce(cohortId) {
|
|
1266
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1267
|
+
if (!state || state.phase !== "AwaitingSigning" /* AwaitingSigning */) {
|
|
1268
|
+
throw new AggregationParticipantError(
|
|
1269
|
+
`Cannot approve nonce for cohort ${cohortId}: not in AwaitingSigning phase.`,
|
|
1270
|
+
"INVALID_PHASE",
|
|
1271
|
+
{ cohortId, phase: state?.phase }
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
if (!state.signingRequest || !state.cohort) {
|
|
1275
|
+
throw new AggregationParticipantError(
|
|
1276
|
+
`Cohort ${cohortId} missing signing request or cohort state.`,
|
|
1277
|
+
"MISSING_STATE",
|
|
1278
|
+
{ cohortId }
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
const tx = import_btc_signer5.Transaction.fromRaw((0, import_utils3.hexToBytes)(state.signingRequest.pendingTxHex), { allowUnknownOutputs: true });
|
|
1282
|
+
this.#assertTxAnchorsValidatedSignal(cohortId, state, tx);
|
|
1283
|
+
const prevOutScripts = [(0, import_utils3.hexToBytes)(state.signingRequest.prevOutScriptHex)];
|
|
1284
|
+
const prevOutValues = [BigInt(state.signingRequest.prevOutValue)];
|
|
1285
|
+
const session = new BeaconSigningSession({
|
|
1286
|
+
id: state.signingRequest.sessionId,
|
|
1287
|
+
cohort: state.cohort,
|
|
1288
|
+
pendingTx: tx,
|
|
1289
|
+
prevOutScripts,
|
|
1290
|
+
prevOutValues
|
|
1291
|
+
});
|
|
1292
|
+
state.signingSession = session;
|
|
1293
|
+
const nonceContribution = this.#signer.withSecret(
|
|
1294
|
+
(secretKey) => session.generateNonceContribution(this.publicKey, secretKey)
|
|
1295
|
+
);
|
|
1296
|
+
state.phase = "NonceSent" /* NonceSent */;
|
|
1297
|
+
return [createNonceContributionMessage({
|
|
1298
|
+
from: this.did,
|
|
1299
|
+
to: state.serviceDid,
|
|
1300
|
+
cohortId,
|
|
1301
|
+
sessionId: session.id,
|
|
1302
|
+
nonceContribution
|
|
1303
|
+
})];
|
|
1304
|
+
}
|
|
1305
|
+
#handleAggregatedNonce(message) {
|
|
1306
|
+
const cohortId = message.body?.cohortId;
|
|
1307
|
+
if (!cohortId) return;
|
|
1308
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1309
|
+
if (!state || !state.signingSession) return;
|
|
1310
|
+
if (state.phase !== "NonceSent" /* NonceSent */) return;
|
|
1311
|
+
const aggregatedNonce = message.body?.aggregatedNonce;
|
|
1312
|
+
if (!aggregatedNonce) return;
|
|
1313
|
+
state.signingSession.aggregatedNonce = aggregatedNonce;
|
|
1314
|
+
state.phase = "AwaitingPartialSig" /* AwaitingPartialSig */;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* User action: generate and return the partial signature.
|
|
1318
|
+
* In most UIs this is automatic after AwaitingPartialSig, but exposing it
|
|
1319
|
+
* as an explicit action lets the client UI confirm before signing if desired.
|
|
1320
|
+
*/
|
|
1321
|
+
generatePartialSignature(cohortId) {
|
|
1322
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1323
|
+
if (!state || state.phase !== "AwaitingPartialSig" /* AwaitingPartialSig */) {
|
|
1324
|
+
throw new AggregationParticipantError(
|
|
1325
|
+
`Cannot generate partial signature for cohort ${cohortId}: not in AwaitingPartialSig phase.`,
|
|
1326
|
+
"INVALID_PHASE",
|
|
1327
|
+
{ cohortId, phase: state?.phase }
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
if (!state.signingSession) {
|
|
1331
|
+
throw new AggregationParticipantError(
|
|
1332
|
+
`No signing session for cohort ${cohortId}.`,
|
|
1333
|
+
"MISSING_STATE",
|
|
1334
|
+
{ cohortId }
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
const signingSession = state.signingSession;
|
|
1338
|
+
const partialSig = this.#signer.withSecret(
|
|
1339
|
+
(secretKey) => signingSession.generatePartialSignature(secretKey)
|
|
1340
|
+
);
|
|
1341
|
+
state.phase = "Complete" /* Complete */;
|
|
1342
|
+
return [createSignatureAuthorizationMessage({
|
|
1343
|
+
from: this.did,
|
|
1344
|
+
to: state.serviceDid,
|
|
1345
|
+
cohortId,
|
|
1346
|
+
sessionId: state.signingSession.id,
|
|
1347
|
+
partialSignature: partialSig
|
|
1348
|
+
})];
|
|
1349
|
+
}
|
|
1350
|
+
/** Fallback signing requests awaiting user approval (UI: review the fallback spend). */
|
|
1351
|
+
get pendingFallbackRequests() {
|
|
1352
|
+
const map = /* @__PURE__ */ new Map();
|
|
1353
|
+
for (const [id, state] of this.#cohortStates) {
|
|
1354
|
+
if (state.phase === "AwaitingFallbackSig" /* AwaitingFallbackSig */ && state.fallbackRequest) {
|
|
1355
|
+
map.set(id, state.fallbackRequest);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return map;
|
|
1359
|
+
}
|
|
1360
|
+
#handleFallbackAuthorizationRequest(message) {
|
|
1361
|
+
const cohortId = message.body?.cohortId;
|
|
1362
|
+
if (!cohortId) return;
|
|
1363
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1364
|
+
if (!state || !state.cohort) return;
|
|
1365
|
+
const acceptFrom = [
|
|
1366
|
+
"ValidationSent" /* ValidationSent */,
|
|
1367
|
+
"AwaitingSigning" /* AwaitingSigning */,
|
|
1368
|
+
"NonceSent" /* NonceSent */,
|
|
1369
|
+
"AwaitingPartialSig" /* AwaitingPartialSig */,
|
|
1370
|
+
"Complete" /* Complete */
|
|
1371
|
+
];
|
|
1372
|
+
if (!acceptFrom.includes(state.phase)) return;
|
|
1373
|
+
const sessionId = message.body?.sessionId;
|
|
1374
|
+
const pendingTxHex = message.body?.pendingTx;
|
|
1375
|
+
const prevOutScriptHex = message.body?.prevOutScriptHex;
|
|
1376
|
+
const prevOutValue = message.body?.prevOutValue;
|
|
1377
|
+
const fallbackLeafScriptHex = message.body?.fallbackLeafScriptHex;
|
|
1378
|
+
if (!sessionId || !pendingTxHex || !prevOutScriptHex || !prevOutValue || !fallbackLeafScriptHex) return;
|
|
1379
|
+
state.fallbackRequest = { cohortId, sessionId, pendingTxHex, prevOutScriptHex, prevOutValue, fallbackLeafScriptHex };
|
|
1380
|
+
state.signingSession?.clearSecrets();
|
|
1381
|
+
state.phase = "AwaitingFallbackSig" /* AwaitingFallbackSig */;
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* User action: authorize the fallback spend. Recomputes the k-of-n fallback
|
|
1385
|
+
* leaf from the member's OWN cohort state (not the service-provided script),
|
|
1386
|
+
* computes the BIP-341 script-path sighash over the requested transaction, and
|
|
1387
|
+
* returns a standalone BIP-340 signature (no nonce round). The member completes
|
|
1388
|
+
* once it has contributed; the service needs only k of these.
|
|
1389
|
+
*/
|
|
1390
|
+
approveFallback(cohortId) {
|
|
1391
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1392
|
+
if (!state || state.phase !== "AwaitingFallbackSig" /* AwaitingFallbackSig */) {
|
|
1393
|
+
throw new AggregationParticipantError(
|
|
1394
|
+
`Cannot approve fallback for cohort ${cohortId}: not in AwaitingFallbackSig phase.`,
|
|
1395
|
+
"INVALID_PHASE",
|
|
1396
|
+
{ cohortId, phase: state?.phase }
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
if (!state.fallbackRequest || !state.cohort) {
|
|
1400
|
+
throw new AggregationParticipantError(
|
|
1401
|
+
`Cohort ${cohortId} missing fallback request or cohort state.`,
|
|
1402
|
+
"MISSING_STATE",
|
|
1403
|
+
{ cohortId }
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
const req = state.fallbackRequest;
|
|
1407
|
+
const tx = import_btc_signer5.Transaction.fromRaw((0, import_utils3.hexToBytes)(req.pendingTxHex), { allowUnknownOutputs: true });
|
|
1408
|
+
const prevOutScript = (0, import_utils3.hexToBytes)(req.prevOutScriptHex);
|
|
1409
|
+
const prevOutValue = BigInt(req.prevOutValue);
|
|
1410
|
+
this.#assertTxAnchorsValidatedSignal(cohortId, state, tx);
|
|
1411
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
1412
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
1413
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold
|
|
1414
|
+
});
|
|
1415
|
+
const sighash = fallbackSighash(tx, 0, prevOutScript, prevOutValue, fallbackLeaf);
|
|
1416
|
+
const signature = this.#signer.withSecret((secretKey) => import_secp256k14.schnorr.sign(sighash, secretKey));
|
|
1417
|
+
state.phase = "Complete" /* Complete */;
|
|
1418
|
+
return [createFallbackSignatureMessage({
|
|
1419
|
+
from: this.did,
|
|
1420
|
+
to: state.serviceDid,
|
|
1421
|
+
cohortId,
|
|
1422
|
+
sessionId: req.sessionId,
|
|
1423
|
+
signerPk: this.publicKey.slice(1),
|
|
1424
|
+
fallbackSignature: signature
|
|
1425
|
+
})];
|
|
1426
|
+
}
|
|
1427
|
+
getCohortPhase(cohortId) {
|
|
1428
|
+
return this.#cohortStates.get(cohortId)?.phase;
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Zeroize any retained MuSig2 secret nonces across all cohorts. The raw
|
|
1432
|
+
* signing key is never held here (it lives behind the {@link AggregationSigner}
|
|
1433
|
+
* and is wiped per-operation), but an abandoned signing session can still hold
|
|
1434
|
+
* a secret nonce; call this on teardown to clear it deterministically.
|
|
1435
|
+
*/
|
|
1436
|
+
clearSecrets() {
|
|
1437
|
+
for (const state of this.#cohortStates.values()) {
|
|
1438
|
+
state.signingSession?.clearSecrets();
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
// src/core/signer.ts
|
|
1444
|
+
var import_keypair2 = require("@did-btcr2/keypair");
|
|
1445
|
+
var KeyPairAggregationSigner = class {
|
|
1446
|
+
publicKey;
|
|
1447
|
+
#keys;
|
|
1448
|
+
/** @param {SchnorrKeyPair} keys The keypair whose secret backs this signer. */
|
|
1449
|
+
constructor(keys) {
|
|
1450
|
+
this.#keys = keys;
|
|
1451
|
+
this.publicKey = keys.publicKey.compressed;
|
|
1452
|
+
}
|
|
1453
|
+
withSecret(fn) {
|
|
1454
|
+
const secretKey = this.#keys.secretKey.bytes;
|
|
1455
|
+
try {
|
|
1456
|
+
return fn(secretKey);
|
|
1457
|
+
} finally {
|
|
1458
|
+
(0, import_keypair2.wipe)(secretKey);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
// src/core/typed-emitter.ts
|
|
1464
|
+
var TypedEventEmitter = class {
|
|
1465
|
+
#listeners = /* @__PURE__ */ new Map();
|
|
1466
|
+
/** Subscribe to an event. */
|
|
1467
|
+
on(event, listener) {
|
|
1468
|
+
let set = this.#listeners.get(event);
|
|
1469
|
+
if (!set) {
|
|
1470
|
+
set = /* @__PURE__ */ new Set();
|
|
1471
|
+
this.#listeners.set(event, set);
|
|
1472
|
+
}
|
|
1473
|
+
set.add(listener);
|
|
1474
|
+
return this;
|
|
1475
|
+
}
|
|
1476
|
+
/** Subscribe to an event, automatically unsubscribing after the first call. */
|
|
1477
|
+
once(event, listener) {
|
|
1478
|
+
const wrapped = ((...args) => {
|
|
1479
|
+
this.off(event, wrapped);
|
|
1480
|
+
listener(...args);
|
|
1481
|
+
});
|
|
1482
|
+
return this.on(event, wrapped);
|
|
1483
|
+
}
|
|
1484
|
+
/** Unsubscribe a specific listener. */
|
|
1485
|
+
off(event, listener) {
|
|
1486
|
+
this.#listeners.get(event)?.delete(listener);
|
|
1487
|
+
return this;
|
|
1488
|
+
}
|
|
1489
|
+
/** Emit an event to all subscribed listeners. */
|
|
1490
|
+
emit(event, ...args) {
|
|
1491
|
+
const set = this.#listeners.get(event);
|
|
1492
|
+
if (!set || set.size === 0) return false;
|
|
1493
|
+
for (const listener of set) {
|
|
1494
|
+
try {
|
|
1495
|
+
listener(...args);
|
|
1496
|
+
} catch (err) {
|
|
1497
|
+
console.error(`Listener for event "${String(event)}" threw:`, err);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return true;
|
|
1501
|
+
}
|
|
1502
|
+
/** Remove all listeners (optionally for a specific event). */
|
|
1503
|
+
removeAllListeners(event) {
|
|
1504
|
+
if (event === void 0) {
|
|
1505
|
+
this.#listeners.clear();
|
|
1506
|
+
} else {
|
|
1507
|
+
this.#listeners.delete(event);
|
|
1508
|
+
}
|
|
1509
|
+
return this;
|
|
1510
|
+
}
|
|
1511
|
+
/** Number of listeners for an event. */
|
|
1512
|
+
listenerCount(event) {
|
|
1513
|
+
return this.#listeners.get(event)?.size ?? 0;
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
|
|
1517
|
+
// src/participant/participant-runner.ts
|
|
1518
|
+
var AggregationParticipantRunner = class _AggregationParticipantRunner extends TypedEventEmitter {
|
|
1519
|
+
/** Direct access to the underlying state machine for advanced use. */
|
|
1520
|
+
session;
|
|
1521
|
+
#transport;
|
|
1522
|
+
#did;
|
|
1523
|
+
#shouldJoin;
|
|
1524
|
+
#onProvideUpdate;
|
|
1525
|
+
#onValidateData;
|
|
1526
|
+
#onApproveSigning;
|
|
1527
|
+
#handlersRegistered = false;
|
|
1528
|
+
#stopped = false;
|
|
1529
|
+
constructor(options) {
|
|
1530
|
+
super();
|
|
1531
|
+
this.#transport = options.transport;
|
|
1532
|
+
this.#did = options.did;
|
|
1533
|
+
this.#shouldJoin = options.shouldJoin ?? (async () => false);
|
|
1534
|
+
this.#onProvideUpdate = options.onProvideUpdate;
|
|
1535
|
+
this.#onValidateData = options.onValidateData ?? (async (info) => ({ approved: info.matches }));
|
|
1536
|
+
this.#onApproveSigning = options.onApproveSigning ?? (async () => ({ approved: true }));
|
|
1537
|
+
this.session = new AggregationParticipant({
|
|
1538
|
+
did: options.did,
|
|
1539
|
+
signer: new KeyPairAggregationSigner(options.keys)
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Start listening for cohorts. The runner stays active until {@link stop}
|
|
1544
|
+
* is called or the underlying transport disconnects.
|
|
1545
|
+
*/
|
|
1546
|
+
async start() {
|
|
1547
|
+
this.#registerHandlers();
|
|
1548
|
+
}
|
|
1549
|
+
/** Stop the runner and detach transport handlers. Safe to call repeatedly. */
|
|
1550
|
+
stop() {
|
|
1551
|
+
this.#stopped = true;
|
|
1552
|
+
this.#unregisterHandlers();
|
|
1553
|
+
this.session.clearSecrets();
|
|
1554
|
+
}
|
|
1555
|
+
/** Message types this runner listens for on the transport. */
|
|
1556
|
+
static #HANDLED_MESSAGE_TYPES = [
|
|
1557
|
+
COHORT_ADVERT,
|
|
1558
|
+
COHORT_OPT_IN_ACCEPT,
|
|
1559
|
+
COHORT_READY,
|
|
1560
|
+
DISTRIBUTE_AGGREGATED_DATA,
|
|
1561
|
+
AUTHORIZATION_REQUEST,
|
|
1562
|
+
AGGREGATED_NONCE,
|
|
1563
|
+
FALLBACK_AUTHORIZATION_REQUEST
|
|
1564
|
+
];
|
|
1565
|
+
/** Internal: detach from the transport. Safe to call repeatedly. */
|
|
1566
|
+
#unregisterHandlers() {
|
|
1567
|
+
if (!this.#handlersRegistered) return;
|
|
1568
|
+
this.#handlersRegistered = false;
|
|
1569
|
+
for (const type of _AggregationParticipantRunner.#HANDLED_MESSAGE_TYPES) {
|
|
1570
|
+
this.#transport.unregisterMessageHandler(this.#did, type);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Single-shot helper: start, join the first cohort that passes `shouldJoin`,
|
|
1575
|
+
* drive it to completion, and resolve. Convenient for tests and demos. The
|
|
1576
|
+
* single-cohort special case of {@link joinMatching} (count = 1).
|
|
1577
|
+
*/
|
|
1578
|
+
static async joinFirst(options) {
|
|
1579
|
+
return new Promise((resolve, reject) => {
|
|
1580
|
+
const runner = new _AggregationParticipantRunner(options);
|
|
1581
|
+
runner.once("cohort-complete", (info) => {
|
|
1582
|
+
runner.stop();
|
|
1583
|
+
resolve(info);
|
|
1584
|
+
});
|
|
1585
|
+
runner.on("error", reject);
|
|
1586
|
+
runner.start().catch(reject);
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Multi-cohort helper: start, join EVERY cohort whose advert passes
|
|
1591
|
+
* `shouldJoin`, drive each to completion in parallel, and resolve once
|
|
1592
|
+
* `count` cohorts have completed (the runner stops at that point). The
|
|
1593
|
+
* N-cohort generalization of {@link joinFirst}, for a participant that joins
|
|
1594
|
+
* several cohorts advertised by one service.
|
|
1595
|
+
*
|
|
1596
|
+
* For an open-ended, long-lived subscriber (no fixed count), construct an
|
|
1597
|
+
* {@link AggregationParticipantRunner} directly, set `shouldJoin`, call
|
|
1598
|
+
* `start()`, and listen for `cohort-complete` - the runner already drives
|
|
1599
|
+
* any number of cohorts concurrently.
|
|
1600
|
+
*
|
|
1601
|
+
* @param options Participant runner options (set `shouldJoin` to select cohorts).
|
|
1602
|
+
* @param count Number of completed cohorts to collect before resolving.
|
|
1603
|
+
* @returns The {@link CohortCompleteInfo} for each completed cohort, in completion order.
|
|
1604
|
+
*/
|
|
1605
|
+
static async joinMatching(options, count) {
|
|
1606
|
+
return new Promise((resolve, reject) => {
|
|
1607
|
+
const runner = new _AggregationParticipantRunner(options);
|
|
1608
|
+
const completed = [];
|
|
1609
|
+
runner.on("cohort-complete", (info) => {
|
|
1610
|
+
completed.push(info);
|
|
1611
|
+
if (completed.length >= count) {
|
|
1612
|
+
runner.stop();
|
|
1613
|
+
resolve(completed);
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
runner.on("error", reject);
|
|
1617
|
+
runner.start().catch(reject);
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Internal: handler registration with the transport. Idempotent and safe to call multiple times,
|
|
1622
|
+
* but only registers handlers once.
|
|
1623
|
+
*/
|
|
1624
|
+
#registerHandlers() {
|
|
1625
|
+
if (this.#handlersRegistered) return;
|
|
1626
|
+
this.#handlersRegistered = true;
|
|
1627
|
+
this.#transport.registerMessageHandler(this.#did, COHORT_ADVERT, this.#handleAdvert.bind(this));
|
|
1628
|
+
this.#transport.registerMessageHandler(this.#did, COHORT_OPT_IN_ACCEPT, this.#handleOptInAccept.bind(this));
|
|
1629
|
+
this.#transport.registerMessageHandler(this.#did, COHORT_READY, this.#handleCohortReady.bind(this));
|
|
1630
|
+
this.#transport.registerMessageHandler(this.#did, DISTRIBUTE_AGGREGATED_DATA, this.#handleDistributeData.bind(this));
|
|
1631
|
+
this.#transport.registerMessageHandler(this.#did, AUTHORIZATION_REQUEST, this.#handleAuthorizationRequest.bind(this));
|
|
1632
|
+
this.#transport.registerMessageHandler(this.#did, AGGREGATED_NONCE, this.#handleAggregatedNonce.bind(this));
|
|
1633
|
+
this.#transport.registerMessageHandler(this.#did, FALLBACK_AUTHORIZATION_REQUEST, this.#handleFallbackAuthorizationRequest.bind(this));
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Internal: handler for cohort adverts. Applies the `shouldJoin` filter and joins if approved.
|
|
1637
|
+
* @param {BaseMessage} msg - The received cohort advert message.
|
|
1638
|
+
* @returns {Promise<void>} Resolves when processing is complete.
|
|
1639
|
+
*/
|
|
1640
|
+
async #handleAdvert(msg) {
|
|
1641
|
+
if (this.#stopped) return;
|
|
1642
|
+
try {
|
|
1643
|
+
this.session.receive(msg);
|
|
1644
|
+
const advert = this.session.discoveredCohorts.get(msg.body?.cohortId ?? "");
|
|
1645
|
+
if (!advert) return;
|
|
1646
|
+
this.emit("cohort-discovered", advert);
|
|
1647
|
+
if (advert.serviceCommunicationPk) {
|
|
1648
|
+
this.#transport.registerPeer(advert.serviceDid, advert.serviceCommunicationPk);
|
|
1649
|
+
}
|
|
1650
|
+
const join = await this.#shouldJoin(advert);
|
|
1651
|
+
if (!join) return;
|
|
1652
|
+
await this.#sendAll(this.session.joinCohort(advert.cohortId));
|
|
1653
|
+
this.emit("cohort-joined", { cohortId: advert.cohortId });
|
|
1654
|
+
} catch (err) {
|
|
1655
|
+
this.emit("error", err);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Internal: handler for opt-in accept messages. Updates the session state accordingly.
|
|
1660
|
+
* @param {BaseMessage} msg - The received opt-in accept message.
|
|
1661
|
+
*/
|
|
1662
|
+
#handleOptInAccept(msg) {
|
|
1663
|
+
if (this.#stopped) return;
|
|
1664
|
+
try {
|
|
1665
|
+
this.session.receive(msg);
|
|
1666
|
+
} catch (err) {
|
|
1667
|
+
this.emit("error", err);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Internal: handler for cohort ready messages. Updates the session state, emits a 'cohort-ready'
|
|
1672
|
+
* event, and triggers the update submission flow via the `onProvideUpdate` callback.
|
|
1673
|
+
* @param {BaseMessage} msg - The received cohort ready message.
|
|
1674
|
+
* @returns {Promise<void>} Resolves when processing is complete.
|
|
1675
|
+
*/
|
|
1676
|
+
async #handleCohortReady(msg) {
|
|
1677
|
+
if (this.#stopped) return;
|
|
1678
|
+
try {
|
|
1679
|
+
this.session.receive(msg);
|
|
1680
|
+
const cohortId = msg.body?.cohortId;
|
|
1681
|
+
if (!cohortId) return;
|
|
1682
|
+
const info = this.session.joinedCohorts.get(cohortId);
|
|
1683
|
+
if (!info) return;
|
|
1684
|
+
this.emit("cohort-ready", { cohortId, beaconAddress: info.beaconAddress });
|
|
1685
|
+
const signedUpdate = await this.#onProvideUpdate({
|
|
1686
|
+
cohortId,
|
|
1687
|
+
beaconAddress: info.beaconAddress
|
|
1688
|
+
});
|
|
1689
|
+
if (signedUpdate === null) {
|
|
1690
|
+
await this.#sendAll(this.session.declineUpdate(cohortId));
|
|
1691
|
+
this.emit("update-declined", { cohortId });
|
|
1692
|
+
} else {
|
|
1693
|
+
await this.#sendAll(this.session.submitUpdate(cohortId, signedUpdate));
|
|
1694
|
+
this.emit("update-submitted", { cohortId });
|
|
1695
|
+
}
|
|
1696
|
+
} catch (err) {
|
|
1697
|
+
this.emit("error", err);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Internal: handler for distribute aggregated data messages. Updates the session state, emits a
|
|
1702
|
+
* 'validation-requested' event, and triggers the validation decision flow via the `onValidateData`
|
|
1703
|
+
* callback. Depending on the decision, sends an approval or rejection message.
|
|
1704
|
+
* @param {BaseMessage} msg - The received distribute aggregated data message.
|
|
1705
|
+
* @returns {Promise<void>} Resolves when processing is complete.
|
|
1706
|
+
* @throws {Error} If an error occurs during message processing or callback execution.
|
|
1707
|
+
*/
|
|
1708
|
+
async #handleDistributeData(msg) {
|
|
1709
|
+
if (this.#stopped) return;
|
|
1710
|
+
try {
|
|
1711
|
+
this.session.receive(msg);
|
|
1712
|
+
const cohortId = msg.body?.cohortId;
|
|
1713
|
+
if (!cohortId) return;
|
|
1714
|
+
const validation = this.session.pendingValidations.get(cohortId);
|
|
1715
|
+
if (!validation) return;
|
|
1716
|
+
this.emit("validation-requested", validation);
|
|
1717
|
+
const decision = await this.#onValidateData(validation);
|
|
1718
|
+
if (decision.approved) {
|
|
1719
|
+
await this.#sendAll(this.session.approveValidation(cohortId));
|
|
1720
|
+
} else {
|
|
1721
|
+
await this.#sendAll(this.session.rejectValidation(cohortId));
|
|
1722
|
+
this.emit("cohort-failed", { cohortId, reason: "Validation rejected by participant" });
|
|
1723
|
+
}
|
|
1724
|
+
} catch (err) {
|
|
1725
|
+
this.emit("error", err);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Internal: handler for authorization request messages. Updates the session state, emits a
|
|
1730
|
+
* 'signing-requested' event, and triggers the signing approval flow via the `onApproveSigning`
|
|
1731
|
+
* callback. Depending on the decision, sends a nonce approval message or emits a 'cohort-failed'
|
|
1732
|
+
* event.
|
|
1733
|
+
* @param {BaseMessage} msg - The received authorization request message.
|
|
1734
|
+
* @returns {Promise<void>} Resolves when processing is complete.
|
|
1735
|
+
* @throws {Error} If an error occurs during message processing or callback execution.
|
|
1736
|
+
*/
|
|
1737
|
+
async #handleAuthorizationRequest(msg) {
|
|
1738
|
+
if (this.#stopped) return;
|
|
1739
|
+
try {
|
|
1740
|
+
this.session.receive(msg);
|
|
1741
|
+
const cohortId = msg.body?.cohortId;
|
|
1742
|
+
if (!cohortId) return;
|
|
1743
|
+
const req = this.session.pendingSigningRequests.get(cohortId);
|
|
1744
|
+
if (!req) return;
|
|
1745
|
+
this.emit("signing-requested", req);
|
|
1746
|
+
const decision = await this.#onApproveSigning(req);
|
|
1747
|
+
if (!decision.approved) {
|
|
1748
|
+
this.emit("cohort-failed", { cohortId, reason: "Signing rejected by participant" });
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
await this.#sendAll(this.session.approveNonce(cohortId));
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
this.emit("error", err);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Internal: handler for aggregated nonce messages. Updates the session state, triggers partial
|
|
1758
|
+
* signature generation, and sends the partial signature to the aggregator. If the cohort reaches
|
|
1759
|
+
* completion after processing the nonce, emits a 'cohort-complete' event.
|
|
1760
|
+
* @param {BaseMessage} msg - The received aggregated nonce message.
|
|
1761
|
+
* @returns {Promise<void>} Resolves when processing is complete.
|
|
1762
|
+
* @throws {Error} If an error occurs during message processing or partial signature generation.
|
|
1763
|
+
*/
|
|
1764
|
+
async #handleAggregatedNonce(msg) {
|
|
1765
|
+
if (this.#stopped) return;
|
|
1766
|
+
try {
|
|
1767
|
+
this.session.receive(msg);
|
|
1768
|
+
const cohortId = msg.body?.cohortId;
|
|
1769
|
+
if (!cohortId) return;
|
|
1770
|
+
await this.#sendAll(this.session.generatePartialSignature(cohortId));
|
|
1771
|
+
this.#emitCohortCompleteIfDone(cohortId);
|
|
1772
|
+
} catch (err) {
|
|
1773
|
+
this.emit("error", err);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Internal: handler for fallback authorization requests. The service abandoned
|
|
1778
|
+
* the optimistic key path; the member authorizes the k-of-n script-path spend
|
|
1779
|
+
* of the same beacon transaction (ADR 042). Reuses `onApproveSigning` to gate
|
|
1780
|
+
* the decision, signs the fallback, and completes once it has contributed (the
|
|
1781
|
+
* service needs only k members).
|
|
1782
|
+
* @param {BaseMessage} msg - The received fallback authorization request message.
|
|
1783
|
+
* @returns {Promise<void>} Resolves when processing is complete.
|
|
1784
|
+
*/
|
|
1785
|
+
async #handleFallbackAuthorizationRequest(msg) {
|
|
1786
|
+
if (this.#stopped) return;
|
|
1787
|
+
try {
|
|
1788
|
+
this.session.receive(msg);
|
|
1789
|
+
const cohortId = msg.body?.cohortId;
|
|
1790
|
+
if (!cohortId) return;
|
|
1791
|
+
const req = this.session.pendingFallbackRequests.get(cohortId);
|
|
1792
|
+
if (!req) return;
|
|
1793
|
+
this.emit("fallback-requested", req);
|
|
1794
|
+
const decision = await this.#onApproveSigning(req);
|
|
1795
|
+
if (!decision.approved) {
|
|
1796
|
+
this.emit("cohort-failed", { cohortId, reason: "Fallback signing rejected by participant" });
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
await this.#sendAll(this.session.approveFallback(cohortId));
|
|
1800
|
+
this.#emitCohortCompleteIfDone(cohortId);
|
|
1801
|
+
} catch (err) {
|
|
1802
|
+
this.emit("error", err);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Internal: emit `cohort-complete` with the participant's retained sidecar once
|
|
1807
|
+
* the cohort has reached the Complete phase. Surfaces the CAS Announcement map
|
|
1808
|
+
* (CAS beacons) or the SMT proof (SMT beacons) the participant keeps for future
|
|
1809
|
+
* DID resolution. Read via getValidation (not pendingValidations, which lists
|
|
1810
|
+
* only the AwaitingValidation phase) so the sidecar is still available at
|
|
1811
|
+
* Complete. Shared by the optimistic and fallback completion paths.
|
|
1812
|
+
*/
|
|
1813
|
+
#emitCohortCompleteIfDone(cohortId) {
|
|
1814
|
+
if (this.session.getCohortPhase(cohortId) !== "Complete" /* Complete */) return;
|
|
1815
|
+
const info = this.session.joinedCohorts.get(cohortId);
|
|
1816
|
+
if (!info) return;
|
|
1817
|
+
const validation = this.session.getValidation(cohortId);
|
|
1818
|
+
this.emit("cohort-complete", {
|
|
1819
|
+
cohortId,
|
|
1820
|
+
beaconAddress: info.beaconAddress,
|
|
1821
|
+
beaconType: validation?.beaconType ?? "",
|
|
1822
|
+
included: validation?.included ?? true,
|
|
1823
|
+
casAnnouncement: validation?.casAnnouncement,
|
|
1824
|
+
smtProof: validation?.smtProof
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Internal: send helper to ensure messages are sent sequentially. This is important for protocol
|
|
1829
|
+
* correctness, as some transports may not guarantee message order if sent in parallel.
|
|
1830
|
+
* @param {BaseMessage[]} msgs - The messages to send.
|
|
1831
|
+
* @returns {Promise<void>} Resolves when all messages have been sent.
|
|
1832
|
+
* @throws {Error} If an error occurs during message sending.
|
|
1833
|
+
*/
|
|
1834
|
+
async #sendAll(msgs) {
|
|
1835
|
+
for (const m of msgs) {
|
|
1836
|
+
await this.#transport.sendMessage(m, this.#did, m.to);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
|
|
1841
|
+
// src/participant/http-client.ts
|
|
1842
|
+
var import_keypair3 = require("@did-btcr2/keypair");
|
|
1843
|
+
|
|
1844
|
+
// src/core/logger.ts
|
|
1845
|
+
var CONSOLE_LOGGER = {
|
|
1846
|
+
debug: (msg, ...args) => console.debug(msg, ...args),
|
|
1847
|
+
info: (msg, ...args) => console.info(msg, ...args),
|
|
1848
|
+
warn: (msg, ...args) => console.warn(msg, ...args),
|
|
1849
|
+
error: (msg, ...args) => console.error(msg, ...args)
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1852
|
+
// src/core/transport/http/envelope.ts
|
|
1853
|
+
var import_common6 = require("@did-btcr2/common");
|
|
1854
|
+
var import_utils4 = require("@noble/hashes/utils");
|
|
1855
|
+
|
|
1856
|
+
// src/core/transport/error.ts
|
|
1857
|
+
var import_common5 = require("@did-btcr2/common");
|
|
1858
|
+
var TransportAdapterError = class extends import_common5.MethodError {
|
|
1859
|
+
constructor(message, type = "TransportAdapterError", data) {
|
|
1860
|
+
super(message, type, data);
|
|
1861
|
+
}
|
|
1862
|
+
};
|
|
1863
|
+
|
|
1864
|
+
// src/core/transport/http/errors.ts
|
|
1865
|
+
var HttpTransportError = class extends TransportAdapterError {
|
|
1866
|
+
constructor(message, type = "HttpTransportError", data) {
|
|
1867
|
+
super(message, type, { adapter: "http", ...data ?? {} });
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
|
|
1871
|
+
// src/core/transport/http/protocol.ts
|
|
1872
|
+
var HTTP_ENVELOPE_VERSION = 1;
|
|
1873
|
+
var HTTP_ROUTE = {
|
|
1874
|
+
ADVERTS: "/v1/adverts",
|
|
1875
|
+
MESSAGES: "/v1/messages",
|
|
1876
|
+
ACTOR_INBOX: "/v1/actors/{did}/inbox",
|
|
1877
|
+
WELL_KNOWN: "/v1/.well-known/aggregation"
|
|
1878
|
+
};
|
|
1879
|
+
var SSE_EVENT = {
|
|
1880
|
+
ADVERT: "advert",
|
|
1881
|
+
MESSAGE: "message",
|
|
1882
|
+
HEARTBEAT: "heartbeat"
|
|
1883
|
+
};
|
|
1884
|
+
var DEFAULT_CLOCK_SKEW_SEC = 60;
|
|
1885
|
+
var DEFAULT_NONCE_LEN_BYTES = 16;
|
|
1886
|
+
|
|
1887
|
+
// src/core/transport/http/envelope.ts
|
|
1888
|
+
function signEnvelope(message, sender, opts = {}) {
|
|
1889
|
+
const timestamp = opts.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
1890
|
+
const nonce = opts.nonce ?? (0, import_utils4.bytesToHex)((0, import_utils4.randomBytes)(DEFAULT_NONCE_LEN_BYTES));
|
|
1891
|
+
const messageJson = normalizeForWire(normalizeMessage(message));
|
|
1892
|
+
const unsigned = {
|
|
1893
|
+
v: HTTP_ENVELOPE_VERSION,
|
|
1894
|
+
from: sender.did,
|
|
1895
|
+
...opts.to !== void 0 ? { to: opts.to } : {},
|
|
1896
|
+
timestamp,
|
|
1897
|
+
nonce,
|
|
1898
|
+
message: messageJson
|
|
1899
|
+
};
|
|
1900
|
+
const digest = (0, import_common6.canonicalHashBytes)(unsigned);
|
|
1901
|
+
const sig = sender.keys.secretKey.sign(digest, { scheme: "schnorr" });
|
|
1902
|
+
return { ...unsigned, sig: (0, import_utils4.bytesToHex)(sig) };
|
|
1903
|
+
}
|
|
1904
|
+
function verifyEnvelope(envelope, senderPk, opts = {}) {
|
|
1905
|
+
if (envelope.v !== HTTP_ENVELOPE_VERSION) {
|
|
1906
|
+
throw new HttpTransportError(
|
|
1907
|
+
`Unsupported envelope version: ${envelope.v}`,
|
|
1908
|
+
"ENVELOPE_VERSION_MISMATCH",
|
|
1909
|
+
{ version: envelope.v, expected: HTTP_ENVELOPE_VERSION }
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
if (opts.expectedFrom !== void 0 && envelope.from !== opts.expectedFrom) {
|
|
1913
|
+
throw new HttpTransportError(
|
|
1914
|
+
`Envelope from mismatch: expected ${opts.expectedFrom}, got ${envelope.from}`,
|
|
1915
|
+
"ENVELOPE_FROM_MISMATCH",
|
|
1916
|
+
{ expected: opts.expectedFrom, got: envelope.from }
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
if ("expectedTo" in opts && envelope.to !== opts.expectedTo) {
|
|
1920
|
+
throw new HttpTransportError(
|
|
1921
|
+
`Envelope to mismatch: expected ${opts.expectedTo ?? "<broadcast>"}, got ${envelope.to ?? "<broadcast>"}`,
|
|
1922
|
+
"ENVELOPE_TO_MISMATCH",
|
|
1923
|
+
{ expected: opts.expectedTo, got: envelope.to }
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
const skewSec = opts.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
|
|
1927
|
+
const nowMs = opts.now ? opts.now() : Date.now();
|
|
1928
|
+
const nowSec = Math.floor(nowMs / 1e3);
|
|
1929
|
+
const diff = Math.abs(nowSec - envelope.timestamp);
|
|
1930
|
+
if (diff > skewSec) {
|
|
1931
|
+
throw new HttpTransportError(
|
|
1932
|
+
`Envelope timestamp out of skew: ${diff}s > ${skewSec}s`,
|
|
1933
|
+
"ENVELOPE_TIMESTAMP_SKEW",
|
|
1934
|
+
{ diff, skewSec, timestamp: envelope.timestamp, now: nowSec }
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
let sigBytes;
|
|
1938
|
+
try {
|
|
1939
|
+
sigBytes = (0, import_utils4.hexToBytes)(envelope.sig);
|
|
1940
|
+
} catch {
|
|
1941
|
+
throw new HttpTransportError(
|
|
1942
|
+
"Envelope signature is not valid hex",
|
|
1943
|
+
"ENVELOPE_SIG_HEX"
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
if (sigBytes.length !== 64) {
|
|
1947
|
+
throw new HttpTransportError(
|
|
1948
|
+
`Invalid signature length: ${sigBytes.length} (expected 64)`,
|
|
1949
|
+
"ENVELOPE_SIG_LENGTH",
|
|
1950
|
+
{ length: sigBytes.length }
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
const { sig: _sig, ...unsigned } = envelope;
|
|
1954
|
+
const digest = (0, import_common6.canonicalHashBytes)(unsigned);
|
|
1955
|
+
const ok = senderPk.verify(sigBytes, digest, { scheme: "schnorr" });
|
|
1956
|
+
if (!ok) {
|
|
1957
|
+
throw new HttpTransportError(
|
|
1958
|
+
"Envelope signature verification failed",
|
|
1959
|
+
"ENVELOPE_SIG_INVALID"
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
function normalizeMessage(message) {
|
|
1964
|
+
const maybeToJSON = message.toJSON;
|
|
1965
|
+
if (typeof maybeToJSON === "function") {
|
|
1966
|
+
return maybeToJSON.call(message);
|
|
1967
|
+
}
|
|
1968
|
+
return message;
|
|
1969
|
+
}
|
|
1970
|
+
function normalizeForWire(value) {
|
|
1971
|
+
if (value instanceof Uint8Array) {
|
|
1972
|
+
return { __bytes: (0, import_utils4.bytesToHex)(value) };
|
|
1973
|
+
}
|
|
1974
|
+
if (Array.isArray(value)) {
|
|
1975
|
+
return value.map((v) => normalizeForWire(v));
|
|
1976
|
+
}
|
|
1977
|
+
if (value && typeof value === "object") {
|
|
1978
|
+
const out = {};
|
|
1979
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1980
|
+
out[k] = normalizeForWire(v);
|
|
1981
|
+
}
|
|
1982
|
+
return out;
|
|
1983
|
+
}
|
|
1984
|
+
return value;
|
|
1985
|
+
}
|
|
1986
|
+
function reviveFromWire(value) {
|
|
1987
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1988
|
+
const rec = value;
|
|
1989
|
+
const keys = Object.keys(rec);
|
|
1990
|
+
if (keys.length === 1 && keys[0] === "__bytes" && typeof rec.__bytes === "string") {
|
|
1991
|
+
return (0, import_utils4.hexToBytes)(rec.__bytes);
|
|
1992
|
+
}
|
|
1993
|
+
const out = {};
|
|
1994
|
+
for (const [k, v] of Object.entries(rec)) out[k] = reviveFromWire(v);
|
|
1995
|
+
return out;
|
|
1996
|
+
}
|
|
1997
|
+
if (Array.isArray(value)) {
|
|
1998
|
+
return value.map((v) => reviveFromWire(v));
|
|
1999
|
+
}
|
|
2000
|
+
return value;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// src/core/transport/http/request-auth.ts
|
|
2004
|
+
var import_common7 = require("@did-btcr2/common");
|
|
2005
|
+
var import_utils5 = require("@noble/hashes/utils");
|
|
2006
|
+
var REQUEST_AUTH_SCHEME = "BTCR2-Sig";
|
|
2007
|
+
function buildRequestAuth(did, keys, path, opts = {}) {
|
|
2008
|
+
const ts = opts.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
2009
|
+
const nonce = opts.nonce ?? (0, import_utils5.bytesToHex)((0, import_utils5.randomBytes)(DEFAULT_NONCE_LEN_BYTES));
|
|
2010
|
+
const digest = (0, import_common7.canonicalHashBytes)({
|
|
2011
|
+
v: HTTP_ENVELOPE_VERSION,
|
|
2012
|
+
did,
|
|
2013
|
+
ts,
|
|
2014
|
+
nonce,
|
|
2015
|
+
path
|
|
2016
|
+
});
|
|
2017
|
+
const sig = keys.secretKey.sign(digest, { scheme: "schnorr" });
|
|
2018
|
+
return `${REQUEST_AUTH_SCHEME} v=${HTTP_ENVELOPE_VERSION},did=${did},ts=${ts},nonce=${nonce},sig=${(0, import_utils5.bytesToHex)(sig)}`;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// src/participant/sse-stream.ts
|
|
2022
|
+
async function* parseSseStream(readable) {
|
|
2023
|
+
const decoder = new TextDecoder("utf-8");
|
|
2024
|
+
const reader = readable.getReader();
|
|
2025
|
+
let buffer = "";
|
|
2026
|
+
let pending = {};
|
|
2027
|
+
const dispatchPending = () => {
|
|
2028
|
+
if (pending.data === void 0) {
|
|
2029
|
+
pending = {};
|
|
2030
|
+
return null;
|
|
2031
|
+
}
|
|
2032
|
+
const ev = { data: pending.data };
|
|
2033
|
+
if (pending.event !== void 0) ev.event = pending.event;
|
|
2034
|
+
if (pending.id !== void 0) ev.id = pending.id;
|
|
2035
|
+
if (pending.retry !== void 0) ev.retry = pending.retry;
|
|
2036
|
+
pending = {};
|
|
2037
|
+
return ev;
|
|
2038
|
+
};
|
|
2039
|
+
const processLine = (line) => {
|
|
2040
|
+
if (line.startsWith(":")) return;
|
|
2041
|
+
const colon = line.indexOf(":");
|
|
2042
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
2043
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
2044
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
2045
|
+
switch (field) {
|
|
2046
|
+
case "data":
|
|
2047
|
+
pending.data = pending.data === void 0 ? value : `${pending.data}
|
|
2048
|
+
${value}`;
|
|
2049
|
+
break;
|
|
2050
|
+
case "event":
|
|
2051
|
+
pending.event = value;
|
|
2052
|
+
break;
|
|
2053
|
+
case "id":
|
|
2054
|
+
if (!value.includes("\0")) pending.id = value;
|
|
2055
|
+
break;
|
|
2056
|
+
case "retry": {
|
|
2057
|
+
const n = Number(value);
|
|
2058
|
+
if (Number.isInteger(n) && n >= 0) pending.retry = n;
|
|
2059
|
+
break;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
};
|
|
2063
|
+
try {
|
|
2064
|
+
for (; ; ) {
|
|
2065
|
+
const { value, done } = await reader.read();
|
|
2066
|
+
if (done) {
|
|
2067
|
+
buffer += decoder.decode();
|
|
2068
|
+
if (buffer.length > 0) {
|
|
2069
|
+
const line = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
|
|
2070
|
+
if (line.length > 0) processLine(line);
|
|
2071
|
+
buffer = "";
|
|
2072
|
+
}
|
|
2073
|
+
const tail = dispatchPending();
|
|
2074
|
+
if (tail) yield tail;
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2078
|
+
let lineEnd = buffer.indexOf("\n");
|
|
2079
|
+
while (lineEnd !== -1) {
|
|
2080
|
+
let line = buffer.slice(0, lineEnd);
|
|
2081
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
2082
|
+
buffer = buffer.slice(lineEnd + 1);
|
|
2083
|
+
if (line.length === 0) {
|
|
2084
|
+
const ev = dispatchPending();
|
|
2085
|
+
if (ev) yield ev;
|
|
2086
|
+
} else {
|
|
2087
|
+
processLine(line);
|
|
2088
|
+
}
|
|
2089
|
+
lineEnd = buffer.indexOf("\n");
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
} finally {
|
|
2093
|
+
try {
|
|
2094
|
+
reader.releaseLock();
|
|
2095
|
+
} catch {
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// src/participant/http-client.ts
|
|
2101
|
+
function defaultReconnectBackoff(attempt) {
|
|
2102
|
+
const base = Math.min(1e3 * 2 ** attempt, 3e4);
|
|
2103
|
+
const jitter = base * 0.2 * Math.random();
|
|
2104
|
+
return Math.floor(base + jitter);
|
|
2105
|
+
}
|
|
2106
|
+
var HttpClientTransport = class {
|
|
2107
|
+
name = "http";
|
|
2108
|
+
#baseUrl;
|
|
2109
|
+
#fetch;
|
|
2110
|
+
#logger;
|
|
2111
|
+
#backoff;
|
|
2112
|
+
#clockSkewSec;
|
|
2113
|
+
#resolveSenderPkFn;
|
|
2114
|
+
#actors = /* @__PURE__ */ new Map();
|
|
2115
|
+
#peers = /* @__PURE__ */ new Map();
|
|
2116
|
+
#started = false;
|
|
2117
|
+
#broadcastAbort;
|
|
2118
|
+
constructor(config) {
|
|
2119
|
+
const base = typeof config.baseUrl === "string" ? new URL(config.baseUrl) : new URL(config.baseUrl.href);
|
|
2120
|
+
if (!base.pathname.endsWith("/")) base.pathname += "/";
|
|
2121
|
+
this.#baseUrl = base;
|
|
2122
|
+
this.#logger = config.logger ?? CONSOLE_LOGGER;
|
|
2123
|
+
this.#backoff = config.reconnectBackoff ?? defaultReconnectBackoff;
|
|
2124
|
+
this.#clockSkewSec = config.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
|
|
2125
|
+
this.#resolveSenderPkFn = config.resolveSenderPk;
|
|
2126
|
+
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
2127
|
+
if (typeof fetchImpl !== "function") {
|
|
2128
|
+
throw new HttpTransportError(
|
|
2129
|
+
"No fetch implementation available. Pass config.fetchImpl explicitly.",
|
|
2130
|
+
"NO_FETCH_IMPL"
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2133
|
+
this.#fetch = fetchImpl;
|
|
2134
|
+
}
|
|
2135
|
+
start() {
|
|
2136
|
+
if (this.#started) return;
|
|
2137
|
+
this.#started = true;
|
|
2138
|
+
this.#broadcastAbort = new AbortController();
|
|
2139
|
+
this.#runBroadcastLoop(this.#broadcastAbort.signal);
|
|
2140
|
+
for (const [did, entry] of this.#actors) {
|
|
2141
|
+
this.#openInbox(did, entry);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Tear down all SSE subscriptions and stop reconnect loops. Not part of the
|
|
2146
|
+
* {@link Transport} interface, but needed in tests and whenever a client
|
|
2147
|
+
* wants to cleanly disconnect without unregistering every actor.
|
|
2148
|
+
*
|
|
2149
|
+
* Idempotent. Actors remain registered (re-call {@link start} to resume).
|
|
2150
|
+
*/
|
|
2151
|
+
stop() {
|
|
2152
|
+
this.#broadcastAbort?.abort();
|
|
2153
|
+
this.#broadcastAbort = void 0;
|
|
2154
|
+
for (const entry of this.#actors.values()) {
|
|
2155
|
+
entry.inboxAbort?.abort();
|
|
2156
|
+
entry.inboxAbort = void 0;
|
|
2157
|
+
}
|
|
2158
|
+
this.#started = false;
|
|
2159
|
+
}
|
|
2160
|
+
registerActor(did, keys) {
|
|
2161
|
+
const existing = this.#actors.get(did);
|
|
2162
|
+
if (existing?.inboxAbort) existing.inboxAbort.abort();
|
|
2163
|
+
const entry = { keys, handlers: /* @__PURE__ */ new Map() };
|
|
2164
|
+
this.#actors.set(did, entry);
|
|
2165
|
+
if (this.#started) this.#openInbox(did, entry);
|
|
2166
|
+
}
|
|
2167
|
+
unregisterActor(did) {
|
|
2168
|
+
const entry = this.#actors.get(did);
|
|
2169
|
+
if (!entry) return;
|
|
2170
|
+
entry.inboxAbort?.abort();
|
|
2171
|
+
this.#actors.delete(did);
|
|
2172
|
+
this.#peers.delete(did);
|
|
2173
|
+
}
|
|
2174
|
+
getActorPk(did) {
|
|
2175
|
+
return this.#actors.get(did)?.keys.publicKey.compressed;
|
|
2176
|
+
}
|
|
2177
|
+
registerPeer(did, communicationPk) {
|
|
2178
|
+
try {
|
|
2179
|
+
new import_keypair3.CompressedSecp256k1PublicKey(communicationPk);
|
|
2180
|
+
} catch {
|
|
2181
|
+
throw new HttpTransportError(
|
|
2182
|
+
`Invalid communication public key for peer ${did}: expected a 33-byte compressed secp256k1 key.`,
|
|
2183
|
+
"INVALID_PEER_KEY",
|
|
2184
|
+
{ did, keyLength: communicationPk.length }
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
this.#peers.set(did, communicationPk);
|
|
2188
|
+
}
|
|
2189
|
+
getPeerPk(did) {
|
|
2190
|
+
return this.#peers.get(did);
|
|
2191
|
+
}
|
|
2192
|
+
registerMessageHandler(actorDid, messageType, handler) {
|
|
2193
|
+
const actor = this.#actors.get(actorDid);
|
|
2194
|
+
if (!actor) {
|
|
2195
|
+
throw new HttpTransportError(
|
|
2196
|
+
`Cannot register handler: actor ${actorDid} not registered. Call registerActor() first.`,
|
|
2197
|
+
"UNKNOWN_ACTOR",
|
|
2198
|
+
{ did: actorDid }
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
actor.handlers.set(messageType, handler);
|
|
2202
|
+
}
|
|
2203
|
+
unregisterMessageHandler(actorDid, messageType) {
|
|
2204
|
+
this.#actors.get(actorDid)?.handlers.delete(messageType);
|
|
2205
|
+
}
|
|
2206
|
+
async sendMessage(message, sender, recipient) {
|
|
2207
|
+
const actor = this.#actors.get(sender);
|
|
2208
|
+
if (!actor) {
|
|
2209
|
+
throw new HttpTransportError(
|
|
2210
|
+
`Unknown sender: ${sender}. Call registerActor() before sending messages.`,
|
|
2211
|
+
"UNKNOWN_SENDER",
|
|
2212
|
+
{ did: sender }
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
const envelope = signEnvelope(
|
|
2216
|
+
message,
|
|
2217
|
+
{ did: sender, keys: actor.keys },
|
|
2218
|
+
{ to: recipient }
|
|
2219
|
+
);
|
|
2220
|
+
const url = this.#route(HTTP_ROUTE.MESSAGES);
|
|
2221
|
+
const res = await this.#fetch(url, {
|
|
2222
|
+
method: "POST",
|
|
2223
|
+
headers: { "content-type": "application/json" },
|
|
2224
|
+
body: JSON.stringify(envelope)
|
|
2225
|
+
});
|
|
2226
|
+
if (!res.ok) {
|
|
2227
|
+
const body = await safeText(res);
|
|
2228
|
+
throw new HttpTransportError(
|
|
2229
|
+
`sendMessage failed: HTTP ${res.status}`,
|
|
2230
|
+
"SEND_MESSAGE_HTTP",
|
|
2231
|
+
{ status: res.status, body: body.slice(0, 256), messageType: message.type }
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
publishRepeating(message, sender, intervalMs, recipient) {
|
|
2236
|
+
let stopped = false;
|
|
2237
|
+
const attempt = () => {
|
|
2238
|
+
if (stopped) return;
|
|
2239
|
+
this.sendMessage(message, sender, recipient).catch((err) => {
|
|
2240
|
+
this.#logger.debug("publishRepeating send failed:", err);
|
|
2241
|
+
});
|
|
2242
|
+
};
|
|
2243
|
+
attempt();
|
|
2244
|
+
const timer = setInterval(attempt, intervalMs);
|
|
2245
|
+
return () => {
|
|
2246
|
+
if (stopped) return;
|
|
2247
|
+
stopped = true;
|
|
2248
|
+
clearInterval(timer);
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
#route(template) {
|
|
2252
|
+
return new URL(template.replace(/^\//, ""), this.#baseUrl);
|
|
2253
|
+
}
|
|
2254
|
+
#openInbox(did, entry) {
|
|
2255
|
+
const abort = new AbortController();
|
|
2256
|
+
entry.inboxAbort = abort;
|
|
2257
|
+
this.#runInboxLoop(did, entry, abort.signal);
|
|
2258
|
+
}
|
|
2259
|
+
async #runBroadcastLoop(signal) {
|
|
2260
|
+
const url = this.#route(HTTP_ROUTE.ADVERTS);
|
|
2261
|
+
let attempt = 0;
|
|
2262
|
+
while (!signal.aborted) {
|
|
2263
|
+
try {
|
|
2264
|
+
const res = await this.#fetch(url, {
|
|
2265
|
+
method: "GET",
|
|
2266
|
+
headers: { accept: "text/event-stream" },
|
|
2267
|
+
signal
|
|
2268
|
+
});
|
|
2269
|
+
if (!res.ok || !res.body) {
|
|
2270
|
+
this.#logger.warn(`Broadcast subscribe failed: HTTP ${res.status}`);
|
|
2271
|
+
await sleep(this.#backoff(attempt++), signal);
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
attempt = 0;
|
|
2275
|
+
for await (const ev of parseSseStream(res.body)) {
|
|
2276
|
+
if (signal.aborted) return;
|
|
2277
|
+
if (ev.event !== SSE_EVENT.ADVERT) continue;
|
|
2278
|
+
this.#dispatchBroadcast(ev.data);
|
|
2279
|
+
}
|
|
2280
|
+
} catch (err) {
|
|
2281
|
+
if (signal.aborted) return;
|
|
2282
|
+
this.#logger.debug("Broadcast loop error:", err);
|
|
2283
|
+
try {
|
|
2284
|
+
await sleep(this.#backoff(attempt++), signal);
|
|
2285
|
+
} catch {
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
async #runInboxLoop(did, entry, signal) {
|
|
2292
|
+
const url = this.#route(HTTP_ROUTE.ACTOR_INBOX.replace("{did}", encodeURIComponent(did)));
|
|
2293
|
+
let attempt = 0;
|
|
2294
|
+
while (!signal.aborted) {
|
|
2295
|
+
try {
|
|
2296
|
+
const auth = buildRequestAuth(did, entry.keys, url.pathname);
|
|
2297
|
+
const res = await this.#fetch(url, {
|
|
2298
|
+
method: "GET",
|
|
2299
|
+
headers: { accept: "text/event-stream", authorization: auth },
|
|
2300
|
+
signal
|
|
2301
|
+
});
|
|
2302
|
+
if (!res.ok || !res.body) {
|
|
2303
|
+
this.#logger.warn(`Inbox subscribe failed for ${did}: HTTP ${res.status}`);
|
|
2304
|
+
await sleep(this.#backoff(attempt++), signal);
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
attempt = 0;
|
|
2308
|
+
for await (const ev of parseSseStream(res.body)) {
|
|
2309
|
+
if (signal.aborted) return;
|
|
2310
|
+
if (ev.event !== SSE_EVENT.MESSAGE) continue;
|
|
2311
|
+
await this.#dispatchInbox(ev.data, did, entry);
|
|
2312
|
+
}
|
|
2313
|
+
} catch (err) {
|
|
2314
|
+
if (signal.aborted) return;
|
|
2315
|
+
this.#logger.debug(`Inbox loop error for ${did}:`, err);
|
|
2316
|
+
try {
|
|
2317
|
+
await sleep(this.#backoff(attempt++), signal);
|
|
2318
|
+
} catch {
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
#dispatchBroadcast(dataJson) {
|
|
2325
|
+
const envelope = parseEnvelope(dataJson, this.#logger);
|
|
2326
|
+
if (!envelope) return;
|
|
2327
|
+
const senderPk = this.#resolveSenderPk(envelope.from);
|
|
2328
|
+
if (!senderPk) {
|
|
2329
|
+
this.#logger.debug(`Broadcast from unresolvable DID: ${envelope.from}`);
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
try {
|
|
2333
|
+
verifyEnvelope(envelope, senderPk, { clockSkewSec: this.#clockSkewSec });
|
|
2334
|
+
} catch (err) {
|
|
2335
|
+
this.#logger.debug("Broadcast envelope verification failed:", err);
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
const revived = reviveFromWire(envelope.message);
|
|
2339
|
+
const flat = flattenMessage(revived);
|
|
2340
|
+
const messageType = typeof flat.type === "string" ? flat.type : void 0;
|
|
2341
|
+
if (!messageType) return;
|
|
2342
|
+
for (const actor of this.#actors.values()) {
|
|
2343
|
+
const handler = actor.handlers.get(messageType);
|
|
2344
|
+
if (handler) void Promise.resolve(handler(flat));
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
async #dispatchInbox(dataJson, actorDid, entry) {
|
|
2348
|
+
const envelope = parseEnvelope(dataJson, this.#logger);
|
|
2349
|
+
if (!envelope) return;
|
|
2350
|
+
const senderPk = this.#resolveSenderPk(envelope.from);
|
|
2351
|
+
if (!senderPk) {
|
|
2352
|
+
this.#logger.debug(`Inbox message from unresolvable DID: ${envelope.from}`);
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
try {
|
|
2356
|
+
verifyEnvelope(envelope, senderPk, {
|
|
2357
|
+
clockSkewSec: this.#clockSkewSec,
|
|
2358
|
+
expectedTo: actorDid
|
|
2359
|
+
});
|
|
2360
|
+
} catch (err) {
|
|
2361
|
+
this.#logger.debug(`Inbox envelope verification failed for ${actorDid}:`, err);
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
const revived = reviveFromWire(envelope.message);
|
|
2365
|
+
const flat = flattenMessage(revived);
|
|
2366
|
+
const messageType = typeof flat.type === "string" ? flat.type : void 0;
|
|
2367
|
+
if (!messageType) return;
|
|
2368
|
+
const handler = entry.handlers.get(messageType);
|
|
2369
|
+
if (handler) await handler(flat);
|
|
2370
|
+
}
|
|
2371
|
+
#resolveSenderPk(did) {
|
|
2372
|
+
const peerBytes = this.#peers.get(did);
|
|
2373
|
+
if (peerBytes) {
|
|
2374
|
+
try {
|
|
2375
|
+
return new import_keypair3.CompressedSecp256k1PublicKey(peerBytes);
|
|
2376
|
+
} catch {
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
return this.#resolveSenderPkFn?.(did);
|
|
2380
|
+
}
|
|
2381
|
+
};
|
|
2382
|
+
function sleep(ms, signal) {
|
|
2383
|
+
if (ms <= 0) return Promise.resolve();
|
|
2384
|
+
return new Promise((resolve, reject) => {
|
|
2385
|
+
const onAbort = () => {
|
|
2386
|
+
clearTimeout(timer);
|
|
2387
|
+
reject(new Error("aborted"));
|
|
2388
|
+
};
|
|
2389
|
+
const timer = setTimeout(() => {
|
|
2390
|
+
signal.removeEventListener("abort", onAbort);
|
|
2391
|
+
resolve();
|
|
2392
|
+
}, ms);
|
|
2393
|
+
if (signal.aborted) onAbort();
|
|
2394
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
async function safeText(res) {
|
|
2398
|
+
try {
|
|
2399
|
+
return await res.text();
|
|
2400
|
+
} catch {
|
|
2401
|
+
return "";
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
function parseEnvelope(dataJson, logger) {
|
|
2405
|
+
try {
|
|
2406
|
+
return JSON.parse(dataJson);
|
|
2407
|
+
} catch (err) {
|
|
2408
|
+
logger.debug("SSE event: failed to parse envelope JSON:", err);
|
|
2409
|
+
return void 0;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
function flattenMessage(msg) {
|
|
2413
|
+
if (msg.body && typeof msg.body === "object") {
|
|
2414
|
+
return { ...msg, ...msg.body };
|
|
2415
|
+
}
|
|
2416
|
+
return msg;
|
|
2417
|
+
}
|
|
2418
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2419
|
+
0 && (module.exports = {
|
|
2420
|
+
AggregationParticipant,
|
|
2421
|
+
AggregationParticipantRunner,
|
|
2422
|
+
HttpClientTransport,
|
|
2423
|
+
defaultReconnectBackoff,
|
|
2424
|
+
parseSseStream
|
|
2425
|
+
});
|