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