@dynamic-labs-wallet/forward-mpc-client 0.3.0 → 0.5.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/index.js CHANGED
@@ -1,11 +1,13 @@
1
- import { EventEmitter } from 'eventemitter3';
1
+ import EventEmitter2, { EventEmitter } from 'eventemitter3';
2
2
  import * as ws from 'ws';
3
- import { messageRegistry, generateMlKem768Keypair, HandshakeV1RequestMessage, decapsulateMlKem768, encryptKeyshare, SignMessageV1RequestMessage, encryptKeygenInit, KeygenV1RequestMessage, decryptKeygenResult, ReceiveKeyV1RequestMessage } from '@dynamic-labs-wallet/forward-mpc-shared';
3
+ import { messageRegistry, generateMlKem768Keypair, HandshakeV1RequestMessage, decapsulateMlKem768, encryptKeyshare, SignMessageV1RequestMessage, encryptKeygenInit, KeygenV1RequestMessage, decryptKeygenResult, ReceiveKeyV1RequestMessage, WebSocketCloseCode, fromDynamicSigningAlgorithm } from '@dynamic-labs-wallet/forward-mpc-shared';
4
4
  import init, { PCRs, validateAttestationDocPcrs, getUserData, getNonce } from '@evervault/wasm-attestation-bindings';
5
5
  import { sha256 } from '@noble/hashes/sha2.js';
6
6
  import { randomBytes, hexToBytes } from '@noble/hashes/utils.js';
7
7
  import { either } from 'fp-ts';
8
8
  import { SigningAlgorithm } from '@dynamic-labs-wallet/core';
9
+ export { SigningAlgorithm } from '@dynamic-labs-wallet/core';
10
+ import { WebSocket } from 'isows';
9
11
 
10
12
  var __defProp = Object.defineProperty;
11
13
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
@@ -19,7 +21,7 @@ var NitroAttestationVerifier = class {
19
21
  __name(this, "NitroAttestationVerifier");
20
22
  }
21
23
  config;
