@qrlwallet/connect 2.0.0 → 3.1.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/README.md +3 -1
- package/dist/index.d.mts +285 -18
- package/dist/index.d.ts +285 -18
- package/dist/index.js +914 -144
- package/dist/index.mjs +896 -143
- package/package.json +10 -8
package/dist/index.mjs
CHANGED
|
@@ -3,68 +3,144 @@ import EventEmitter4 from "eventemitter3";
|
|
|
3
3
|
|
|
4
4
|
// src/ConnectionManager.ts
|
|
5
5
|
import EventEmitter3 from "eventemitter3";
|
|
6
|
-
import { v4 as uuidv4 } from "uuid";
|
|
7
6
|
|
|
8
7
|
// src/KeyExchange.ts
|
|
9
8
|
import EventEmitter from "eventemitter3";
|
|
10
9
|
|
|
11
|
-
// src/
|
|
10
|
+
// src/crypto/primitives.ts
|
|
12
11
|
import { ml_kem768 } from "@noble/post-quantum/ml-kem.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
var DIR_DAPP_TX = new Uint8Array([0, 0, 0, 1]);
|
|
17
|
-
var DIR_WALLET_TX = new Uint8Array([0, 0, 0, 2]);
|
|
18
|
-
var ML_KEM_768_CT_LEN = 1088;
|
|
19
|
-
var AEAD_KEY_LEN = 32;
|
|
20
|
-
function subtle() {
|
|
12
|
+
import { shake256 } from "@noble/hashes/sha3.js";
|
|
13
|
+
import * as mldsa from "@theqrl/mldsa87";
|
|
14
|
+
function webCrypto() {
|
|
21
15
|
const c = globalThis.crypto;
|
|
22
|
-
if (!c
|
|
23
|
-
throw new Error("
|
|
16
|
+
if (!c) {
|
|
17
|
+
throw new Error("crypto: WebCrypto is not available in this environment");
|
|
18
|
+
}
|
|
19
|
+
return c;
|
|
20
|
+
}
|
|
21
|
+
function subtle() {
|
|
22
|
+
const c = webCrypto();
|
|
23
|
+
if (!c.subtle) {
|
|
24
|
+
throw new Error("crypto: WebCrypto SubtleCrypto is not available in this environment");
|
|
24
25
|
}
|
|
25
26
|
return c.subtle;
|
|
26
27
|
}
|
|
27
28
|
function bs(u) {
|
|
28
|
-
|
|
29
|
+
if (u.buffer instanceof ArrayBuffer) {
|
|
30
|
+
return new Uint8Array(u.buffer, u.byteOffset, u.byteLength);
|
|
31
|
+
}
|
|
32
|
+
const copy = new Uint8Array(u.byteLength);
|
|
33
|
+
copy.set(u);
|
|
34
|
+
return copy;
|
|
29
35
|
}
|
|
30
|
-
function
|
|
36
|
+
function mlkemKeygen() {
|
|
31
37
|
const { publicKey, secretKey } = ml_kem768.keygen();
|
|
32
38
|
return { pk: publicKey, sk: secretKey };
|
|
33
39
|
}
|
|
34
|
-
function
|
|
40
|
+
function mlkemEncaps(pk) {
|
|
35
41
|
const { cipherText, sharedSecret } = ml_kem768.encapsulate(pk);
|
|
36
42
|
return { ct: cipherText, ss: sharedSecret };
|
|
37
43
|
}
|
|
38
|
-
function
|
|
44
|
+
function mlkemDecaps(sk, ct) {
|
|
39
45
|
return ml_kem768.decapsulate(ct, sk);
|
|
40
46
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return new Uint8Array(await subtle().digest("SHA-256", bs(buf)));
|
|
47
|
+
function shake256Digest(data, dkLen) {
|
|
48
|
+
return shake256(data, { dkLen });
|
|
44
49
|
}
|
|
45
|
-
async function
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
async function sha256(data) {
|
|
51
|
+
return new Uint8Array(await subtle().digest("SHA-256", bs(data)));
|
|
52
|
+
}
|
|
53
|
+
function mldsaVerify(signature, digest, publicKey, ctx) {
|
|
54
|
+
return mldsa.cryptoSignVerify(signature, digest, publicKey, ctx);
|
|
55
|
+
}
|
|
56
|
+
async function hkdfAesGcmKey(ikm, salt, info) {
|
|
57
|
+
const ikmKey = await subtle().importKey("raw", bs(ikm), "HKDF", false, ["deriveKey"]);
|
|
48
58
|
return subtle().deriveKey(
|
|
49
|
-
{ name: "HKDF", hash: "SHA-256", salt:
|
|
50
|
-
|
|
59
|
+
{ name: "HKDF", hash: "SHA-256", salt: bs(salt), info: bs(info) },
|
|
60
|
+
ikmKey,
|
|
51
61
|
{ name: "AES-GCM", length: 256 },
|
|
52
62
|
true,
|
|
53
63
|
["encrypt", "decrypt"]
|
|
54
64
|
);
|
|
55
65
|
}
|
|
56
|
-
async function
|
|
57
|
-
if (raw.length !== AEAD_KEY_LEN) {
|
|
58
|
-
throw new Error(`PQCrypto: raw AEAD key must be ${AEAD_KEY_LEN} bytes`);
|
|
59
|
-
}
|
|
66
|
+
async function importAesGcmKey(raw) {
|
|
60
67
|
return subtle().importKey("raw", bs(raw), { name: "AES-GCM", length: 256 }, true, [
|
|
61
68
|
"encrypt",
|
|
62
69
|
"decrypt"
|
|
63
70
|
]);
|
|
64
71
|
}
|
|
72
|
+
async function exportAesGcmKey(key) {
|
|
73
|
+
return new Uint8Array(await subtle().exportKey("raw", key));
|
|
74
|
+
}
|
|
75
|
+
async function aesGcmEncrypt(key, iv, aad2, plaintext) {
|
|
76
|
+
const ct = await subtle().encrypt(
|
|
77
|
+
{ name: "AES-GCM", iv: bs(iv), additionalData: bs(aad2) },
|
|
78
|
+
key,
|
|
79
|
+
bs(plaintext)
|
|
80
|
+
);
|
|
81
|
+
return new Uint8Array(ct);
|
|
82
|
+
}
|
|
83
|
+
async function aesGcmDecrypt(key, iv, aad2, ciphertext) {
|
|
84
|
+
const pt = await subtle().decrypt(
|
|
85
|
+
{ name: "AES-GCM", iv: bs(iv), additionalData: bs(aad2) },
|
|
86
|
+
key,
|
|
87
|
+
bs(ciphertext)
|
|
88
|
+
);
|
|
89
|
+
return new Uint8Array(pt);
|
|
90
|
+
}
|
|
91
|
+
function randomBytes(n) {
|
|
92
|
+
const c = webCrypto();
|
|
93
|
+
if (typeof c.getRandomValues !== "function") {
|
|
94
|
+
throw new Error("crypto: crypto.getRandomValues is not available");
|
|
95
|
+
}
|
|
96
|
+
return c.getRandomValues(new Uint8Array(n));
|
|
97
|
+
}
|
|
98
|
+
function randomUuid() {
|
|
99
|
+
const b = randomBytes(16);
|
|
100
|
+
b[6] = (b[6] ?? 0) & 15 | 64;
|
|
101
|
+
b[8] = (b[8] ?? 0) & 63 | 128;
|
|
102
|
+
const hex = Array.from(b, (v) => v.toString(16).padStart(2, "0")).join("");
|
|
103
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
104
|
+
}
|
|
105
|
+
function constantTimeEquals(a, b) {
|
|
106
|
+
if (a.length !== b.length) return false;
|
|
107
|
+
let d = 0;
|
|
108
|
+
for (let i = 0; i < a.length; i++) d |= (a[i] ?? 0) ^ (b[i] ?? 0);
|
|
109
|
+
return d === 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/PQCrypto.ts
|
|
113
|
+
var textEncoder = new TextEncoder();
|
|
114
|
+
var LABEL = textEncoder.encode("pq-pair/v1");
|
|
115
|
+
var LABEL_AEAD_SUFFIX = textEncoder.encode(" aead");
|
|
116
|
+
var DIR_DAPP_TX = new Uint8Array([0, 0, 0, 1]);
|
|
117
|
+
var DIR_WALLET_TX = new Uint8Array([0, 0, 0, 2]);
|
|
118
|
+
var ML_KEM_768_CT_LEN = 1088;
|
|
119
|
+
var AEAD_KEY_LEN = 32;
|
|
120
|
+
function kemKeygen() {
|
|
121
|
+
return mlkemKeygen();
|
|
122
|
+
}
|
|
123
|
+
function kemEncaps(pk) {
|
|
124
|
+
return mlkemEncaps(pk);
|
|
125
|
+
}
|
|
126
|
+
function kemDecaps(sk, ct) {
|
|
127
|
+
return mlkemDecaps(sk, ct);
|
|
128
|
+
}
|
|
129
|
+
async function transcriptHash(cid, pk, ct) {
|
|
130
|
+
return sha256(concat(LABEL, cid, pk, ct));
|
|
131
|
+
}
|
|
132
|
+
async function deriveAeadKey(ss, htx) {
|
|
133
|
+
const info = concat(LABEL, LABEL_AEAD_SUFFIX, htx);
|
|
134
|
+
return hkdfAesGcmKey(ss, new Uint8Array(32), info);
|
|
135
|
+
}
|
|
136
|
+
async function importRawAeadKey(raw) {
|
|
137
|
+
if (raw.length !== AEAD_KEY_LEN) {
|
|
138
|
+
throw new Error(`PQCrypto: raw AEAD key must be ${AEAD_KEY_LEN} bytes`);
|
|
139
|
+
}
|
|
140
|
+
return importAesGcmKey(raw);
|
|
141
|
+
}
|
|
65
142
|
async function exportRawAeadKey(key) {
|
|
66
|
-
|
|
67
|
-
return new Uint8Array(buf);
|
|
143
|
+
return exportAesGcmKey(key);
|
|
68
144
|
}
|
|
69
145
|
function nonce(dir, seq) {
|
|
70
146
|
if (dir.length !== 4) {
|
|
@@ -86,20 +162,10 @@ function aad(htx, seq) {
|
|
|
86
162
|
return out;
|
|
87
163
|
}
|
|
88
164
|
async function seal(key, dir, seq, htx, pt) {
|
|
89
|
-
|
|
90
|
-
{ name: "AES-GCM", iv: bs(nonce(dir, seq)), additionalData: bs(aad(htx, seq)) },
|
|
91
|
-
key,
|
|
92
|
-
bs(pt)
|
|
93
|
-
);
|
|
94
|
-
return new Uint8Array(ct);
|
|
165
|
+
return aesGcmEncrypt(key, nonce(dir, seq), aad(htx, seq), pt);
|
|
95
166
|
}
|
|
96
167
|
async function open(key, dir, seq, htx, ct) {
|
|
97
|
-
|
|
98
|
-
{ name: "AES-GCM", iv: bs(nonce(dir, seq)), additionalData: bs(aad(htx, seq)) },
|
|
99
|
-
key,
|
|
100
|
-
bs(ct)
|
|
101
|
-
);
|
|
102
|
-
return new Uint8Array(pt);
|
|
168
|
+
return aesGcmDecrypt(key, nonce(dir, seq), aad(htx, seq), ct);
|
|
103
169
|
}
|
|
104
170
|
function zeroize(b) {
|
|
105
171
|
b.fill(0);
|
|
@@ -120,7 +186,7 @@ function toBase64(bytes) {
|
|
|
120
186
|
let bin = "";
|
|
121
187
|
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
122
188
|
const slice = bytes.subarray(i, i + CHUNK);
|
|
123
|
-
bin += String.fromCharCode.apply(null, slice);
|
|
189
|
+
bin += String.fromCharCode.apply(null, Array.from(slice));
|
|
124
190
|
}
|
|
125
191
|
return btoa(bin);
|
|
126
192
|
}
|
|
@@ -130,12 +196,6 @@ function fromBase64(b64) {
|
|
|
130
196
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
131
197
|
return out;
|
|
132
198
|
}
|
|
133
|
-
function constantTimeEquals(a, b) {
|
|
134
|
-
if (a.length !== b.length) return false;
|
|
135
|
-
let d = 0;
|
|
136
|
-
for (let i = 0; i < a.length; i++) d |= a[i] ^ b[i];
|
|
137
|
-
return d === 0;
|
|
138
|
-
}
|
|
139
199
|
|
|
140
200
|
// src/config.ts
|
|
141
201
|
var DEFAULT_RELAY_URL = "https://qrlwallet.com";
|
|
@@ -145,15 +205,13 @@ var SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
|
145
205
|
var STORAGE_KEY_PREFIX = "@qrlwallet/connect";
|
|
146
206
|
var REQUEST_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
147
207
|
var WALLET_UNRESPONSIVE_MS = 30 * 1e3;
|
|
208
|
+
var RECONNECT_WALLET_PROBE_MS = 12 * 1e3;
|
|
148
209
|
var RESTRICTED_METHODS = /* @__PURE__ */ new Set([
|
|
149
210
|
"qrl_requestAccounts",
|
|
150
211
|
"qrl_sendTransaction",
|
|
151
212
|
"qrl_signTransaction",
|
|
152
|
-
"
|
|
153
|
-
"personal_sign",
|
|
213
|
+
"qrl_signMessage",
|
|
154
214
|
"qrl_signTypedData",
|
|
155
|
-
"qrl_signTypedData_v3",
|
|
156
|
-
"qrl_signTypedData_v4",
|
|
157
215
|
"wallet_addQrlChain",
|
|
158
216
|
"wallet_switchQrlChain"
|
|
159
217
|
]);
|
|
@@ -257,6 +315,13 @@ var KeyExchange = class extends EventEmitter {
|
|
|
257
315
|
this.awaitingSynAck = false;
|
|
258
316
|
this.awaitingAck = false;
|
|
259
317
|
this.keysExchanged = false;
|
|
318
|
+
// The ACK we produced for the current handshake. A wallet that lost its
|
|
319
|
+
// transport right after sending SYNACK re-sends the identical SYNACK on
|
|
320
|
+
// rejoin; we answer with this cached ACK so the handshake converges
|
|
321
|
+
// instead of stalling (onSynAck ignores duplicates and would otherwise
|
|
322
|
+
// never reply). Cleared on reset; not persisted (a reloaded dApp has a
|
|
323
|
+
// completed session and the wallet is no longer awaiting an ACK).
|
|
324
|
+
this.lastAck = null;
|
|
260
325
|
this.isOriginator = isOriginator;
|
|
261
326
|
if (restored) {
|
|
262
327
|
this.session = restored;
|
|
@@ -287,7 +352,7 @@ var KeyExchange = class extends EventEmitter {
|
|
|
287
352
|
*/
|
|
288
353
|
async onSynAck(cid, msg) {
|
|
289
354
|
if (!this.isOriginator) {
|
|
290
|
-
warn("KeyExchange", "Responder received SYNACK
|
|
355
|
+
warn("KeyExchange", "Responder received SYNACK - ignoring");
|
|
291
356
|
return null;
|
|
292
357
|
}
|
|
293
358
|
if (!this.awaitingSynAck || !this.keypair) {
|
|
@@ -330,11 +395,13 @@ var KeyExchange = class extends EventEmitter {
|
|
|
330
395
|
this.step = "key_handshake_ACK" /* ACK */;
|
|
331
396
|
this.emit("keys_exchanged");
|
|
332
397
|
this.emit("step_change", this.step);
|
|
333
|
-
|
|
398
|
+
const ack = {
|
|
334
399
|
type: "key_handshake_ACK" /* ACK */,
|
|
335
400
|
c1: toBase64(c1),
|
|
336
401
|
v: PROTOCOL_VERSION
|
|
337
402
|
};
|
|
403
|
+
this.lastAck = ack;
|
|
404
|
+
return ack;
|
|
338
405
|
}
|
|
339
406
|
/**
|
|
340
407
|
* Responder (wallet) side: begin handshake from a scanned QR.
|
|
@@ -375,7 +442,7 @@ var KeyExchange = class extends EventEmitter {
|
|
|
375
442
|
*/
|
|
376
443
|
async onAck(msg) {
|
|
377
444
|
if (this.isOriginator) {
|
|
378
|
-
warn("KeyExchange", "Originator received ACK
|
|
445
|
+
warn("KeyExchange", "Originator received ACK - ignoring");
|
|
379
446
|
return;
|
|
380
447
|
}
|
|
381
448
|
if (!this.awaitingAck) {
|
|
@@ -402,23 +469,17 @@ var KeyExchange = class extends EventEmitter {
|
|
|
402
469
|
/** Encrypt a string for the counterparty. Returns base64. */
|
|
403
470
|
async encryptMessage(data) {
|
|
404
471
|
if (!this.session) {
|
|
405
|
-
throw new Error("KeyExchange: cannot encrypt
|
|
472
|
+
throw new Error("KeyExchange: cannot encrypt - session not established");
|
|
406
473
|
}
|
|
474
|
+
const seq = this.session.sendSeq++;
|
|
407
475
|
const pt = textEncoder2.encode(data);
|
|
408
|
-
const ct = await seal(
|
|
409
|
-
this.session.key,
|
|
410
|
-
this.session.sendDir,
|
|
411
|
-
this.session.sendSeq,
|
|
412
|
-
this.session.htx,
|
|
413
|
-
pt
|
|
414
|
-
);
|
|
415
|
-
this.session.sendSeq++;
|
|
476
|
+
const ct = await seal(this.session.key, this.session.sendDir, seq, this.session.htx, pt);
|
|
416
477
|
return toBase64(ct);
|
|
417
478
|
}
|
|
418
479
|
/** Decrypt a base64 ciphertext from the counterparty. */
|
|
419
480
|
async decryptMessage(b64) {
|
|
420
481
|
if (!this.session) {
|
|
421
|
-
throw new Error("KeyExchange: cannot decrypt
|
|
482
|
+
throw new Error("KeyExchange: cannot decrypt - session not established");
|
|
422
483
|
}
|
|
423
484
|
const ct = fromBase64(b64);
|
|
424
485
|
const pt = await open(
|
|
@@ -444,12 +505,17 @@ var KeyExchange = class extends EventEmitter {
|
|
|
444
505
|
this.keysExchanged = false;
|
|
445
506
|
this.awaitingSynAck = false;
|
|
446
507
|
this.awaitingAck = false;
|
|
508
|
+
this.lastAck = null;
|
|
447
509
|
this.step = "key_handshake_SYN" /* SYN */;
|
|
448
510
|
if (emit) this.emit("step_change", this.step);
|
|
449
511
|
}
|
|
450
512
|
areKeysExchanged() {
|
|
451
513
|
return this.keysExchanged;
|
|
452
514
|
}
|
|
515
|
+
/** The ACK produced for the current handshake, for duplicate-SYNACK replies. */
|
|
516
|
+
getLastAck() {
|
|
517
|
+
return this.lastAck;
|
|
518
|
+
}
|
|
453
519
|
getSession() {
|
|
454
520
|
return this.session;
|
|
455
521
|
}
|
|
@@ -516,7 +582,10 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
516
582
|
* Connect to the relay server.
|
|
517
583
|
*/
|
|
518
584
|
connect() {
|
|
519
|
-
if (this.socket
|
|
585
|
+
if (this.socket) {
|
|
586
|
+
if (!this.socket.connected) this.socket.connect();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
520
589
|
log("Socket", `Connecting to ${this.relayUrl}`);
|
|
521
590
|
this.socket = io(this.relayUrl, {
|
|
522
591
|
path: RELAY_PATH,
|
|
@@ -534,7 +603,7 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
534
603
|
const reconnectChannelId = this.channelId;
|
|
535
604
|
this.joinChannelNow(reconnectChannelId).then((result) => {
|
|
536
605
|
this.settlePendingJoin(reconnectChannelId, { result });
|
|
537
|
-
this.emit("reconnected");
|
|
606
|
+
this.emit("reconnected", result);
|
|
538
607
|
}).catch((err) => {
|
|
539
608
|
const reconnectErr = err instanceof Error ? err : new Error(String(err));
|
|
540
609
|
this.settlePendingJoin(reconnectChannelId, { error: reconnectErr });
|
|
@@ -552,8 +621,14 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
552
621
|
this.emit("message", data);
|
|
553
622
|
});
|
|
554
623
|
this.socket.on("participants_changed", (data) => {
|
|
555
|
-
|
|
556
|
-
|
|
624
|
+
if (typeof data !== "object" || data === null) return;
|
|
625
|
+
const rec = { ...data };
|
|
626
|
+
const event = typeof rec.event === "string" ? rec.event : "";
|
|
627
|
+
log("Socket", `Participants changed: ${event}`);
|
|
628
|
+
this.emit("participants_changed", {
|
|
629
|
+
event,
|
|
630
|
+
...typeof rec.clientType === "string" ? { clientType: rec.clientType } : {}
|
|
631
|
+
});
|
|
557
632
|
});
|
|
558
633
|
this.socket.on("connect_error", (err) => {
|
|
559
634
|
warn("Socket", `Connection error: ${err.message}`);
|
|
@@ -566,7 +641,7 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
566
641
|
* it doesn't fire after the fact.
|
|
567
642
|
*/
|
|
568
643
|
settlePendingJoin(channelId, outcome) {
|
|
569
|
-
if (
|
|
644
|
+
if (this.pendingJoin?.channelId !== channelId) return;
|
|
570
645
|
clearTimeout(this.pendingJoin.watchdog);
|
|
571
646
|
const pending = this.pendingJoin;
|
|
572
647
|
this.pendingJoin = null;
|
|
@@ -633,12 +708,14 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
633
708
|
if (response.success) {
|
|
634
709
|
log("Socket", `Joined channel ${channelId}`);
|
|
635
710
|
resolve({
|
|
636
|
-
bufferedMessages: response.bufferedMessages
|
|
637
|
-
channelPublicKey: response.channelPublicKey ?? null
|
|
711
|
+
bufferedMessages: response.bufferedMessages ?? [],
|
|
712
|
+
channelPublicKey: response.channelPublicKey ?? null,
|
|
713
|
+
participants: response.participants ?? [],
|
|
714
|
+
terminated: response.terminated === true
|
|
638
715
|
});
|
|
639
716
|
} else {
|
|
640
717
|
error("Socket", `Failed to join channel: ${response.error}`);
|
|
641
|
-
reject(new Error(response.error
|
|
718
|
+
reject(new Error(response.error ?? "Failed to join channel"));
|
|
642
719
|
}
|
|
643
720
|
}
|
|
644
721
|
);
|
|
@@ -661,7 +738,7 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
661
738
|
if (response?.success) {
|
|
662
739
|
resolve({ success: true, buffered: response.buffered });
|
|
663
740
|
} else {
|
|
664
|
-
reject(new Error(response?.error
|
|
741
|
+
reject(new Error(response?.error ?? "Failed to send message"));
|
|
665
742
|
}
|
|
666
743
|
}
|
|
667
744
|
);
|
|
@@ -717,26 +794,26 @@ var DECODE = (() => {
|
|
|
717
794
|
})();
|
|
718
795
|
function decodeChar(charCode) {
|
|
719
796
|
if (charCode < 0 || charCode >= 128) return -1;
|
|
720
|
-
return DECODE[charCode];
|
|
797
|
+
return DECODE[charCode] ?? -1;
|
|
721
798
|
}
|
|
722
799
|
function base45Encode(bytes) {
|
|
723
800
|
const n = bytes.length;
|
|
724
801
|
let out = "";
|
|
725
802
|
let i = 0;
|
|
726
803
|
while (i + 2 <= n) {
|
|
727
|
-
const v = bytes[i] << 8 | bytes[i + 1];
|
|
804
|
+
const v = (bytes[i] ?? 0) << 8 | (bytes[i + 1] ?? 0);
|
|
728
805
|
const c2 = Math.floor(v / 2025);
|
|
729
806
|
const r = v - c2 * 2025;
|
|
730
807
|
const c1 = Math.floor(r / 45);
|
|
731
808
|
const c0 = r - c1 * 45;
|
|
732
|
-
out += ALPHABET[c0]
|
|
809
|
+
out += `${ALPHABET[c0] ?? ""}${ALPHABET[c1] ?? ""}${ALPHABET[c2] ?? ""}`;
|
|
733
810
|
i += 2;
|
|
734
811
|
}
|
|
735
812
|
if (i < n) {
|
|
736
|
-
const v = bytes[i];
|
|
813
|
+
const v = bytes[i] ?? 0;
|
|
737
814
|
const c1 = Math.floor(v / 45);
|
|
738
815
|
const c0 = v - c1 * 45;
|
|
739
|
-
out += ALPHABET[c0]
|
|
816
|
+
out += `${ALPHABET[c0] ?? ""}${ALPHABET[c1] ?? ""}`;
|
|
740
817
|
}
|
|
741
818
|
return out;
|
|
742
819
|
}
|
|
@@ -785,29 +862,18 @@ var FP_LABEL = new TextEncoder().encode("pq-fp/v2");
|
|
|
785
862
|
var CID_LEN = 16;
|
|
786
863
|
var FP_LEN = 32;
|
|
787
864
|
var BLOB_LEN = 4 + CID_LEN + FP_LEN;
|
|
788
|
-
function bs2(u) {
|
|
789
|
-
return u;
|
|
790
|
-
}
|
|
791
865
|
async function computeFingerprint(cid, pk) {
|
|
792
866
|
if (cid.length !== CID_LEN) {
|
|
793
867
|
throw new Error(`qrUri: cid must be ${CID_LEN} bytes`);
|
|
794
868
|
}
|
|
795
|
-
const c = globalThis.crypto;
|
|
796
|
-
if (!c || !c.subtle) {
|
|
797
|
-
throw new Error("qrUri: WebCrypto SubtleCrypto is not available");
|
|
798
|
-
}
|
|
799
869
|
const buf = new Uint8Array(FP_LABEL.length + cid.length + pk.length);
|
|
800
870
|
buf.set(FP_LABEL, 0);
|
|
801
871
|
buf.set(cid, FP_LABEL.length);
|
|
802
872
|
buf.set(pk, FP_LABEL.length + cid.length);
|
|
803
|
-
|
|
804
|
-
return new Uint8Array(digest);
|
|
873
|
+
return sha256(buf);
|
|
805
874
|
}
|
|
806
875
|
function fingerprintEquals(a, b) {
|
|
807
|
-
|
|
808
|
-
let diff = 0;
|
|
809
|
-
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
810
|
-
return diff === 0;
|
|
876
|
+
return constantTimeEquals(a, b);
|
|
811
877
|
}
|
|
812
878
|
async function generateConnectionURI(cid, pk, relayUrl) {
|
|
813
879
|
if (cid.length !== CID_LEN) {
|
|
@@ -846,7 +912,7 @@ async function parseConnectionURI(uri) {
|
|
|
846
912
|
}
|
|
847
913
|
if (params.has("channelId") || params.has("pubKey")) {
|
|
848
914
|
throw new Error(
|
|
849
|
-
"qrUri: legacy v1 URI detected
|
|
915
|
+
"qrUri: legacy v1 URI detected - this wallet and this dApp must both run protocol v2"
|
|
850
916
|
);
|
|
851
917
|
}
|
|
852
918
|
const q = params.get("q");
|
|
@@ -862,7 +928,7 @@ async function parseConnectionURI(uri) {
|
|
|
862
928
|
}
|
|
863
929
|
if (blob.length !== BLOB_LEN) {
|
|
864
930
|
if (blob.length === 1208 && startsWith(blob, PQP1_MAGIC)) {
|
|
865
|
-
throw new Error("qrUri: legacy PQP1 URI detected
|
|
931
|
+
throw new Error("qrUri: legacy PQP1 URI detected - regenerate the QR with a v2.0+ dApp SDK");
|
|
866
932
|
}
|
|
867
933
|
throw new Error(`qrUri: expected ${BLOB_LEN}-byte blob, got ${blob.length}`);
|
|
868
934
|
}
|
|
@@ -871,8 +937,8 @@ async function parseConnectionURI(uri) {
|
|
|
871
937
|
}
|
|
872
938
|
const cid = blob.slice(4, 4 + CID_LEN);
|
|
873
939
|
const fp = blob.slice(4 + CID_LEN, 4 + CID_LEN + FP_LEN);
|
|
874
|
-
const
|
|
875
|
-
return { cid, fp, relayUrl };
|
|
940
|
+
const r = params.get("r");
|
|
941
|
+
return { cid, fp, relayUrl: r === null || r === "" ? void 0 : r };
|
|
876
942
|
}
|
|
877
943
|
function cidToString(cid) {
|
|
878
944
|
if (cid.length !== CID_LEN) {
|
|
@@ -880,7 +946,7 @@ function cidToString(cid) {
|
|
|
880
946
|
}
|
|
881
947
|
let hex = "";
|
|
882
948
|
for (let i = 0; i < CID_LEN; i++) {
|
|
883
|
-
hex += cid[i].toString(16).padStart(2, "0");
|
|
949
|
+
hex += (cid[i] ?? 0).toString(16).padStart(2, "0");
|
|
884
950
|
}
|
|
885
951
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
886
952
|
}
|
|
@@ -896,15 +962,79 @@ function cidFromString(s) {
|
|
|
896
962
|
return out;
|
|
897
963
|
}
|
|
898
964
|
function cidRandom() {
|
|
899
|
-
|
|
900
|
-
if (!c || typeof c.getRandomValues !== "function") {
|
|
901
|
-
throw new Error("qrUri: crypto.getRandomValues is not available");
|
|
902
|
-
}
|
|
903
|
-
return c.getRandomValues(new Uint8Array(CID_LEN));
|
|
965
|
+
return randomBytes(CID_LEN);
|
|
904
966
|
}
|
|
905
967
|
|
|
906
968
|
// src/ConnectionManager.ts
|
|
907
969
|
var DAPP_PARTICIPANT_CONFLICT_ERROR_MSG = "dapp participant is already connected";
|
|
970
|
+
function isRecord(v) {
|
|
971
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
972
|
+
}
|
|
973
|
+
function isStringArray(v) {
|
|
974
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
975
|
+
}
|
|
976
|
+
function isRelayMessage(v) {
|
|
977
|
+
return isRecord(v) && typeof v.id === "string" && (v.clientType === "dapp" || v.clientType === "wallet") && "message" in v && (typeof v.message === "string" || typeof v.message === "object" && v.message !== null);
|
|
978
|
+
}
|
|
979
|
+
var MESSAGE_TYPE_BY_VALUE = Object.fromEntries(
|
|
980
|
+
Object.values(MessageType).map((m) => [m, m])
|
|
981
|
+
);
|
|
982
|
+
function parseJsonRpcResponse(msg) {
|
|
983
|
+
if (typeof msg.id !== "string" && typeof msg.id !== "number") return null;
|
|
984
|
+
const out = {
|
|
985
|
+
jsonrpc: typeof msg.jsonrpc === "string" ? msg.jsonrpc : "2.0",
|
|
986
|
+
id: msg.id
|
|
987
|
+
};
|
|
988
|
+
if ("result" in msg) out.result = msg.result;
|
|
989
|
+
if (msg.error !== void 0) {
|
|
990
|
+
if (!isRecord(msg.error) || typeof msg.error.message !== "string") return null;
|
|
991
|
+
out.error = {
|
|
992
|
+
code: typeof msg.error.code === "number" ? msg.error.code : -32e3,
|
|
993
|
+
message: msg.error.message,
|
|
994
|
+
data: msg.error.data
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
function parsePersistedKex(v) {
|
|
1000
|
+
if (!isRecord(v)) return null;
|
|
1001
|
+
const { cid, kAeadRaw, htx, sendDir, recvDir, sendSeq, recvSeq } = v;
|
|
1002
|
+
if (typeof cid !== "string" || typeof kAeadRaw !== "string" || typeof htx !== "string" || typeof sendDir !== "string" || typeof recvDir !== "string" || typeof sendSeq !== "number" || !Number.isInteger(sendSeq) || sendSeq < 0 || typeof recvSeq !== "number" || !Number.isInteger(recvSeq) || recvSeq < 0) {
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
return { cid, kAeadRaw, htx, sendDir, recvDir, sendSeq, recvSeq };
|
|
1006
|
+
}
|
|
1007
|
+
function parseDAppMetadata(v) {
|
|
1008
|
+
if (!isRecord(v) || typeof v.name !== "string" || typeof v.url !== "string") return null;
|
|
1009
|
+
const meta = { name: v.name, url: v.url };
|
|
1010
|
+
if (typeof v.icon === "string") meta.icon = v.icon;
|
|
1011
|
+
if (typeof v.redirectUrl === "string") meta.redirectUrl = v.redirectUrl;
|
|
1012
|
+
return meta;
|
|
1013
|
+
}
|
|
1014
|
+
function parseStoredSession(raw) {
|
|
1015
|
+
let v;
|
|
1016
|
+
try {
|
|
1017
|
+
v = JSON.parse(raw);
|
|
1018
|
+
} catch {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
if (!isRecord(v) || v.version !== 3) return null;
|
|
1022
|
+
if (typeof v.channelId !== "string" || typeof v.chainId !== "string") return null;
|
|
1023
|
+
if (typeof v.createdAt !== "number" || typeof v.lastActivity !== "number") return null;
|
|
1024
|
+
const keyExchange = parsePersistedKex(v.keyExchange);
|
|
1025
|
+
const dappMetadata = parseDAppMetadata(v.dappMetadata);
|
|
1026
|
+
if (!keyExchange || !dappMetadata || !isStringArray(v.connectedAccounts)) return null;
|
|
1027
|
+
return {
|
|
1028
|
+
version: 3,
|
|
1029
|
+
channelId: v.channelId,
|
|
1030
|
+
keyExchange,
|
|
1031
|
+
dappMetadata,
|
|
1032
|
+
connectedAccounts: v.connectedAccounts,
|
|
1033
|
+
chainId: v.chainId,
|
|
1034
|
+
createdAt: v.createdAt,
|
|
1035
|
+
lastActivity: v.lastActivity
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
908
1038
|
var TERMINATE_SEND_TIMEOUT_MS = 800;
|
|
909
1039
|
var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
910
1040
|
constructor(options) {
|
|
@@ -913,14 +1043,16 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
913
1043
|
this.status = "disconnected" /* DISCONNECTED */;
|
|
914
1044
|
this.connectedAccounts = [];
|
|
915
1045
|
this.unresponsiveTimer = null;
|
|
1046
|
+
this.reconnectProbeTimer = null;
|
|
916
1047
|
this.failedReconnects = 0;
|
|
917
1048
|
this.walletPresent = false;
|
|
918
1049
|
this.pendingRestore = null;
|
|
919
1050
|
this.messageQueue = Promise.resolve();
|
|
1051
|
+
this.outboundQueue = Promise.resolve();
|
|
920
1052
|
this.dappMetadata = options.dappMetadata;
|
|
921
|
-
this.relayUrl = options.relayUrl
|
|
922
|
-
this.chainId = options.chainId
|
|
923
|
-
this.storageKey = options.storageKey
|
|
1053
|
+
this.relayUrl = options.relayUrl ?? DEFAULT_RELAY_URL;
|
|
1054
|
+
this.chainId = options.chainId ?? "0x0";
|
|
1055
|
+
this.storageKey = options.storageKey ?? `${STORAGE_KEY_PREFIX}:session`;
|
|
924
1056
|
const stored = this.readStoredSession();
|
|
925
1057
|
if (stored) {
|
|
926
1058
|
this.channelId = stored.channelId;
|
|
@@ -930,7 +1062,7 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
930
1062
|
this.pendingRestore = stored;
|
|
931
1063
|
log("ConnectionManager", `Found persisted session for channel ${this.channelId}`);
|
|
932
1064
|
} else {
|
|
933
|
-
this.channelId =
|
|
1065
|
+
this.channelId = randomUuid();
|
|
934
1066
|
}
|
|
935
1067
|
this.socketClient = new SocketClient(this.relayUrl, "dapp");
|
|
936
1068
|
this.setupSocketListeners();
|
|
@@ -942,13 +1074,23 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
942
1074
|
this.failedReconnects = 0;
|
|
943
1075
|
}
|
|
944
1076
|
});
|
|
945
|
-
this.socketClient.on("reconnected", () => {
|
|
1077
|
+
this.socketClient.on("reconnected", (result) => {
|
|
1078
|
+
if (result?.terminated) {
|
|
1079
|
+
log("ConnectionManager", "Channel terminated, observed on auto-reconnect");
|
|
1080
|
+
this.handleSessionTerminated();
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
if (result) {
|
|
1084
|
+
this.walletPresent = result.participants.includes("wallet");
|
|
1085
|
+
}
|
|
946
1086
|
if (this.keyExchange?.areKeysExchanged()) {
|
|
947
1087
|
if (this.walletPresent) {
|
|
1088
|
+
this.clearReconnectProbe();
|
|
948
1089
|
this.setStatus("connected" /* CONNECTED */);
|
|
949
1090
|
this.failedReconnects = 0;
|
|
950
1091
|
} else {
|
|
951
1092
|
this.setStatus("waiting" /* WAITING */);
|
|
1093
|
+
this.armReconnectProbe();
|
|
952
1094
|
}
|
|
953
1095
|
} else {
|
|
954
1096
|
this.setStatus("waiting" /* WAITING */);
|
|
@@ -975,11 +1117,17 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
975
1117
|
this.walletPresent = true;
|
|
976
1118
|
log("ConnectionManager", "Wallet joined channel");
|
|
977
1119
|
this.clearUnresponsiveTimer();
|
|
1120
|
+
this.clearReconnectProbe();
|
|
978
1121
|
if (this.keyExchange?.areKeysExchanged()) {
|
|
979
1122
|
this.failedReconnects = 0;
|
|
980
1123
|
this.setStatus("connected" /* CONNECTED */);
|
|
981
1124
|
}
|
|
982
1125
|
}
|
|
1126
|
+
if (data.event === "close") {
|
|
1127
|
+
log("ConnectionManager", "Wallet closed the channel (explicit terminate)");
|
|
1128
|
+
this.handleSessionTerminated();
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
983
1131
|
if (data.event === "disconnect" || data.event === "leave") {
|
|
984
1132
|
if (data.clientType === "wallet" || !data.clientType) {
|
|
985
1133
|
this.walletPresent = false;
|
|
@@ -995,6 +1143,7 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
995
1143
|
if (!this.keyExchange) return;
|
|
996
1144
|
this.keyExchange.on("keys_exchanged", () => {
|
|
997
1145
|
log("ConnectionManager", "Key exchange complete");
|
|
1146
|
+
this.clearReconnectProbe();
|
|
998
1147
|
this.setStatus("connected" /* CONNECTED */);
|
|
999
1148
|
this.walletPresent = true;
|
|
1000
1149
|
this.failedReconnects = 0;
|
|
@@ -1015,6 +1164,7 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1015
1164
|
*/
|
|
1016
1165
|
async getConnectionURI(retryOnConflict = true) {
|
|
1017
1166
|
this.setStatus("connecting" /* CONNECTING */);
|
|
1167
|
+
this.clearReconnectProbe();
|
|
1018
1168
|
this.pendingRestore = null;
|
|
1019
1169
|
this.walletPresent = false;
|
|
1020
1170
|
if (this.keyExchange) {
|
|
@@ -1024,7 +1174,7 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1024
1174
|
this.setupKeyExchangeListeners();
|
|
1025
1175
|
}
|
|
1026
1176
|
const pk = this.keyExchange.initiate();
|
|
1027
|
-
this.channelId =
|
|
1177
|
+
this.channelId = randomUuid();
|
|
1028
1178
|
this.socketClient.setPublicKey(toBase64(pk));
|
|
1029
1179
|
this.socketClient.connect();
|
|
1030
1180
|
try {
|
|
@@ -1035,7 +1185,7 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1035
1185
|
"ConnectionManager",
|
|
1036
1186
|
"Channel already has an active dApp participant. Rotating to a fresh channel."
|
|
1037
1187
|
);
|
|
1038
|
-
this.channelId =
|
|
1188
|
+
this.channelId = randomUuid();
|
|
1039
1189
|
return this.getConnectionURI(false);
|
|
1040
1190
|
}
|
|
1041
1191
|
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
@@ -1069,17 +1219,39 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1069
1219
|
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1070
1220
|
return false;
|
|
1071
1221
|
}
|
|
1222
|
+
return this.joinAndSettle();
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* (Re)open the socket, join the persisted channel, drain buffered relay
|
|
1226
|
+
* messages, and settle status. Shared by the cold-restore reconnect() and
|
|
1227
|
+
* the warm resume() path whose socket was torn down by the reconnect probe.
|
|
1228
|
+
* Assumes channelId and a hydrated keyExchange are already in place, and
|
|
1229
|
+
* that the caller has reset walletPresent + set RECONNECTING status.
|
|
1230
|
+
*/
|
|
1231
|
+
async joinAndSettle() {
|
|
1072
1232
|
this.socketClient.connect();
|
|
1073
1233
|
try {
|
|
1074
|
-
const { bufferedMessages } = await this.socketClient.joinChannel(
|
|
1234
|
+
const { bufferedMessages, participants, terminated } = await this.socketClient.joinChannel(
|
|
1235
|
+
this.channelId
|
|
1236
|
+
);
|
|
1237
|
+
if (terminated) {
|
|
1238
|
+
log("ConnectionManager", "Stored session was terminated by the wallet; dropping it");
|
|
1239
|
+
this.handleSessionTerminated();
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
if (participants.includes("wallet")) {
|
|
1243
|
+
this.walletPresent = true;
|
|
1244
|
+
}
|
|
1075
1245
|
for (const msg of bufferedMessages) {
|
|
1076
1246
|
this.enqueueRelayMessage(msg);
|
|
1077
1247
|
}
|
|
1078
1248
|
await this.messageQueue;
|
|
1079
|
-
if (this.keyExchange?.areKeysExchanged()) {
|
|
1080
|
-
this.
|
|
1249
|
+
if (this.keyExchange?.areKeysExchanged() && this.walletPresent) {
|
|
1250
|
+
this.clearReconnectProbe();
|
|
1251
|
+
this.setStatus("connected" /* CONNECTED */);
|
|
1081
1252
|
} else {
|
|
1082
1253
|
this.setStatus("waiting" /* WAITING */);
|
|
1254
|
+
this.armReconnectProbe();
|
|
1083
1255
|
}
|
|
1084
1256
|
return true;
|
|
1085
1257
|
} catch (err) {
|
|
@@ -1088,6 +1260,65 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1088
1260
|
return false;
|
|
1089
1261
|
}
|
|
1090
1262
|
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Nudge the connection back to life after the dApp tab / wallet app was
|
|
1265
|
+
* backgrounded. Idempotent and safe to call from visibilitychange / online
|
|
1266
|
+
* / pageshow handlers. If a restored session has not yet been hydrated,
|
|
1267
|
+
* runs the full reconnect(); otherwise just re-opens the socket, whose
|
|
1268
|
+
* own connect handler re-joins the channel and drains the relay buffer.
|
|
1269
|
+
*/
|
|
1270
|
+
resume() {
|
|
1271
|
+
if (this.status === "connected" /* CONNECTED */) return;
|
|
1272
|
+
if (this.pendingRestore && !this.keyExchange?.areKeysExchanged()) {
|
|
1273
|
+
void this.reconnect();
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
if (this.channelId && this.keyExchange?.areKeysExchanged()) {
|
|
1277
|
+
if (this.socketClient.getChannelId()) {
|
|
1278
|
+
this.socketClient.connect();
|
|
1279
|
+
} else {
|
|
1280
|
+
this.setStatus("reconnecting" /* RECONNECTING */);
|
|
1281
|
+
this.walletPresent = false;
|
|
1282
|
+
void this.joinAndSettle();
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* A session was terminated for good (wallet sent a relay 'close', or the
|
|
1288
|
+
* join ack reported a tombstone). Clear local state and surface
|
|
1289
|
+
* DISCONNECTED so the consumer drops to a fresh-pairing UI.
|
|
1290
|
+
*/
|
|
1291
|
+
handleSessionTerminated() {
|
|
1292
|
+
this.clearReconnectProbe();
|
|
1293
|
+
this.clearUnresponsiveTimer();
|
|
1294
|
+
this.walletPresent = false;
|
|
1295
|
+
this.pendingRestore = null;
|
|
1296
|
+
this.keyExchange = null;
|
|
1297
|
+
this.socketClient.leaveChannel();
|
|
1298
|
+
this.socketClient.disconnect();
|
|
1299
|
+
this.clearSession();
|
|
1300
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1301
|
+
}
|
|
1302
|
+
armReconnectProbe() {
|
|
1303
|
+
this.clearReconnectProbe();
|
|
1304
|
+
this.reconnectProbeTimer = setTimeout(() => {
|
|
1305
|
+
this.reconnectProbeTimer = null;
|
|
1306
|
+
if (this.walletPresent) return;
|
|
1307
|
+
warn(
|
|
1308
|
+
"ConnectionManager",
|
|
1309
|
+
`No wallet rejoined within ${RECONNECT_WALLET_PROBE_MS}ms; treating reconnect as dead`
|
|
1310
|
+
);
|
|
1311
|
+
this.socketClient.leaveChannel();
|
|
1312
|
+
this.socketClient.disconnect();
|
|
1313
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1314
|
+
}, RECONNECT_WALLET_PROBE_MS);
|
|
1315
|
+
}
|
|
1316
|
+
clearReconnectProbe() {
|
|
1317
|
+
if (this.reconnectProbeTimer) {
|
|
1318
|
+
clearTimeout(this.reconnectProbeTimer);
|
|
1319
|
+
this.reconnectProbeTimer = null;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1091
1322
|
/**
|
|
1092
1323
|
* Send a JSON-RPC request to the wallet (async, fire-and-forget).
|
|
1093
1324
|
*/
|
|
@@ -1124,8 +1355,8 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1124
1355
|
try {
|
|
1125
1356
|
const raw = localStorage.getItem(this.storageKey);
|
|
1126
1357
|
if (!raw) return false;
|
|
1127
|
-
const session =
|
|
1128
|
-
if (session
|
|
1358
|
+
const session = parseStoredSession(raw);
|
|
1359
|
+
if (!session) return false;
|
|
1129
1360
|
return Date.now() - session.createdAt <= SESSION_TTL_MS;
|
|
1130
1361
|
} catch {
|
|
1131
1362
|
return false;
|
|
@@ -1140,8 +1371,7 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1140
1371
|
*/
|
|
1141
1372
|
async flushTerminate() {
|
|
1142
1373
|
if (!this.keyExchange?.areKeysExchanged()) return;
|
|
1143
|
-
const send = this.sendEncrypted({ type: "terminate" /* TERMINATE */ }).catch(() =>
|
|
1144
|
-
});
|
|
1374
|
+
const send = this.sendEncrypted({ type: "terminate" /* TERMINATE */ }).catch(() => void 0);
|
|
1145
1375
|
const timeout = new Promise((resolve) => setTimeout(resolve, TERMINATE_SEND_TIMEOUT_MS));
|
|
1146
1376
|
await Promise.race([send, timeout]);
|
|
1147
1377
|
}
|
|
@@ -1152,6 +1382,7 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1152
1382
|
*/
|
|
1153
1383
|
async resetForNewChannel() {
|
|
1154
1384
|
this.clearUnresponsiveTimer();
|
|
1385
|
+
this.clearReconnectProbe();
|
|
1155
1386
|
this.walletPresent = false;
|
|
1156
1387
|
await this.flushTerminate();
|
|
1157
1388
|
this.socketClient.leaveChannel();
|
|
@@ -1159,7 +1390,7 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1159
1390
|
this.clearSession();
|
|
1160
1391
|
this.connectedAccounts = [];
|
|
1161
1392
|
this.pendingRestore = null;
|
|
1162
|
-
this.channelId =
|
|
1393
|
+
this.channelId = randomUuid();
|
|
1163
1394
|
this.keyExchange = null;
|
|
1164
1395
|
this.socketClient = new SocketClient(this.relayUrl, "dapp");
|
|
1165
1396
|
this.setupSocketListeners();
|
|
@@ -1167,6 +1398,7 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1167
1398
|
}
|
|
1168
1399
|
async disconnect() {
|
|
1169
1400
|
this.clearUnresponsiveTimer();
|
|
1401
|
+
this.clearReconnectProbe();
|
|
1170
1402
|
this.walletPresent = false;
|
|
1171
1403
|
await this.flushTerminate();
|
|
1172
1404
|
this.socketClient.leaveChannel();
|
|
@@ -1174,36 +1406,58 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1174
1406
|
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1175
1407
|
this.clearSession();
|
|
1176
1408
|
this.connectedAccounts = [];
|
|
1409
|
+
this.pendingRestore = null;
|
|
1410
|
+
this.keyExchange = null;
|
|
1177
1411
|
}
|
|
1178
1412
|
// ── Internals ──────────────────────────────────────────────
|
|
1179
1413
|
enqueueRelayMessage(data) {
|
|
1180
|
-
|
|
1414
|
+
if (!isRelayMessage(data)) {
|
|
1415
|
+
warn("ConnectionManager", "Dropping malformed relay envelope");
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
this.messageQueue = this.messageQueue.then(() => this.handleRelayMessage(data)).catch((err) => {
|
|
1419
|
+
error("ConnectionManager", "messageQueue handler error:", err);
|
|
1420
|
+
});
|
|
1181
1421
|
}
|
|
1182
1422
|
async handleRelayMessage(data) {
|
|
1183
1423
|
if (data.clientType === "wallet") {
|
|
1184
1424
|
this.walletPresent = true;
|
|
1425
|
+
this.clearReconnectProbe();
|
|
1185
1426
|
if (this.keyExchange?.areKeysExchanged() && this.status !== "connected" /* CONNECTED */) {
|
|
1186
1427
|
this.failedReconnects = 0;
|
|
1187
1428
|
this.setStatus("connected" /* CONNECTED */);
|
|
1188
1429
|
}
|
|
1189
1430
|
}
|
|
1190
1431
|
const message = data.message;
|
|
1191
|
-
if (
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1432
|
+
if (isRecord(message)) {
|
|
1433
|
+
if (message.type === "key_handshake_SYNACK" /* SYNACK */) {
|
|
1434
|
+
if (typeof message.ct === "string" && typeof message.c0 === "string") {
|
|
1435
|
+
await this.handleSynAck({
|
|
1436
|
+
type: "key_handshake_SYNACK" /* SYNACK */,
|
|
1437
|
+
ct: message.ct,
|
|
1438
|
+
c0: message.c0,
|
|
1439
|
+
v: typeof message.v === "number" ? message.v : 0
|
|
1440
|
+
});
|
|
1441
|
+
} else {
|
|
1442
|
+
warn("ConnectionManager", "Dropping malformed SYNACK");
|
|
1443
|
+
}
|
|
1195
1444
|
return;
|
|
1196
1445
|
}
|
|
1197
|
-
if (
|
|
1198
|
-
warn("ConnectionManager", `Unexpected ${
|
|
1446
|
+
if (message.type === "key_handshake_SYN" /* SYN */ || message.type === "key_handshake_ACK" /* ACK */) {
|
|
1447
|
+
warn("ConnectionManager", `Unexpected ${message.type} on dApp side, ignoring`);
|
|
1199
1448
|
return;
|
|
1200
1449
|
}
|
|
1201
1450
|
}
|
|
1202
1451
|
if (typeof message === "string" && this.keyExchange?.areKeysExchanged()) {
|
|
1203
1452
|
try {
|
|
1204
1453
|
const decrypted = await this.keyExchange.decryptMessage(message);
|
|
1454
|
+
await this.persistSession();
|
|
1205
1455
|
const parsed = JSON.parse(decrypted);
|
|
1206
|
-
|
|
1456
|
+
if (isRecord(parsed)) {
|
|
1457
|
+
this.handleDecryptedMessage(parsed);
|
|
1458
|
+
} else {
|
|
1459
|
+
warn("ConnectionManager", "Dropping non-object decrypted payload");
|
|
1460
|
+
}
|
|
1207
1461
|
} catch (err) {
|
|
1208
1462
|
error("ConnectionManager", "Failed to decrypt message:", err);
|
|
1209
1463
|
}
|
|
@@ -1223,16 +1477,21 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1223
1477
|
}
|
|
1224
1478
|
if (response) {
|
|
1225
1479
|
this.sendPlaintext(response);
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const cachedAck = this.keyExchange.getLastAck();
|
|
1483
|
+
if (this.keyExchange.areKeysExchanged() && cachedAck) {
|
|
1484
|
+
log("ConnectionManager", "Duplicate SYNACK after handshake; re-sending cached ACK");
|
|
1485
|
+
this.sendPlaintext(cachedAck);
|
|
1226
1486
|
}
|
|
1227
1487
|
}
|
|
1228
1488
|
handleDecryptedMessage(msg) {
|
|
1229
1489
|
this.clearUnresponsiveTimer();
|
|
1230
|
-
const type = msg.type;
|
|
1231
|
-
switch (type) {
|
|
1490
|
+
const type = typeof msg.type === "string" ? msg.type : "";
|
|
1491
|
+
switch (MESSAGE_TYPE_BY_VALUE[type]) {
|
|
1232
1492
|
case "wallet_info" /* WALLET_INFO */: {
|
|
1233
|
-
const
|
|
1234
|
-
const
|
|
1235
|
-
const nextChainId = info.chainId || this.chainId;
|
|
1493
|
+
const nextAccounts = isStringArray(msg.accounts) ? msg.accounts : [];
|
|
1494
|
+
const nextChainId = typeof msg.chainId === "string" && msg.chainId ? msg.chainId : this.chainId;
|
|
1236
1495
|
const accountsChanged = !this.areArraysEqual(this.connectedAccounts, nextAccounts);
|
|
1237
1496
|
const chainChanged = this.chainId !== nextChainId;
|
|
1238
1497
|
this.connectedAccounts = nextAccounts;
|
|
@@ -1247,7 +1506,11 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1247
1506
|
break;
|
|
1248
1507
|
}
|
|
1249
1508
|
case "jsonrpc" /* JSONRPC */: {
|
|
1250
|
-
const response = msg;
|
|
1509
|
+
const response = parseJsonRpcResponse(msg);
|
|
1510
|
+
if (!response) {
|
|
1511
|
+
warn("ConnectionManager", "Dropping malformed JSON-RPC response");
|
|
1512
|
+
break;
|
|
1513
|
+
}
|
|
1251
1514
|
this.emit("jsonrpc_response", response);
|
|
1252
1515
|
break;
|
|
1253
1516
|
}
|
|
@@ -1269,11 +1532,28 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1269
1532
|
error("ConnectionManager", "Failed to send plaintext:", err);
|
|
1270
1533
|
});
|
|
1271
1534
|
}
|
|
1272
|
-
|
|
1535
|
+
/**
|
|
1536
|
+
* Serialize every encrypt+persist+send on a single outbound queue.
|
|
1537
|
+
* Callers like sendJsonRpc() are fire-and-forget, so without the queue two
|
|
1538
|
+
* rapid provider.request() calls would interleave inside encryptMessage()
|
|
1539
|
+
* across its await and could complete out of order; the receiver's
|
|
1540
|
+
* contiguous-seq check would then drop the late one. (Nonce reuse itself is
|
|
1541
|
+
* already prevented by the synchronous seq reservation in KeyExchange.)
|
|
1542
|
+
*/
|
|
1543
|
+
sendEncrypted(message) {
|
|
1544
|
+
const task = this.outboundQueue.then(() => this.sendEncryptedNow(message));
|
|
1545
|
+
this.outboundQueue = task.then(
|
|
1546
|
+
() => void 0,
|
|
1547
|
+
() => void 0
|
|
1548
|
+
);
|
|
1549
|
+
return task;
|
|
1550
|
+
}
|
|
1551
|
+
async sendEncryptedNow(message) {
|
|
1273
1552
|
if (!this.keyExchange?.areKeysExchanged()) {
|
|
1274
1553
|
throw new Error("sendEncrypted: not connected");
|
|
1275
1554
|
}
|
|
1276
1555
|
const encrypted = await this.keyExchange.encryptMessage(JSON.stringify(message));
|
|
1556
|
+
await this.persistSession();
|
|
1277
1557
|
await this.socketClient.sendMessage({
|
|
1278
1558
|
id: this.channelId,
|
|
1279
1559
|
clientType: "dapp",
|
|
@@ -1314,7 +1594,7 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1314
1594
|
const persistedKex = await this.keyExchange.exportPersisted();
|
|
1315
1595
|
if (!persistedKex) return;
|
|
1316
1596
|
const session = {
|
|
1317
|
-
version:
|
|
1597
|
+
version: 3,
|
|
1318
1598
|
channelId: this.channelId,
|
|
1319
1599
|
keyExchange: persistedKex,
|
|
1320
1600
|
dappMetadata: this.dappMetadata,
|
|
@@ -1334,13 +1614,13 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1334
1614
|
try {
|
|
1335
1615
|
const raw = localStorage.getItem(this.storageKey);
|
|
1336
1616
|
if (!raw) return null;
|
|
1337
|
-
const session =
|
|
1338
|
-
if (session
|
|
1339
|
-
log("ConnectionManager", "Dropping legacy
|
|
1617
|
+
const session = parseStoredSession(raw);
|
|
1618
|
+
if (!session) {
|
|
1619
|
+
log("ConnectionManager", "Dropping legacy or malformed session from storage");
|
|
1340
1620
|
localStorage.removeItem(this.storageKey);
|
|
1341
1621
|
return null;
|
|
1342
1622
|
}
|
|
1343
|
-
if (
|
|
1623
|
+
if (Date.now() - session.createdAt > SESSION_TTL_MS) {
|
|
1344
1624
|
localStorage.removeItem(this.storageKey);
|
|
1345
1625
|
return null;
|
|
1346
1626
|
}
|
|
@@ -1370,11 +1650,27 @@ function getAppStoreUrl() {
|
|
|
1370
1650
|
}
|
|
1371
1651
|
|
|
1372
1652
|
// src/QRLConnectProvider.ts
|
|
1373
|
-
var
|
|
1653
|
+
var QRL_CONNECT_PROVIDER_INFO = {
|
|
1654
|
+
name: "MyQRLWallet (Mobile)",
|
|
1655
|
+
rdns: "com.qrlwallet.connect",
|
|
1656
|
+
icon: "data:image/png;base64,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"
|
|
1657
|
+
};
|
|
1658
|
+
var EIP6963_ANNOUNCE_EVENT = "eip6963:announceProvider";
|
|
1659
|
+
var EIP6963_REQUEST_EVENT = "eip6963:requestProvider";
|
|
1374
1660
|
var QRLConnectProvider = class extends EventEmitter4 {
|
|
1375
1661
|
constructor(options) {
|
|
1376
1662
|
super();
|
|
1377
1663
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
1664
|
+
this.eip6963Detail = null;
|
|
1665
|
+
this.eip6963RequestListener = null;
|
|
1666
|
+
this.resumeListener = null;
|
|
1667
|
+
this.resumeDebounce = null;
|
|
1668
|
+
// Random per-instance prefix keeps request ids unique across page loads.
|
|
1669
|
+
// A bare counter restarts at 1 on reload, and the relay buffers messages
|
|
1670
|
+
// for 5 minutes, so a stale buffered response could otherwise be matched
|
|
1671
|
+
// to a fresh request that drew the same small id.
|
|
1672
|
+
this.requestIdPrefix = randomUuid().slice(0, 8);
|
|
1673
|
+
this.requestCounter = 0;
|
|
1378
1674
|
this.isQRLConnect = true;
|
|
1379
1675
|
this.options = options;
|
|
1380
1676
|
if (options.debug) {
|
|
@@ -1388,8 +1684,83 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1388
1684
|
});
|
|
1389
1685
|
this.setupConnectionListeners();
|
|
1390
1686
|
if (options.autoReconnect !== false) {
|
|
1391
|
-
this.connectionManager.reconnect();
|
|
1687
|
+
void this.connectionManager.reconnect();
|
|
1688
|
+
}
|
|
1689
|
+
this.setupResumeListeners();
|
|
1690
|
+
if (options.announceProvider !== false) {
|
|
1691
|
+
this.startEip6963Announce();
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
startEip6963Announce() {
|
|
1695
|
+
if (typeof window === "undefined" || typeof CustomEvent === "undefined") {
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
const overrides = this.options.providerInfo ?? {};
|
|
1699
|
+
this.eip6963Detail = Object.freeze({
|
|
1700
|
+
info: Object.freeze({
|
|
1701
|
+
uuid: randomUuid(),
|
|
1702
|
+
name: overrides.name ?? QRL_CONNECT_PROVIDER_INFO.name,
|
|
1703
|
+
icon: overrides.icon ?? QRL_CONNECT_PROVIDER_INFO.icon,
|
|
1704
|
+
rdns: overrides.rdns ?? QRL_CONNECT_PROVIDER_INFO.rdns
|
|
1705
|
+
}),
|
|
1706
|
+
provider: this
|
|
1707
|
+
});
|
|
1708
|
+
const announce = () => {
|
|
1709
|
+
if (!this.eip6963Detail) return;
|
|
1710
|
+
window.dispatchEvent(new CustomEvent(EIP6963_ANNOUNCE_EVENT, { detail: this.eip6963Detail }));
|
|
1711
|
+
};
|
|
1712
|
+
this.eip6963RequestListener = announce;
|
|
1713
|
+
window.addEventListener(EIP6963_REQUEST_EVENT, announce);
|
|
1714
|
+
announce();
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Re-open the relay socket when the dApp tab regains focus or the network
|
|
1718
|
+
* comes back. Debounced because visibilitychange + online can fire together.
|
|
1719
|
+
* No-op when already connected (ConnectionManager.resume guards that).
|
|
1720
|
+
*/
|
|
1721
|
+
setupResumeListeners() {
|
|
1722
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
1723
|
+
if (this.resumeListener) return;
|
|
1724
|
+
const resume = () => {
|
|
1725
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
1726
|
+
if (this.resumeDebounce) clearTimeout(this.resumeDebounce);
|
|
1727
|
+
this.resumeDebounce = setTimeout(() => {
|
|
1728
|
+
this.resumeDebounce = null;
|
|
1729
|
+
this.connectionManager.resume();
|
|
1730
|
+
}, 300);
|
|
1731
|
+
};
|
|
1732
|
+
this.resumeListener = resume;
|
|
1733
|
+
document.addEventListener("visibilitychange", resume);
|
|
1734
|
+
window.addEventListener("online", resume);
|
|
1735
|
+
window.addEventListener("pageshow", resume);
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Remove the foreground/online resume listeners. Called on disconnect so a
|
|
1739
|
+
* torn-down session can't be silently revived by a later tab focus; the
|
|
1740
|
+
* listeners are re-armed by getConnectionURI()/newConnection() if the same
|
|
1741
|
+
* provider is re-paired. Safe in any env.
|
|
1742
|
+
*/
|
|
1743
|
+
teardownResumeListeners() {
|
|
1744
|
+
if (typeof window !== "undefined" && typeof document !== "undefined" && this.resumeListener) {
|
|
1745
|
+
document.removeEventListener("visibilitychange", this.resumeListener);
|
|
1746
|
+
window.removeEventListener("online", this.resumeListener);
|
|
1747
|
+
window.removeEventListener("pageshow", this.resumeListener);
|
|
1748
|
+
}
|
|
1749
|
+
this.resumeListener = null;
|
|
1750
|
+
if (this.resumeDebounce) {
|
|
1751
|
+
clearTimeout(this.resumeDebounce);
|
|
1752
|
+
this.resumeDebounce = null;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Stop announcing this provider over EIP-6963. Safe to call from any env.
|
|
1757
|
+
*/
|
|
1758
|
+
stopEip6963Announce() {
|
|
1759
|
+
if (typeof window !== "undefined" && this.eip6963RequestListener) {
|
|
1760
|
+
window.removeEventListener(EIP6963_REQUEST_EVENT, this.eip6963RequestListener);
|
|
1392
1761
|
}
|
|
1762
|
+
this.eip6963RequestListener = null;
|
|
1763
|
+
this.eip6963Detail = null;
|
|
1393
1764
|
}
|
|
1394
1765
|
setupConnectionListeners() {
|
|
1395
1766
|
this.connectionManager.on("status_changed", (status) => {
|
|
@@ -1447,6 +1818,7 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1447
1818
|
* Generate a connection URI for QR code display or deep link redirect.
|
|
1448
1819
|
*/
|
|
1449
1820
|
async getConnectionURI() {
|
|
1821
|
+
this.setupResumeListeners();
|
|
1450
1822
|
return this.connectionManager.getConnectionURI();
|
|
1451
1823
|
}
|
|
1452
1824
|
/**
|
|
@@ -1479,7 +1851,7 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1479
1851
|
if (this.connectionManager.getStatus() !== "connected" /* CONNECTED */) {
|
|
1480
1852
|
throw new Error("Not connected to QRL Wallet");
|
|
1481
1853
|
}
|
|
1482
|
-
const id = ++requestCounter
|
|
1854
|
+
const id = `${this.requestIdPrefix}-${++this.requestCounter}`;
|
|
1483
1855
|
return new Promise((resolve, reject) => {
|
|
1484
1856
|
const pending = {
|
|
1485
1857
|
id,
|
|
@@ -1560,7 +1932,7 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1560
1932
|
/**
|
|
1561
1933
|
* Disconnect from wallet and clean up. Returns once the TERMINATE has
|
|
1562
1934
|
* either been flushed to the relay or the 800ms best-effort window has
|
|
1563
|
-
* elapsed
|
|
1935
|
+
* elapsed - the wallet gets an instant disconnect instead of landing in
|
|
1564
1936
|
* its stale-session grace period.
|
|
1565
1937
|
*/
|
|
1566
1938
|
async disconnect() {
|
|
@@ -1568,15 +1940,380 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1568
1940
|
pending.reject(new Error("Disconnected"));
|
|
1569
1941
|
}
|
|
1570
1942
|
this.pendingRequests.clear();
|
|
1943
|
+
this.teardownResumeListeners();
|
|
1571
1944
|
await this.connectionManager.disconnect();
|
|
1572
1945
|
}
|
|
1573
1946
|
};
|
|
1947
|
+
|
|
1948
|
+
// src/signing/ctx.ts
|
|
1949
|
+
var SCHEME_VERSION_MSG = "QRL-SIGN-MSG-v1";
|
|
1950
|
+
var SCHEME_VERSION_TYPED = "QRL-SIGN-TYPED-v1";
|
|
1951
|
+
var SCHEME_TAG_MSG = new TextEncoder().encode(SCHEME_VERSION_MSG);
|
|
1952
|
+
var SCHEME_TAG_TYPED = new TextEncoder().encode(SCHEME_VERSION_TYPED);
|
|
1953
|
+
var DIGEST_LEN = 64;
|
|
1954
|
+
|
|
1955
|
+
// src/signing/bytes.ts
|
|
1956
|
+
function hexToBytes(hex) {
|
|
1957
|
+
if (typeof hex !== "string") throw new Error("hex must be a string");
|
|
1958
|
+
if (!/^0x([0-9a-fA-F]{2})*$/.test(hex)) {
|
|
1959
|
+
throw new Error("hex must be 0x-prefixed even-length");
|
|
1960
|
+
}
|
|
1961
|
+
const n = (hex.length - 2) / 2;
|
|
1962
|
+
const out = new Uint8Array(n);
|
|
1963
|
+
for (let i = 0; i < n; i++) {
|
|
1964
|
+
out[i] = parseInt(hex.slice(2 + i * 2, 4 + i * 2), 16);
|
|
1965
|
+
}
|
|
1966
|
+
return out;
|
|
1967
|
+
}
|
|
1968
|
+
function bytesToHex(bytes) {
|
|
1969
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
1970
|
+
throw new Error("bytes must be a Uint8Array");
|
|
1971
|
+
}
|
|
1972
|
+
let s = "0x";
|
|
1973
|
+
for (const v of bytes) s += v.toString(16).padStart(2, "0");
|
|
1974
|
+
return s;
|
|
1975
|
+
}
|
|
1976
|
+
function concatBytes(...parts) {
|
|
1977
|
+
return concatBytesArr(parts);
|
|
1978
|
+
}
|
|
1979
|
+
function concatBytesArr(parts) {
|
|
1980
|
+
let total = 0;
|
|
1981
|
+
for (const p of parts) total += p.length;
|
|
1982
|
+
const out = new Uint8Array(total);
|
|
1983
|
+
let off = 0;
|
|
1984
|
+
for (const p of parts) {
|
|
1985
|
+
out.set(p, off);
|
|
1986
|
+
off += p.length;
|
|
1987
|
+
}
|
|
1988
|
+
return out;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// src/signing/messageDigest.ts
|
|
1992
|
+
function computeMessageDigest(messageBytes) {
|
|
1993
|
+
if (!(messageBytes instanceof Uint8Array)) {
|
|
1994
|
+
throw new Error("messageBytes must be a Uint8Array");
|
|
1995
|
+
}
|
|
1996
|
+
return shake256Digest(concatBytes(SCHEME_TAG_MSG, messageBytes), DIGEST_LEN);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// src/signing/typedData.ts
|
|
2000
|
+
var SLOT = 32;
|
|
2001
|
+
function isMessageObject(v) {
|
|
2002
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2003
|
+
}
|
|
2004
|
+
var ATOMIC_RE = /^(?:(address|bool|string|bytes)|(u?int)(\d+)|bytes(\d+)|(.+?)\[(\d*)\])$/;
|
|
2005
|
+
var MAX_TYPE_DEPTH = 12;
|
|
2006
|
+
function parseFieldType(type, types, depth = 0) {
|
|
2007
|
+
if (depth > MAX_TYPE_DEPTH) throw new Error(`type nesting too deep: ${type}`);
|
|
2008
|
+
if (Object.prototype.hasOwnProperty.call(types, type)) {
|
|
2009
|
+
return { kind: "ref", name: type };
|
|
2010
|
+
}
|
|
2011
|
+
const m = ATOMIC_RE.exec(type);
|
|
2012
|
+
if (!m) throw new Error(`unknown type: ${type}`);
|
|
2013
|
+
const [, atomic, intKind, intWidthStr, bytesWidthStr, innerType, sizeStr] = m;
|
|
2014
|
+
if (atomic === "address" || atomic === "bool" || atomic === "string" || atomic === "bytes") {
|
|
2015
|
+
return { kind: atomic };
|
|
2016
|
+
}
|
|
2017
|
+
if (intKind) {
|
|
2018
|
+
const width = Number(intWidthStr);
|
|
2019
|
+
if (!Number.isInteger(width) || width < 8 || width > 256 || width % 8 !== 0) {
|
|
2020
|
+
throw new Error(`invalid int width: ${type}`);
|
|
2021
|
+
}
|
|
2022
|
+
return { kind: intKind === "uint" ? "uintN" : "intN", width };
|
|
2023
|
+
}
|
|
2024
|
+
if (bytesWidthStr) {
|
|
2025
|
+
const width = Number(bytesWidthStr);
|
|
2026
|
+
if (!Number.isInteger(width) || width < 1 || width > 32) {
|
|
2027
|
+
throw new Error(`invalid bytesN width: ${type}`);
|
|
2028
|
+
}
|
|
2029
|
+
return { kind: "bytesN", width };
|
|
2030
|
+
}
|
|
2031
|
+
if (innerType !== void 0) {
|
|
2032
|
+
parseFieldType(innerType, types, depth + 1);
|
|
2033
|
+
if (sizeStr) {
|
|
2034
|
+
const size = Number(sizeStr);
|
|
2035
|
+
if (!Number.isInteger(size) || size <= 0) {
|
|
2036
|
+
throw new Error(`invalid array size: ${type}`);
|
|
2037
|
+
}
|
|
2038
|
+
return { kind: "array", inner: innerType, size };
|
|
2039
|
+
}
|
|
2040
|
+
return { kind: "array", inner: innerType };
|
|
2041
|
+
}
|
|
2042
|
+
throw new Error(`unhandled type: ${type}`);
|
|
2043
|
+
}
|
|
2044
|
+
function baseTypeName(type) {
|
|
2045
|
+
return type.replace(/(\[\d*\])+$/, "");
|
|
2046
|
+
}
|
|
2047
|
+
function collectDependencies(primary, types) {
|
|
2048
|
+
if (!Object.prototype.hasOwnProperty.call(types, primary)) {
|
|
2049
|
+
throw new Error(`unknown primary type: ${primary}`);
|
|
2050
|
+
}
|
|
2051
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2052
|
+
const visit = (name, path) => {
|
|
2053
|
+
if (path.includes(name)) {
|
|
2054
|
+
throw new Error(`cyclic type reference: ${[...path, name].join(" -> ")}`);
|
|
2055
|
+
}
|
|
2056
|
+
if (visited.has(name)) return;
|
|
2057
|
+
visited.add(name);
|
|
2058
|
+
const def = types[name];
|
|
2059
|
+
if (!def) throw new Error(`unknown type: ${name}`);
|
|
2060
|
+
for (const f of def) {
|
|
2061
|
+
const base = baseTypeName(f.type);
|
|
2062
|
+
if (Object.prototype.hasOwnProperty.call(types, base)) {
|
|
2063
|
+
visit(base, [...path, name]);
|
|
2064
|
+
}
|
|
2065
|
+
parseFieldType(f.type, types);
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
visit(primary, []);
|
|
2069
|
+
return visited;
|
|
2070
|
+
}
|
|
2071
|
+
function validateTypeMap(types) {
|
|
2072
|
+
for (const [name, def] of Object.entries(types)) {
|
|
2073
|
+
const defUnknown = def;
|
|
2074
|
+
if (!Array.isArray(defUnknown) || def.length === 0) {
|
|
2075
|
+
throw new Error(`empty or invalid struct: ${name}`);
|
|
2076
|
+
}
|
|
2077
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2078
|
+
for (const f of def) {
|
|
2079
|
+
if (!f || typeof f.name !== "string" || !f.name) {
|
|
2080
|
+
throw new Error(`bad field in ${name}`);
|
|
2081
|
+
}
|
|
2082
|
+
if (typeof f.type !== "string" || !f.type) {
|
|
2083
|
+
throw new Error(`bad field type in ${name}.${f.name}`);
|
|
2084
|
+
}
|
|
2085
|
+
if (seen.has(f.name)) {
|
|
2086
|
+
throw new Error(`duplicate field "${f.name}" in ${name}`);
|
|
2087
|
+
}
|
|
2088
|
+
seen.add(f.name);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
function encodeType(primary, types) {
|
|
2093
|
+
const deps = collectDependencies(primary, types);
|
|
2094
|
+
const others = [...deps].filter((n) => n !== primary).sort();
|
|
2095
|
+
return [primary, ...others].map((name) => {
|
|
2096
|
+
const fields = types[name];
|
|
2097
|
+
if (!fields) throw new Error(`unknown type: ${name}`);
|
|
2098
|
+
const inner = fields.map((f) => `${f.type} ${f.name}`).join(",");
|
|
2099
|
+
return `${name}(${inner})`;
|
|
2100
|
+
}).join("");
|
|
2101
|
+
}
|
|
2102
|
+
function typeHash(primary, types) {
|
|
2103
|
+
return shake256Digest(new TextEncoder().encode(encodeType(primary, types)), DIGEST_LEN);
|
|
2104
|
+
}
|
|
2105
|
+
function parseQAddress(addr) {
|
|
2106
|
+
if (typeof addr !== "string" || !/^Q[0-9a-fA-F]{40}$/.test(addr)) {
|
|
2107
|
+
throw new Error(`invalid Q-address: ${addr}`);
|
|
2108
|
+
}
|
|
2109
|
+
return hexToBytes("0x" + addr.slice(1).toLowerCase());
|
|
2110
|
+
}
|
|
2111
|
+
function padLeft32(bytes) {
|
|
2112
|
+
if (bytes.length > SLOT) throw new Error("cannot pad: bytes > 32");
|
|
2113
|
+
const out = new Uint8Array(SLOT);
|
|
2114
|
+
out.set(bytes, SLOT - bytes.length);
|
|
2115
|
+
return out;
|
|
2116
|
+
}
|
|
2117
|
+
function padRight32(bytes) {
|
|
2118
|
+
if (bytes.length > SLOT) throw new Error("cannot pad: bytes > 32");
|
|
2119
|
+
const out = new Uint8Array(SLOT);
|
|
2120
|
+
out.set(bytes, 0);
|
|
2121
|
+
return out;
|
|
2122
|
+
}
|
|
2123
|
+
function bigIntToSlot(value, width, signed) {
|
|
2124
|
+
if (signed) {
|
|
2125
|
+
const limit = 1n << BigInt(width - 1);
|
|
2126
|
+
if (value >= limit || value < -limit) {
|
|
2127
|
+
throw new Error(`int${width} out of range: ${value}`);
|
|
2128
|
+
}
|
|
2129
|
+
} else {
|
|
2130
|
+
if (value < 0n) throw new Error(`uint${width} negative: ${value}`);
|
|
2131
|
+
const limit = 1n << BigInt(width);
|
|
2132
|
+
if (value >= limit) throw new Error(`uint${width} out of range: ${value}`);
|
|
2133
|
+
}
|
|
2134
|
+
const slotMask = (1n << 256n) - 1n;
|
|
2135
|
+
const repr = value < 0n ? value + (1n << 256n) & slotMask : value;
|
|
2136
|
+
const out = new Uint8Array(SLOT);
|
|
2137
|
+
let v = repr;
|
|
2138
|
+
for (let i = SLOT - 1; i >= 0; i--) {
|
|
2139
|
+
out[i] = Number(v & 0xffn);
|
|
2140
|
+
v >>= 8n;
|
|
2141
|
+
}
|
|
2142
|
+
return out;
|
|
2143
|
+
}
|
|
2144
|
+
function parseIntValue(v, typeLabel) {
|
|
2145
|
+
if (typeof v === "bigint") return v;
|
|
2146
|
+
if (typeof v === "string") {
|
|
2147
|
+
if (/^-?0x[0-9a-fA-F]+$/i.test(v)) {
|
|
2148
|
+
const isNegative = v.startsWith("-");
|
|
2149
|
+
const abs = BigInt(isNegative ? v.slice(1) : v);
|
|
2150
|
+
return isNegative ? -abs : abs;
|
|
2151
|
+
}
|
|
2152
|
+
if (!/^-?(0|[1-9]\d*)$/.test(v)) {
|
|
2153
|
+
throw new Error(`invalid ${typeLabel} string: ${v}`);
|
|
2154
|
+
}
|
|
2155
|
+
return BigInt(v);
|
|
2156
|
+
}
|
|
2157
|
+
if (typeof v === "number") {
|
|
2158
|
+
if (!Number.isInteger(v) || !Number.isSafeInteger(v)) {
|
|
2159
|
+
throw new Error(`unsafe ${typeLabel} number: ${v}, pass as string`);
|
|
2160
|
+
}
|
|
2161
|
+
return BigInt(v);
|
|
2162
|
+
}
|
|
2163
|
+
throw new Error(`unsupported ${typeLabel} value: ${typeof v}`);
|
|
2164
|
+
}
|
|
2165
|
+
function encodeField(type, value, types) {
|
|
2166
|
+
const parsed = parseFieldType(type, types);
|
|
2167
|
+
switch (parsed.kind) {
|
|
2168
|
+
case "ref":
|
|
2169
|
+
if (!isMessageObject(value)) {
|
|
2170
|
+
throw new Error(`struct field expects object: ${type}`);
|
|
2171
|
+
}
|
|
2172
|
+
return hashStruct(parsed.name, value, types);
|
|
2173
|
+
case "address":
|
|
2174
|
+
if (typeof value !== "string") {
|
|
2175
|
+
throw new Error(`address field expects string: ${typeof value}`);
|
|
2176
|
+
}
|
|
2177
|
+
return padLeft32(parseQAddress(value));
|
|
2178
|
+
case "bool":
|
|
2179
|
+
if (typeof value !== "boolean") {
|
|
2180
|
+
throw new Error(`bool field expects boolean: ${typeof value}`);
|
|
2181
|
+
}
|
|
2182
|
+
return padLeft32(new Uint8Array([value ? 1 : 0]));
|
|
2183
|
+
case "string":
|
|
2184
|
+
if (typeof value !== "string") {
|
|
2185
|
+
throw new Error(`string field expects string: ${typeof value}`);
|
|
2186
|
+
}
|
|
2187
|
+
return shake256Digest(new TextEncoder().encode(value), DIGEST_LEN);
|
|
2188
|
+
case "bytes":
|
|
2189
|
+
if (typeof value !== "string") {
|
|
2190
|
+
throw new Error(`bytes field expects 0x-hex string: ${typeof value}`);
|
|
2191
|
+
}
|
|
2192
|
+
return shake256Digest(hexToBytes(value), DIGEST_LEN);
|
|
2193
|
+
case "uintN":
|
|
2194
|
+
return bigIntToSlot(parseIntValue(value, `uint${parsed.width}`), parsed.width, false);
|
|
2195
|
+
case "intN":
|
|
2196
|
+
return bigIntToSlot(parseIntValue(value, `int${parsed.width}`), parsed.width, true);
|
|
2197
|
+
case "bytesN": {
|
|
2198
|
+
if (typeof value !== "string") {
|
|
2199
|
+
throw new Error(`bytes${parsed.width} expects 0x-hex string: ${typeof value}`);
|
|
2200
|
+
}
|
|
2201
|
+
const raw = hexToBytes(value);
|
|
2202
|
+
if (raw.length !== parsed.width) {
|
|
2203
|
+
throw new Error(`bytes${parsed.width} requires ${parsed.width} bytes, got ${raw.length}`);
|
|
2204
|
+
}
|
|
2205
|
+
return padRight32(raw);
|
|
2206
|
+
}
|
|
2207
|
+
case "array": {
|
|
2208
|
+
if (!Array.isArray(value)) {
|
|
2209
|
+
throw new Error(`array field expects array: ${typeof value}`);
|
|
2210
|
+
}
|
|
2211
|
+
if (parsed.size !== void 0 && value.length !== parsed.size) {
|
|
2212
|
+
throw new Error(`fixed array ${type} requires length ${parsed.size}, got ${value.length}`);
|
|
2213
|
+
}
|
|
2214
|
+
const chunks = value.map((v) => encodeField(parsed.inner, v, types));
|
|
2215
|
+
return shake256Digest(concatBytesArr(chunks), DIGEST_LEN);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
function hashStruct(primary, data, types) {
|
|
2220
|
+
const fields = types[primary];
|
|
2221
|
+
if (!fields) throw new Error(`unknown struct: ${primary}`);
|
|
2222
|
+
const expected = new Set(fields.map((f) => f.name));
|
|
2223
|
+
for (const k of Object.keys(data)) {
|
|
2224
|
+
if (!expected.has(k)) throw new Error(`unknown field in ${primary}: ${k}`);
|
|
2225
|
+
}
|
|
2226
|
+
const parts = [typeHash(primary, types)];
|
|
2227
|
+
for (const f of fields) {
|
|
2228
|
+
if (!(f.name in data)) throw new Error(`missing field ${primary}.${f.name}`);
|
|
2229
|
+
parts.push(encodeField(f.type, data[f.name], types));
|
|
2230
|
+
}
|
|
2231
|
+
return shake256Digest(concatBytesArr(parts), DIGEST_LEN);
|
|
2232
|
+
}
|
|
2233
|
+
var RESERVED_DOMAIN_FIELDS = {
|
|
2234
|
+
name: "string",
|
|
2235
|
+
version: "string",
|
|
2236
|
+
chainId: "uint256",
|
|
2237
|
+
verifyingContract: "address",
|
|
2238
|
+
salt: "bytes32"
|
|
2239
|
+
};
|
|
2240
|
+
function validateDomainTypes(types) {
|
|
2241
|
+
const def = types.QRLDomain;
|
|
2242
|
+
if (!def) throw new Error("QRLDomain type is required");
|
|
2243
|
+
let hasName = false;
|
|
2244
|
+
for (const f of def) {
|
|
2245
|
+
const expected = RESERVED_DOMAIN_FIELDS[f.name];
|
|
2246
|
+
if (!expected) {
|
|
2247
|
+
throw new Error(
|
|
2248
|
+
`QRLDomain field "${f.name}" not in reserved set (name, version, chainId, verifyingContract, salt)`
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
if (f.type !== expected) {
|
|
2252
|
+
throw new Error(`QRLDomain field "${f.name}" must be type "${expected}", got "${f.type}"`);
|
|
2253
|
+
}
|
|
2254
|
+
if (f.name === "name") hasName = true;
|
|
2255
|
+
}
|
|
2256
|
+
if (!hasName) throw new Error("QRLDomain.name is required");
|
|
2257
|
+
}
|
|
2258
|
+
function validatePayloadReachability(primary, types) {
|
|
2259
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
2260
|
+
for (const root of ["QRLDomain", primary]) {
|
|
2261
|
+
for (const t of collectDependencies(root, types)) reachable.add(t);
|
|
2262
|
+
}
|
|
2263
|
+
for (const k of Object.keys(types)) {
|
|
2264
|
+
if (!reachable.has(k)) throw new Error(`unused referenced type: ${k}`);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
function computeTypedDataDigest(payload) {
|
|
2268
|
+
if (!payload || typeof payload !== "object") {
|
|
2269
|
+
throw new Error("invalid typed data payload");
|
|
2270
|
+
}
|
|
2271
|
+
validateTypeMap(payload.types);
|
|
2272
|
+
validateDomainTypes(payload.types);
|
|
2273
|
+
validatePayloadReachability(payload.primaryType, payload.types);
|
|
2274
|
+
const domainHash = hashStruct("QRLDomain", payload.domain, payload.types);
|
|
2275
|
+
const messageHash = hashStruct(payload.primaryType, payload.message, payload.types);
|
|
2276
|
+
return shake256Digest(concatBytes(SCHEME_TAG_TYPED, domainHash, messageHash), DIGEST_LEN);
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// src/signing/verify.ts
|
|
2280
|
+
function bytesOrHex(v) {
|
|
2281
|
+
if (v instanceof Uint8Array) return v;
|
|
2282
|
+
if (typeof v === "string") return hexToBytes(v);
|
|
2283
|
+
throw new Error("expected Uint8Array or 0x-hex string");
|
|
2284
|
+
}
|
|
2285
|
+
function verifyMessage({
|
|
2286
|
+
signature,
|
|
2287
|
+
publicKey,
|
|
2288
|
+
messageBytes
|
|
2289
|
+
}) {
|
|
2290
|
+
try {
|
|
2291
|
+
const sig = bytesOrHex(signature);
|
|
2292
|
+
const pk = bytesOrHex(publicKey);
|
|
2293
|
+
const msg = bytesOrHex(messageBytes);
|
|
2294
|
+
const digest = computeMessageDigest(msg);
|
|
2295
|
+
return mldsaVerify(sig, digest, pk, SCHEME_TAG_MSG);
|
|
2296
|
+
} catch {
|
|
2297
|
+
return false;
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
function verifyTypedData({ signature, publicKey, payload }) {
|
|
2301
|
+
try {
|
|
2302
|
+
const sig = bytesOrHex(signature);
|
|
2303
|
+
const pk = bytesOrHex(publicKey);
|
|
2304
|
+
const digest = computeTypedDataDigest(payload);
|
|
2305
|
+
return mldsaVerify(sig, digest, pk, SCHEME_TAG_TYPED);
|
|
2306
|
+
} catch {
|
|
2307
|
+
return false;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
1574
2310
|
export {
|
|
1575
2311
|
BLOB_LEN,
|
|
1576
2312
|
CID_LEN,
|
|
1577
2313
|
ConnectionManager,
|
|
1578
2314
|
ConnectionStatus,
|
|
1579
2315
|
DEFAULT_RELAY_URL,
|
|
2316
|
+
DIGEST_LEN,
|
|
1580
2317
|
FP_LEN,
|
|
1581
2318
|
KeyExchange,
|
|
1582
2319
|
KeyExchangeMessageType,
|
|
@@ -1584,16 +2321,32 @@ export {
|
|
|
1584
2321
|
PROTOCOL_VERSION,
|
|
1585
2322
|
QRLConnectProvider as QRLConnect,
|
|
1586
2323
|
QRLConnectProvider,
|
|
2324
|
+
QRL_CONNECT_PROVIDER_INFO,
|
|
1587
2325
|
RESTRICTED_METHODS,
|
|
2326
|
+
SCHEME_TAG_MSG,
|
|
2327
|
+
SCHEME_TAG_TYPED,
|
|
2328
|
+
SCHEME_VERSION_MSG,
|
|
2329
|
+
SCHEME_VERSION_TYPED,
|
|
1588
2330
|
SocketClient,
|
|
1589
2331
|
UNRESTRICTED_METHODS,
|
|
2332
|
+
bytesToHex,
|
|
1590
2333
|
cidFromString,
|
|
1591
2334
|
cidRandom,
|
|
1592
2335
|
cidToString,
|
|
1593
2336
|
computeFingerprint,
|
|
2337
|
+
computeMessageDigest,
|
|
2338
|
+
computeTypedDataDigest,
|
|
2339
|
+
concatBytes,
|
|
2340
|
+
encodeField,
|
|
2341
|
+
encodeType,
|
|
1594
2342
|
fingerprintEquals,
|
|
1595
2343
|
generateConnectionURI,
|
|
1596
2344
|
getAppStoreUrl,
|
|
2345
|
+
hashStruct,
|
|
2346
|
+
hexToBytes,
|
|
1597
2347
|
isMobileBrowser,
|
|
1598
|
-
parseConnectionURI
|
|
2348
|
+
parseConnectionURI,
|
|
2349
|
+
typeHash,
|
|
2350
|
+
verifyMessage,
|
|
2351
|
+
verifyTypedData
|
|
1599
2352
|
};
|