22
- wasmInitialized = false;
24
+ wasmInitPromise = null;
23
25
  constructor(config) {
24
26
  this.config = {
25
27
  strictCertValidation: true,
@@ -28,17 +30,16 @@ var NitroAttestationVerifier = class {
28
30
  };
29
31
  }
30
32
  /**
31
- * Initialize WASM module if not already initialized
33
+ * Initialises the WASM module exactly once. Concurrent callers share the
34
+ * same in-flight promise, preventing duplicate initialisation.
35
+ * On failure the promise is cleared so the next call may retry.
32
36
  */
33
- async ensureWasmInitialized() {
34
- if (!this.wasmInitialized) {
35
- try {
36
- await init();
37
- this.wasmInitialized = true;
38
- } catch (error) {
39
- throw new Error(`Failed to initialize WASM module: ${error instanceof Error ? error.message : "Unknown error"}`);
40
- }
41
- }
37
+ ensureWasmInitialized() {
38
+ this.wasmInitPromise ??= init().then(() => void 0).catch((error) => {
39
+ this.wasmInitPromise = null;
40
+ throw new Error(`Failed to initialize WASM module: ${error instanceof Error ? error.message : "Unknown error"}`);
41
+ });
42
+ return this.wasmInitPromise;
42
43
  }
43
44
  /**
44
45
  * Verify an attestation document using Evervault WASM bindings
@@ -46,9 +47,9 @@ var NitroAttestationVerifier = class {
46
47
  *
47
48
  * @param attestationDocBase64 - Base64-encoded attestation document
48
49
  * @param expectedChallenge - Expected challenge (ciphertext hash)
49
- * @param expectedNonce - Expected nonce (REQUIRED for security)
50
+ * @param nonce - Expected nonce (REQUIRED for security)
50
51
  */
51
- async verify(attestationDocBase64, expectedChallenge, expectedNonce) {
52
+ async verify(attestationDocBase64, expectedChallenge, nonce) {
52
53
  try {
53
54
  await this.ensureWasmInitialized();
54
55
  const expectedPcrs = PCRs.empty();
@@ -65,37 +66,44 @@ var NitroAttestationVerifier = class {
65
66
  timestamp: Date.now()
66
67
  };
67
68
  }
68
- if (expectedChallenge) {
69
- try {
70
- const userData = getUserData(attestationDocBase64);
71
- if (!userData) {
72
- return {
73
- valid: false,
74
- errors: [
75
- "No user data found in attestation document"
76
- ],
77
- timestamp: Date.now()
78
- };
79
- }
80
- const userDataString = new TextDecoder("utf-8").decode(userData);
81
- if (!userDataString.includes(expectedChallenge)) {
82
- return {
83
- valid: false,
84
- errors: [
85
- "Ciphertext hash verification failed - challenge not found in attestation user data"
86
- ],
87
- timestamp: Date.now()
88
- };
89
- }
90
- } catch (error) {
69
+ if (!expectedChallenge) {
70
+ return {
71
+ valid: false,
72
+ errors: [
73
+ "No challenge provided \u2014 ciphertext binding cannot be verified"
74
+ ],
75
+ timestamp: Date.now()
76
+ };
77
+ }
78
+ try {
79
+ const userData = getUserData(attestationDocBase64);
80
+ if (!userData) {
81
+ return {
82
+ valid: false,
83
+ errors: [
84
+ "No user data found in attestation document"
85
+ ],
86
+ timestamp: Date.now()
87
+ };
88
+ }
89
+ const userDataString = new TextDecoder("utf-8").decode(userData);
90
+ if (userDataString !== expectedChallenge) {
91
91
  return {
92
92
  valid: false,
93
93
  errors: [
94
- `Failed to extract or verify ciphertext hash: ${error instanceof Error ? error.message : String(error)}`
94
+ "Ciphertext hash verification failed - challenge mismatch in attestation user data"
95
95
  ],
96
96
  timestamp: Date.now()
97
97
  };
98
98
  }
99
+ } catch (error) {
100
+ return {
101
+ valid: false,
102
+ errors: [
103
+ `Failed to extract or verify ciphertext hash: ${error instanceof Error ? error.message : String(error)}`
104
+ ],
105
+ timestamp: Date.now()
106
+ };
99
107
  }
100
108
  try {
101
109
  const extractedNonceRaw = getNonce(attestationDocBase64);
@@ -118,8 +126,7 @@ var NitroAttestationVerifier = class {
118
126
  extractedNonce[i] = binaryString.charCodeAt(i);
119
127
  }
120
128
  } else {
121
- const decodedBuffer = Buffer.from(nonceString, "base64");
122
- extractedNonce = new Uint8Array(decodedBuffer);
129
+ extractedNonce = new Uint8Array(Buffer.from(nonceString, "base64"));
123
130
  }
124
131
  } catch (decodeError) {
125
132
  return {
@@ -130,25 +137,27 @@ var NitroAttestationVerifier = class {
130
137
  timestamp: Date.now()
131
138
  };
132
139
  }
133
- if (extractedNonce.length !== expectedNonce.length) {
140
+ if (extractedNonce.length !== nonce.length) {
134
141
  return {
135
142
  valid: false,
136
143
  errors: [
137
- `Nonce length mismatch: expected ${expectedNonce.length} bytes, got ${extractedNonce.length} bytes`
144
+ `Nonce length mismatch: expected ${nonce.length} bytes, got ${extractedNonce.length} bytes`
138
145
  ],
139
146
  timestamp: Date.now()
140
147
  };
141
148
  }
142
- for (let i = 0; i < expectedNonce.length; i++) {
143
- if (extractedNonce[i] !== expectedNonce[i]) {
144
- return {
145
- valid: false,
146
- errors: [
147
- "Nonce verification failed - nonce mismatch"
148
- ],
149
- timestamp: Date.now()
150
- };
151
- }
149
+ let diff = 0;
150
+ for (let i = 0; i < nonce.length; i++) {
151
+ diff |= extractedNonce[i] ^ nonce[i];
152
+ }
153
+ if (diff !== 0) {
154
+ return {
155
+ valid: false,
156
+ errors: [
157
+ "Nonce verification failed - nonce mismatch"
158
+ ],
159
+ timestamp: Date.now()
160
+ };
152
161
  }
153
162
  } catch (error) {
154
163
  return {
@@ -554,7 +563,800 @@ var ForwardMPCClient = class extends EventEmitter {
554
563
  }
555
564
  }
556
565
  };
566
+ var ErrorCode = {
567
+ // Transport
568
+ CONNECTION_FAILED: "CONNECTION_FAILED",
569
+ CONNECTION_TIMEOUT: "CONNECTION_TIMEOUT",
570
+ NOT_CONNECTED: "NOT_CONNECTED",
571
+ // Session
572
+ HANDSHAKE_FAILED: "HANDSHAKE_FAILED",
573
+ HANDSHAKE_INVALID_RESPONSE: "HANDSHAKE_INVALID_RESPONSE",
574
+ ATTESTATION_FAILED: "ATTESTATION_FAILED",
575
+ ATTESTATION_NONCE_MISSING: "ATTESTATION_NONCE_MISSING",
576
+ REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
577
+ SESSION_DISPOSED: "SESSION_DISPOSED",
578
+ SERVER_ERROR: "SERVER_ERROR",
579
+ MESSAGE_PARSE_FAILED: "MESSAGE_PARSE_FAILED",
580
+ // Client
581
+ SESSION_ESTABLISH_FAILED: "SESSION_ESTABLISH_FAILED",
582
+ UNSUPPORTED_ALGORITHM: "UNSUPPORTED_ALGORITHM"
583
+ };
584
+ var ForwardMPCErrorType = {
585
+ TRANSPORT: "transport",
586
+ SESSION: "session",
587
+ CLIENT: "client"
588
+ };
589
+ var ForwardMPCError = class extends Error {
590
+ static {
591
+ __name(this, "ForwardMPCError");
592
+ }
593
+ code;
594
+ type;
595
+ context;
596
+ constructor(message, code, type, context) {
597
+ super(message);
598
+ this.name = this.constructor.name;
599
+ this.code = code;
600
+ this.type = type;
601
+ this.context = context;
602
+ Object.setPrototypeOf(this, new.target.prototype);
603
+ }
604
+ toJSON() {
605
+ return {
606
+ name: this.name,
607
+ message: this.message,
608
+ code: this.code,
609
+ type: this.type,
610
+ stack: this.stack,
611
+ context: this.context
612
+ };
613
+ }
614
+ };
615
+ var TransportError = class extends ForwardMPCError {
616
+ static {
617
+ __name(this, "TransportError");
618
+ }
619
+ constructor(message, code, context) {
620
+ super(message, code, ForwardMPCErrorType.TRANSPORT, context);
621
+ }
622
+ };
623
+ var SessionError = class extends ForwardMPCError {
624
+ static {
625
+ __name(this, "SessionError");
626
+ }
627
+ constructor(message, code, context) {
628
+ super(message, code, ForwardMPCErrorType.SESSION, context);
629
+ }
630
+ };
631
+ var ClientError = class extends ForwardMPCError {
632
+ static {
633
+ __name(this, "ClientError");
634
+ }
635
+ constructor(message, code, context) {
636
+ super(message, code, ForwardMPCErrorType.CLIENT, context);
637
+ }
638
+ };
639
+ var TransportConnectionError = class extends TransportError {
640
+ static {
641
+ __name(this, "TransportConnectionError");
642
+ }
643
+ constructor(context) {
644
+ super("WebSocket connection failed", ErrorCode.CONNECTION_FAILED, context);
645
+ }
646
+ };
647
+ var TransportConnectionTimeoutError = class extends TransportError {
648
+ static {
649
+ __name(this, "TransportConnectionTimeoutError");
650
+ }
651
+ constructor(context) {
652
+ super("WebSocket connection timed out", ErrorCode.CONNECTION_TIMEOUT, context);
653
+ }
654
+ };
655
+ var TransportNotConnectedError = class extends TransportError {
656
+ static {
657
+ __name(this, "TransportNotConnectedError");
658
+ }
659
+ constructor(context) {
660
+ super("WebSocket is not connected", ErrorCode.NOT_CONNECTED, context);
661
+ }
662
+ };
663
+ var SessionHandshakeError = class extends SessionError {
664
+ static {
665
+ __name(this, "SessionHandshakeError");
666
+ }
667
+ constructor(reason, context) {
668
+ super(`ML-KEM-768 handshake failed: ${reason}`, ErrorCode.HANDSHAKE_FAILED, {
669
+ reason,
670
+ ...context
671
+ });
672
+ }
673
+ };
674
+ var SessionHandshakeInvalidResponseError = class extends SessionError {
675
+ static {
676
+ __name(this, "SessionHandshakeInvalidResponseError");
677
+ }
678
+ constructor(context) {
679
+ super("Handshake response was invalid or incomplete", ErrorCode.HANDSHAKE_INVALID_RESPONSE, context);
680
+ }
681
+ };
682
+ var SessionAttestationError = class extends SessionError {
683
+ static {
684
+ __name(this, "SessionAttestationError");
685
+ }
686
+ constructor(context) {
687
+ super("Attestation verification failed", ErrorCode.ATTESTATION_FAILED, context);
688
+ }
689
+ };
690
+ var SessionAttestationNonceMissingError = class extends SessionError {
691
+ static {
692
+ __name(this, "SessionAttestationNonceMissingError");
693
+ }
694
+ constructor(context) {
695
+ super("Nonce missing from attestation document", ErrorCode.ATTESTATION_NONCE_MISSING, context);
696
+ }
697
+ };
698
+ var SessionRequestTimeoutError = class extends SessionError {
699
+ static {
700
+ __name(this, "SessionRequestTimeoutError");
701
+ }
702
+ constructor(context) {
703
+ super("Request timed out waiting for server response", ErrorCode.REQUEST_TIMEOUT, context);
704
+ }
705
+ };
706
+ var SessionDisposedError = class extends SessionError {
707
+ static {
708
+ __name(this, "SessionDisposedError");
709
+ }
710
+ constructor(context) {
711
+ super("Session has been disposed", ErrorCode.SESSION_DISPOSED, context);
712
+ }
713
+ };
714
+ var SessionServerError = class extends SessionError {
715
+ static {
716
+ __name(this, "SessionServerError");
717
+ }
718
+ constructor(reason, context) {
719
+ super(`Server returned an error response: ${reason}`, ErrorCode.SERVER_ERROR, {
720
+ reason,
721
+ ...context
722
+ });
723
+ }
724
+ };
725
+ var SessionMessageParseError = class extends SessionError {
726
+ static {
727
+ __name(this, "SessionMessageParseError");
728
+ }
729
+ constructor(context) {
730
+ super("Failed to parse server message", ErrorCode.MESSAGE_PARSE_FAILED, context);
731
+ }
732
+ };
733
+ var SessionRemoteError = class extends SessionError {
734
+ static {
735
+ __name(this, "SessionRemoteError");
736
+ }
737
+ serverError;
738
+ constructor(serverError, context) {
739
+ super(serverError.message, ErrorCode.SERVER_ERROR, context), this.serverError = serverError;
740
+ }
741
+ };
742
+ var ClientUnsupportedAlgorithmError = class extends ClientError {
743
+ static {
744
+ __name(this, "ClientUnsupportedAlgorithmError");
745
+ }
746
+ constructor(context) {
747
+ super("Signing algorithm is not supported", ErrorCode.UNSUPPORTED_ALGORITHM, context);
748
+ }
749
+ };
750
+ var ClientSessionEstablishFailedError = class extends ClientError {
751
+ static {
752
+ __name(this, "ClientSessionEstablishFailedError");
753
+ }
754
+ constructor(context) {
755
+ super("Failed to establish session", ErrorCode.SESSION_ESTABLISH_FAILED, context);
756
+ }
757
+ };
758
+
759
+ // src/client-v2/transport.ts
760
+ var ForwardMPCTransport = class extends EventEmitter2 {
761
+ static {
762
+ __name(this, "ForwardMPCTransport");
763
+ }
764
+ url;
765
+ ws = null;
766
+ _isConnected = false;
767
+ _destroyed = false;
768
+ _hadSuccessfulConnection = false;
769
+ _midSessionReconnectCount = 0;
770
+ _connectPromise = null;
771
+ options;
772
+ logger;
773
+ constructor(url, options = {}) {
774
+ super(), this.url = url;
775
+ this.logger = options.logger;
776
+ this.options = {
777
+ reconnectAttempts: options.reconnectAttempts ?? 1,
778
+ reconnectInterval: options.reconnectInterval ?? 1e3,
779
+ connectionTimeout: options.connectionTimeout ?? 1e4
780
+ };
781
+ }
782
+ get connected() {
783
+ return this._isConnected;
784
+ }
785
+ /**
786
+ * Opens the WebSocket connection. Concurrent callers coalesce on a single
787
+ * in-flight promise. `reconnectAttempts` controls how many silent retries
788
+ * are attempted before the promise rejects (default: 1 retry = 2 total tries).
789
+ */
790
+ async connect() {
791
+ this._destroyed = false;
792
+ if (this._isConnected) return;
793
+ if (this._connectPromise) return this._connectPromise;
794
+ this._connectPromise = this._connectWithRetry().finally(() => {
795
+ this._connectPromise = null;
796
+ });
797
+ return this._connectPromise;
798
+ }
799
+ disconnect() {
800
+ this._destroyed = true;
801
+ if (this.ws) {
802
+ this.ws.close();
803
+ this.ws = null;
804
+ }
805
+ this._isConnected = false;
806
+ this.emit("disconnected");
807
+ }
808
+ send(data) {
809
+ if (!this._isConnected || !this.ws) {
810
+ throw new TransportNotConnectedError();
811
+ }
812
+ this.ws.send(data);
813
+ }
814
+ /**
815
+ * Attempts the initial connection, then silently retries up to
816
+ * `reconnectAttempts` times before surfacing an error.
817
+ */
818
+ async _connectWithRetry() {
819
+ let lastError;
820
+ for (let attempt = 0; attempt <= this.options.reconnectAttempts; attempt++) {
821
+ try {
822
+ await this._connectOnce();
823
+ return;
824
+ } catch (error) {
825
+ lastError = error;
826
+ }
827
+ }
828
+ throw lastError;
829
+ }
830
+ _connectOnce() {
831
+ return new Promise((resolve, reject) => {
832
+ const timeoutHandle = setTimeout(() => {
833
+ this.ws?.close();
834
+ reject(new TransportConnectionTimeoutError({
835
+ url: this.url
836
+ }));
837
+ }, this.options.connectionTimeout);
838
+ this.ws = new WebSocket(this.url);
839
+ this.ws.onopen = () => {
840
+ clearTimeout(timeoutHandle);
841
+ this._isConnected = true;
842
+ this._hadSuccessfulConnection = true;
843
+ this._midSessionReconnectCount = 0;
844
+ this.logger?.info("WebSocket connected", {
845
+ url: this.url
846
+ });
847
+ this.emit("connected");
848
+ resolve();
849
+ };
850
+ this.ws.onerror = () => {
851
+ clearTimeout(timeoutHandle);
852
+ const err = new TransportConnectionError({
853
+ url: this.url
854
+ });
855
+ this.emit("error", err);
856
+ reject(err);
857
+ };
858
+ this.ws.onmessage = (event) => {
859
+ this.logger?.debug("WebSocket message received", {
860
+ data: event.data
861
+ });
862
+ this.emit("message", event.data);
863
+ };
864
+ this.ws.onclose = (event) => {
865
+ this._isConnected = false;
866
+ this.logger?.warn("WebSocket closed", {
867
+ url: this.url,
868
+ closeCode: event.code
869
+ });
870
+ if (!this._destroyed) {
871
+ this.emit("disconnected");
872
+ }
873
+ this.maybeReconnect(event.code);
874
+ };
875
+ });
876
+ }
877
+ /**
878
+ * Attempts mid-session reconnects after a drop, up to `reconnectAttempts`
879
+ * times. Only fires when a successful connection was previously established
880
+ * and the close was unexpected (not a normal or idle-timeout close).
881
+ */
882
+ maybeReconnect(closeCode) {
883
+ if (this._destroyed) return;
884
+ if (!this._hadSuccessfulConnection) return;
885
+ if (this._midSessionReconnectCount >= this.options.reconnectAttempts) return;
886
+ if (closeCode === WebSocketCloseCode.NORMAL || closeCode === WebSocketCloseCode.IDLE_TIMEOUT) {
887
+ this.logger?.info("WebSocket closed gracefully \u2014 not reconnecting", {
888
+ url: this.url,
889
+ closeCode
890
+ });
891
+ return;
892
+ }
893
+ this._midSessionReconnectCount++;
894
+ this.logger?.warn("WebSocket disconnected \u2014 attempting reconnect", {
895
+ url: this.url,
896
+ attempt: this._midSessionReconnectCount,
897
+ maxAttempts: this.options.reconnectAttempts
898
+ });
899
+ setTimeout(() => {
900
+ this._connectOnce().catch((error) => {
901
+ const err = error instanceof Error ? error : new Error(String(error));
902
+ this.logger?.error("Reconnect failed", {
903
+ attempt: this._midSessionReconnectCount
904
+ }, err);
905
+ this.emit("error", err);
906
+ });
907
+ }, this.options.reconnectInterval);
908
+ }
909
+ };
910
+ function isWebSocketError(v) {
911
+ return typeof v === "object" && v !== null && typeof v["message"] === "string" && typeof v["type"] === "string";
912
+ }
913
+ __name(isWebSocketError, "isWebSocketError");
914
+ var Session = class _Session {
915
+ static {
916
+ __name(this, "Session");
917
+ }
918
+ transport;
919
+ _connectionId;
920
+ _sharedSecret;
921
+ requestTimeout;
922
+ logger;
923
+ _disposed = false;
924
+ _abort = new AbortController();
925
+ /**
926
+ * Session is only constructed with fully-validated crypto material.
927
+ * All handshake and attestation work is done in the static handshake() factory
928
+ * before this constructor is called.
929
+ */
930
+ constructor(transport, _connectionId, _sharedSecret, requestTimeout, logger) {
931
+ this.transport = transport;
932
+ this._connectionId = _connectionId;
933
+ this._sharedSecret = _sharedSecret;
934
+ this.requestTimeout = requestTimeout;
935
+ this.logger = logger;
936
+ }
937
+ get connectionId() {
938
+ return this._connectionId;
939
+ }
940
+ get sharedSecret() {
941
+ if (this._disposed) {
942
+ throw new SessionDisposedError();
943
+ }
944
+ return this._sharedSecret;
945
+ }
946
+ /**
947
+ * Performs the ML-KEM-768 handshake over an established transport connection
948
+ * and returns a fully authenticated Session. All crypto material is derived
949
+ * before the Session object is created — the constructor never receives
950
+ * partially-initialised state.
951
+ *
952
+ * Attestation is verified (when configured) before the Session is returned.
953
+ */
954
+ static async handshake(transport, traceContext, options = {}, logger) {
955
+ const requestTimeout = options.requestTimeout ?? 3e4;
956
+ const { encapsulationKey, decapsulationKey } = generateMlKem768Keypair();
957
+ const nonceBytes = randomBytes(32);
958
+ const request = new HandshakeV1RequestMessage({
959
+ challenge: encapsulationKey,
960
+ nonce: nonceBytes,
961
+ traceContext
962
+ });
963
+ let data;
964
+ try {
965
+ data = await _Session.doRequest(transport, request, requestTimeout);
966
+ } catch (error) {
967
+ decapsulationKey.fill(0);
968
+ nonceBytes.fill(0);
969
+ const message = error instanceof Error ? error.message : String(error);
970
+ throw new SessionHandshakeError(message);
971
+ }
972
+ if (!data.encapsulatedSharedSecret || !data.connectionId) {
973
+ decapsulationKey.fill(0);
974
+ nonceBytes.fill(0);
975
+ throw new SessionHandshakeInvalidResponseError();
976
+ }
977
+ const connectionId = data.connectionId;
978
+ const cipherText = hexToBytes(data.encapsulatedSharedSecret);
979
+ const sharedSecret = decapsulateMlKem768(decapsulationKey, cipherText);
980
+ decapsulationKey.fill(0);
981
+ if (options.attestationVerifier && !options.bypassAttestation) {
982
+ if (!data.attestationDoc) {
983
+ sharedSecret.fill(0);
984
+ nonceBytes.fill(0);
985
+ throw new SessionAttestationError({
986
+ reason: "Server did not return an attestation document"
987
+ });
988
+ }
989
+ try {
990
+ await _Session.verifyAttestation(data.attestationDoc, cipherText, nonceBytes, options.attestationVerifier);
991
+ } catch (error) {
992
+ sharedSecret.fill(0);
993
+ nonceBytes.fill(0);
994
+ throw error;
995
+ }
996
+ }
997
+ nonceBytes.fill(0);
998
+ logger?.debug("Handshake completed", {
999
+ connectionId
1000
+ });
1001
+ return new _Session(transport, connectionId, sharedSecret, requestTimeout, logger);
1002
+ }
1003
+ sendRequest(message) {
1004
+ if (this._disposed) {
1005
+ return Promise.reject(new SessionDisposedError());
1006
+ }
1007
+ return _Session.doRequest(this.transport, message, this.requestTimeout, this._abort.signal);
1008
+ }
1009
+ dispose() {
1010
+ if (this._disposed) return;
1011
+ this._disposed = true;
1012
+ this._abort.abort();
1013
+ this._sharedSecret.fill(0);
1014
+ this.logger?.debug("Session disposed", {
1015
+ connectionId: this._connectionId
1016
+ });
1017
+ }
1018
+ /**
1019
+ * Sends a single request and resolves with the decoded response data.
1020
+ * Used by both handshake() and sendRequest(). The optional AbortSignal
1021
+ * allows dispose() to cancel all in-flight requests immediately.
1022
+ */
1023
+ static doRequest(transport, message, timeout, signal) {
1024
+ const requestId = _Session.generateRequestId();
1025
+ return new Promise((resolve, reject) => {
1026
+ if (signal?.aborted) {
1027
+ reject(new SessionDisposedError());
1028
+ return;
1029
+ }
1030
+ const cleanup = /* @__PURE__ */ __name(() => {
1031
+ clearTimeout(timeoutHandle);
1032
+ transport.off("message", handler);
1033
+ signal?.removeEventListener("abort", onAbort);
1034
+ }, "cleanup");
1035
+ const onAbort = /* @__PURE__ */ __name(() => {
1036
+ cleanup();
1037
+ reject(new SessionDisposedError());
1038
+ }, "onAbort");
1039
+ signal?.addEventListener("abort", onAbort, {
1040
+ once: true
1041
+ });
1042
+ const timeoutHandle = setTimeout(() => {
1043
+ cleanup();
1044
+ reject(new SessionRequestTimeoutError({
1045
+ requestId
1046
+ }));
1047
+ }, timeout);
1048
+ const handler = /* @__PURE__ */ __name((rawData) => {
1049
+ let parsed;
1050
+ try {
1051
+ parsed = JSON.parse(rawData);
1052
+ } catch {
1053
+ return;
1054
+ }
1055
+ if (parsed["requestId"] !== requestId) return;
1056
+ cleanup();
1057
+ const { requestId: _rid, ...body } = parsed;
1058
+ let msg = null;
1059
+ try {
1060
+ const result = messageRegistry.decode(body);
1061
+ if (either.isRight(result)) msg = result.right;
1062
+ } catch {
1063
+ }
1064
+ if (msg === null) {
1065
+ reject(new SessionMessageParseError({
1066
+ requestId
1067
+ }));
1068
+ return;
1069
+ }
1070
+ if (msg.type === "error") {
1071
+ reject(isWebSocketError(msg.error) ? new SessionRemoteError(msg.error) : new SessionMessageParseError({
1072
+ requestId
1073
+ }));
1074
+ } else {
1075
+ resolve(msg.getData());
1076
+ }
1077
+ }, "handler");
1078
+ transport.on("message", handler);
1079
+ try {
1080
+ transport.send(_Session.serializeWithRequestId(message, requestId));
1081
+ } catch (error) {
1082
+ cleanup();
1083
+ reject(error instanceof Error ? error : new Error(String(error)));
1084
+ }
1085
+ });
1086
+ }
1087
+ /**
1088
+ * Serialises a message with the given requestId injected without mutating
1089
+ * the original message object. Handles both encodeable (registry) messages
1090
+ * and plain objects.
1091
+ */
1092
+ static serializeWithRequestId(message, requestId) {
1093
+ const msg = message;
1094
+ if (typeof msg["encode"] === "function") {
1095
+ const encoded = msg["encode"]();
1096
+ encoded["requestId"] = requestId;
1097
+ return JSON.stringify(encoded);
1098
+ }
1099
+ return JSON.stringify({
1100
+ ...msg,
1101
+ requestId
1102
+ });
1103
+ }
1104
+ static generateRequestId() {
1105
+ const rand = randomBytes(8);
1106
+ return `req_${Array.from(rand).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
1107
+ }
1108
+ static async verifyAttestation(attestationDocBase64, cipherText, nonce, verifier) {
1109
+ const challengeHash = sha256(cipherText);
1110
+ const expectedChallenge = Array.from(challengeHash).map((b) => b.toString(16).padStart(2, "0")).join("");
1111
+ const result = await verifier.verify(attestationDocBase64, expectedChallenge, nonce);
1112
+ if (!result.valid) {
1113
+ throw new SessionAttestationError({
1114
+ errors: result.errors
1115
+ });
1116
+ }
1117
+ }
1118
+ };
1119
+
1120
+ // src/client-v2/logger.ts
1121
+ var Logger = class {
1122
+ static {
1123
+ __name(this, "Logger");
1124
+ }
1125
+ externalLogger;
1126
+ MESSAGE_PREFIX = "[ForwardMPCClientV2]";
1127
+ constructor(externalLogger) {
1128
+ this.externalLogger = externalLogger;
1129
+ }
1130
+ debug(message, messageContext, error) {
1131
+ this.externalLogger?.debug(`${this.MESSAGE_PREFIX} ${message}`, messageContext, error);
1132
+ }
1133
+ info(message, messageContext, error) {
1134
+ this.externalLogger?.info(`${this.MESSAGE_PREFIX} ${message}`, messageContext, error);
1135
+ }
1136
+ warn(message, messageContext, error) {
1137
+ this.externalLogger?.warn(`${this.MESSAGE_PREFIX} ${message}`, messageContext, error);
1138
+ }
1139
+ error(message, messageContext, error) {
1140
+ this.externalLogger?.error(`${this.MESSAGE_PREFIX} ${message}`, messageContext, error);
1141
+ }
1142
+ };
1143
+
1144
+ // src/client-v2/client-v2.ts
1145
+ var ForwardMPCClientV2 = class extends EventEmitter2 {
1146
+ static {
1147
+ __name(this, "ForwardMPCClientV2");
1148
+ }
1149
+ url;
1150
+ options;
1151
+ transport;
1152
+ sessionOptions;
1153
+ logger;
1154
+ session = null;
1155
+ _connectPromise = null;
1156
+ _handshaking = false;
1157
+ _disconnectedIntentionally = false;
1158
+ constructor(url, options = {}) {
1159
+ super();
1160
+ this.url = url;
1161
+ this.options = options;
1162
+ if (options.dangerouslyBypassAttestation) {
1163
+ console.warn("[ForwardMPCClientV2] dangerouslyBypassAttestation is enabled \u2014 attestation verification is disabled. Do not use in production.");
1164
+ } else if (!options.attestationVerifier && !options.attestationConfig) {
1165
+ console.warn("[ForwardMPCClientV2] No attestation verifier configured \u2014 connections will not be attested. This is insecure in production.");
1166
+ }
1167
+ this.logger = new Logger(this.options.logger);
1168
+ const transportOptions = {
1169
+ reconnectAttempts: this.options.reconnectAttempts,
1170
+ reconnectInterval: this.options.reconnectInterval,
1171
+ connectionTimeout: this.options.connectionTimeout,
1172
+ logger: this.logger
1173
+ };
1174
+ this.transport = new ForwardMPCTransport(url, transportOptions);
1175
+ const attestationVerifier = options.attestationVerifier ?? (options.attestationConfig ? new NitroAttestationVerifier(options.attestationConfig) : void 0);
1176
+ this.sessionOptions = {
1177
+ attestationVerifier,
1178
+ bypassAttestation: options.dangerouslyBypassAttestation,
1179
+ requestTimeout: options.requestTimeout
1180
+ };
1181
+ this.transport.on("connected", () => this.onTransportConnected());
1182
+ this.transport.on("disconnected", () => this.onTransportDisconnected());
1183
+ this.transport.on("error", (error) => this.emit("error", error));
1184
+ }
1185
+ get connected() {
1186
+ return this.transport.connected && this.session !== null;
1187
+ }
1188
+ /**
1189
+ * Opens the WebSocket connection and performs the ML-KEM-768 handshake.
1190
+ * Resolves once the session is fully established (and attested, if configured).
1191
+ * Concurrent calls coalesce on a single in-flight promise.
1192
+ */
1193
+ async connect(traceContext) {
1194
+ if (this.session) return;
1195
+ if (this._connectPromise) return this._connectPromise;
1196
+ this._connectPromise = this._doConnect(traceContext).finally(() => {
1197
+ this._connectPromise = null;
1198
+ });
1199
+ return this._connectPromise;
1200
+ }
1201
+ /**
1202
+ * Disposes the current session (zeroing crypto material) and closes the transport.
1203
+ */
1204
+ disconnect() {
1205
+ this._disconnectedIntentionally = true;
1206
+ this.session?.dispose();
1207
+ this.session = null;
1208
+ this.transport.disconnect();
1209
+ }
1210
+ async signMessage(params) {
1211
+ const session = await this.ensureSession();
1212
+ let messageToSign;
1213
+ if (typeof params.message === "string") {
1214
+ const hex = params.message.startsWith("0x") ? params.message.slice(2) : params.message;
1215
+ messageToSign = hexToBytes(hex);
1216
+ } else {
1217
+ messageToSign = params.message;
1218
+ }
1219
+ const encryptedKeyshare = encryptKeyshare(params.keyshare, session.sharedSecret, session.connectionId, fromDynamicSigningAlgorithm(params.signingAlgo));
1220
+ const request = new SignMessageV1RequestMessage({
1221
+ relayDomain: params.relayDomain,
1222
+ signingAlgo: params.signingAlgo,
1223
+ hashAlgo: params.hashAlgo,
1224
+ derivationPath: params.derivationPath,
1225
+ tweak: params.tweak,
1226
+ keyshare: encryptedKeyshare,
1227
+ message: messageToSign,
1228
+ roomUuid: params.roomUuid,
1229
+ traceContext: params.traceContext,
1230
+ userId: params.userId,
1231
+ environmentId: params.environmentId
1232
+ });
1233
+ const { signature } = await session.sendRequest(request);
1234
+ if (!signature) {
1235
+ throw new SessionServerError("No signature in response");
1236
+ }
1237
+ return {
1238
+ signature
1239
+ };
1240
+ }
1241
+ /**
1242
+ * MPC key generation for ECDSA and BIP340.
1243
+ * ED25519 is not supported here — use receiveKey() instead.
1244
+ */
1245
+ async keygen(params) {
1246
+ if (params.signingAlgo === SigningAlgorithm.ED25519) {
1247
+ throw new ClientUnsupportedAlgorithmError();
1248
+ }
1249
+ const session = await this.ensureSession();
1250
+ const encryptedKeygenInit = encryptKeygenInit(params.keygenInit, session.sharedSecret, session.connectionId);
1251
+ const request = new KeygenV1RequestMessage({
1252
+ relayDomain: params.relayDomain,
1253
+ signingAlgo: params.signingAlgo,
1254
+ roomUuid: params.roomUuid,
1255
+ numParties: params.numParties,
1256
+ threshold: params.threshold,
1257
+ keygenInit: encryptedKeygenInit,
1258
+ keygenIds: params.keygenIds,
1259
+ traceContext: params.traceContext,
1260
+ userId: params.userId,
1261
+ environmentId: params.environmentId
1262
+ });
1263
+ const { keygenResult } = await session.sendRequest(request);
1264
+ if (!keygenResult) {
1265
+ throw new SessionServerError("No keygen result in response");
1266
+ }
1267
+ return decryptKeygenResult(keygenResult, session.sharedSecret, session.connectionId);
1268
+ }
1269
+ /**
1270
+ * Receives an ED25519 key generated by another party (ExportableEd25519).
1271
+ */
1272
+ async receiveKey(params) {
1273
+ const session = await this.ensureSession();
1274
+ const encryptedKeygenInit = encryptKeygenInit(params.keygenInit, session.sharedSecret, session.connectionId);
1275
+ const request = new ReceiveKeyV1RequestMessage({
1276
+ relayDomain: params.relayDomain,
1277
+ signingAlgo: "ed25519",
1278
+ roomUuid: params.roomUuid,
1279
+ numParties: params.numParties,
1280
+ threshold: params.threshold,
1281
+ keygenInit: encryptedKeygenInit,
1282
+ keygenIds: params.keygenIds,
1283
+ traceContext: params.traceContext,
1284
+ userId: params.userId,
1285
+ environmentId: params.environmentId
1286
+ });
1287
+ const { keygenResult } = await session.sendRequest(request);
1288
+ if (!keygenResult) {
1289
+ throw new SessionServerError("No keygen result in response");
1290
+ }
1291
+ return decryptKeygenResult(keygenResult, session.sharedSecret, session.connectionId);
1292
+ }
1293
+ /**
1294
+ * Ensures an active session exists, auto-connecting if needed.
1295
+ */
1296
+ async ensureSession() {
1297
+ if (this.session) return this.session;
1298
+ await this.connect();
1299
+ if (!this.session) {
1300
+ throw new ClientSessionEstablishFailedError();
1301
+ }
1302
+ return this.session;
1303
+ }
1304
+ async _runHandshake(traceContext) {
1305
+ this.session = await Session.handshake(this.transport, traceContext, this.sessionOptions, this.logger);
1306
+ this.emit("connected");
1307
+ }
1308
+ async _doConnect(traceContext) {
1309
+ this._handshaking = true;
1310
+ try {
1311
+ await this.transport.connect();
1312
+ await this._runHandshake(traceContext);
1313
+ } finally {
1314
+ this._handshaking = false;
1315
+ }
1316
+ }
1317
+ /**
1318
+ * Called when the transport connects (both initial and after auto-reconnect).
1319
+ * The `_handshaking` flag is set synchronously at the top of `_doConnect`
1320
+ * before any await, so it reliably indicates when we already own the handshake.
1321
+ *
1322
+ * For transport-initiated reconnects, `_connectPromise` is set so that
1323
+ * concurrent `connect()` or `ensureSession()` callers coalesce on the
1324
+ * in-flight handshake rather than initiating a second one.
1325
+ */
1326
+ onTransportConnected() {
1327
+ if (this._handshaking) return;
1328
+ this._handshaking = true;
1329
+ const doHandshake = /* @__PURE__ */ __name(async () => {
1330
+ try {
1331
+ await this._runHandshake();
1332
+ } catch (error) {
1333
+ this.emit("error", error instanceof Error ? error : new Error(String(error)));
1334
+ } finally {
1335
+ this._handshaking = false;
1336
+ this._connectPromise = null;
1337
+ }
1338
+ }, "doHandshake");
1339
+ this._connectPromise = doHandshake();
1340
+ }
1341
+ onTransportDisconnected() {
1342
+ const intentional = this._disconnectedIntentionally;
1343
+ this._disconnectedIntentionally = false;
1344
+ this.session?.dispose();
1345
+ this.session = null;
1346
+ if (!intentional) {
1347
+ this.logger.warn("Unexpected WebSocket disconnect");
1348
+ }
1349
+ this.emit("disconnected");
1350
+ }
1351
+ };
1352
+
1353
+ // src/client-v2/singleton.ts
1354
+ var ForwardMPCClientSingleton = class extends ForwardMPCClientV2 {
1355
+ static {
1356
+ __name(this, "ForwardMPCClientSingleton");
1357
+ }
1358
+ };
557
1359
 
558
- export { ForwardMPCClient };
1360
+ export { ClientError, ClientSessionEstablishFailedError, ClientUnsupportedAlgorithmError, ErrorCode, ForwardMPCClient, ForwardMPCClientSingleton, ForwardMPCClientV2, ForwardMPCError, ForwardMPCErrorType, NitroAttestationVerifier, SessionAttestationError, SessionAttestationNonceMissingError, SessionDisposedError, SessionError, SessionHandshakeError, SessionHandshakeInvalidResponseError, SessionMessageParseError, SessionRemoteError, SessionRequestTimeoutError, SessionServerError, TransportConnectionError, TransportConnectionTimeoutError, TransportError, TransportNotConnectedError };
559
1361
  //# sourceMappingURL=index.js.map
560
1362
  //# sourceMappingURL=index.js.map