@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/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/PQCrypto.ts
10
+ // src/crypto/primitives.ts
12
11
  import { ml_kem768 } from "@noble/post-quantum/ml-kem.js";
13
- var textEncoder = new TextEncoder();
14
- var LABEL = textEncoder.encode("pq-pair/v1");
15
- var LABEL_AEAD_SUFFIX = textEncoder.encode(" aead");
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 || !c.subtle) {
23
- throw new Error("PQCrypto: WebCrypto SubtleCrypto is not available in this environment");
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
- return u;
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 kemKeygen() {
36
+ function mlkemKeygen() {
31
37
  const { publicKey, secretKey } = ml_kem768.keygen();
32
38
  return { pk: publicKey, sk: secretKey };
33
39
  }
34
- function kemEncaps(pk) {
40
+ function mlkemEncaps(pk) {
35
41
  const { cipherText, sharedSecret } = ml_kem768.encapsulate(pk);
36
42
  return { ct: cipherText, ss: sharedSecret };
37
43
  }
38
- function kemDecaps(sk, ct) {
44
+ function mlkemDecaps(sk, ct) {
39
45
  return ml_kem768.decapsulate(ct, sk);
40
46
  }
41
- async function transcriptHash(cid, pk, ct) {
42
- const buf = concat(LABEL, cid, pk, ct);
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 deriveAeadKey(ss, htx) {
46
- const ikm = await subtle().importKey("raw", bs(ss), "HKDF", false, ["deriveKey"]);
47
- const info = concat(LABEL, LABEL_AEAD_SUFFIX, htx);
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: new Uint8Array(32), info: bs(info) },
50
- ikm,
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 importRawAeadKey(raw) {
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
- const buf = await subtle().exportKey("raw", key);
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
- const ct = await subtle().encrypt(
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
- const pt = await subtle().decrypt(
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
- "qrl_sign",
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 \u2014 ignoring");
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
- return {
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 \u2014 ignoring");
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 \u2014 session not established");
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 \u2014 session not established");
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?.connected) return;
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
- log("Socket", `Participants changed: ${data.event}`);
556
- this.emit("participants_changed", data);
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 (!this.pendingJoin || this.pendingJoin.channelId !== channelId) return;
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 || "Failed to join channel"));
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 || "Failed to send message"));
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] + ALPHABET[c1] + ALPHABET[c2];
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] + ALPHABET[c1];
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
- const digest = await c.subtle.digest("SHA-256", bs2(buf));
804
- return new Uint8Array(digest);
873
+ return sha256(buf);
805
874
  }
806
875
  function fingerprintEquals(a, b) {
807
- if (a.length !== b.length) return false;
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 \u2014 this wallet and this dApp must both run protocol v2"
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 \u2014 regenerate the QR with a v2.0+ dApp SDK");
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 relayUrl = params.get("r") || void 0;
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
- const c = globalThis.crypto;
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 || DEFAULT_RELAY_URL;
922
- this.chainId = options.chainId || "0x0";
923
- this.storageKey = options.storageKey || `${STORAGE_KEY_PREFIX}:session`;
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 = uuidv4();
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 = uuidv4();
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 = uuidv4();
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(this.channelId);
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.setStatus(this.walletPresent ? "connected" /* CONNECTED */ : "waiting" /* WAITING */);
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 = JSON.parse(raw);
1128
- if (session.version !== 2) return false;
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 = uuidv4();
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
- this.messageQueue = this.messageQueue.then(() => this.handleRelayMessage(data)).catch((err) => error("ConnectionManager", "messageQueue handler error:", err));
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 (typeof message === "object" && message !== null) {
1192
- const msg = message;
1193
- if (msg.type === "key_handshake_SYNACK" /* SYNACK */) {
1194
- await this.handleSynAck(message);
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 (msg.type === "key_handshake_SYN" /* SYN */ || msg.type === "key_handshake_ACK" /* ACK */) {
1198
- warn("ConnectionManager", `Unexpected ${msg.type} on dApp side \u2014 ignoring`);
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
- this.handleDecryptedMessage(parsed);
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 info = msg;
1234
- const nextAccounts = info.accounts || [];
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
- async sendEncrypted(message) {
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: 2,
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 = JSON.parse(raw);
1338
- if (session.version !== 2) {
1339
- log("ConnectionManager", "Dropping legacy (pre-v2) session from storage");
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 (!session.createdAt || Date.now() - session.createdAt > SESSION_TTL_MS) {
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 requestCounter = 0;
1653
+ var QRL_CONNECT_PROVIDER_INFO = {
1654
+ name: "MyQRLWallet (Mobile)",
1655
+ rdns: "com.qrlwallet.connect",
1656
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAYAAABS3GwHAAAAAXNSR0IArs4c6QAAH1dJREFUeF7tnXt8XGWZ+L/PmaT3JpOkrVCKgD9wFcELwgoLArIsWnDLCnIVgZ8gF1lcylVsk5xMKuXiiiKLn7L82OWiK1ZBtgpdFRDdAu4P1EVAuaNoQbrNTJJek5nzbN+TpE0nk8yZmTMzZybv/AXNe3ue9/me9/a8zyvYn9XAJNaATGLZrehWA1gArBFMag1YACZ191vhLQDWBia1BiwAk7r7rfAWAGsDk1oDFoBJ3f1WeAuAtYFJrQELwKTufiu8BcDawKTWwKQEQF0cwGGtPQjMtv6ngA8m8ViJJ6D1TsekAEDZZugu01MeHxLlIBXmi7A7ymwxKNjfdg1ohkEV3sDhj+LxG8fhN7M7eVGkPmGodwBEXRp7MpwdE7pUZBYwq96FDpHnLQqbFF2dXsf5c29hY72NCnVrC6+6TGvxOA6HK1TlQyNGUbcCh2j1I0WNnv8IbFH0+kbhG7Nc3ixDdVUpsi7tQRXp66TDE7lSYEZVNFuPlQoZ9XhePF0UX8bL9SBi3QHwxtXMnTqFOx2Rj6J2kRu6kZq1gOqrHpzV0sWaWp8S1RUA6tLQq6wAOdvf5bG/smlAlDcyaT2m9RqeKVslFSi4bgDQk4gl38VpTkzutF/+CliOX4X+clD427kuaytVY9j11A0AvS4f8zx5QMROe8I2knHLM5MhR7/a4nJpxeoMuaK6AEBdZvSqfBPhePv1D9lC8hSnqhlxeGfc5ZXK1hxObXUBwLp2PhgTecQRZoejFltKYA0IKqqrmoQTxSUdOF9EEtY8AGbLM9nJchG5quaFiYhRFNoMsyBOD+rRbct5rtC81U5f8zbzrMuU3TxeR2RetZU5WetXYTOefqolwX21poOaB6CvnUM8kTXYxW9VbU887WzuJlHVRhRRec0D0NPOGXbrs4ieDz2LfqvZ5Yxac5qreQCS7ZwtMbnd7v6EbtEFFegvhLs43gJQkNpKT5zswEWks+ZJLl0VVSvBOM2J6oPNXRxnAahwN6RclqnKEgtAhRU/XN0Oj1H9SdzlGAtAhfsh1cEyRJaEXW2WK3DYxVesvOwrXWF/KCwAFevK3BWVGYC1Ar9WYSvKXwD7hm1A5VafwgaUx0Ukqei7BPYL01FwBAAR/XFzJx+1I0C5ezSr/LIBoDyVdvSCQXhu17UM9s5jgcbkq8Ci2oFAN6ny8fQWnpwzg409MF/SnOU0SHdYmwYWgAobfHZ1ZQFAzf1XvTCeMK7VO34plwNB/hNlapXFDlS94+llTd18ZXRic3Le6/I0iBkJSv7ZKVDJKiytgLIAAF7G0/e2dfPsTsZzHo2pXUiJSPRvmQnqoIc1uTyWreFkB3eKyKdL0/xQbjsChKHFEsooFwADm3S3eTeMvfua7GCDiMwsockVyaqKxlQPberm8Ryj5i2IXBhGQ+wIEIYWSyijXAA0bNLdZlUAAH8PvQT5x82qaMbRw9pyjAA9nXzDQS4Io1oLQBhaLKGMcgFQiRFg2Hg8M48QQ4HxZwrrHvMEAKQ6+QYWAN/qyvLxKcGeC85aLgDKPgIMXS7/sQ5yVXwKT/dvpcVr5DOKXI3QUnLHWAAC2VLJeg5USxkT9Q6fBIdchVdOAPwvv2r/xkH2WrCc9SNtNwNBbwfLNYy7DRaAQCZRDwAkPJX2kAUpPwDo3S1djNmJSbmcLCp3KEwL1IPjJao0AKI/jtuDsJK6rKjMPS6fc5Cbzf52iBCUDYBRi8abW7q4OFvoZDtHSExWmbilRSlkx3Ay7iI4zDXAqOp+EHdZZE+CS+q1wjMnl7IIR75vFpAWgFH6q9AIsL1G1RXNCS6stUBZIdpM4cYbRo7XFzN99mzWqMgHQhSmeiNAgiPEq60RQJWNip7SmuCHYfRpJcsI0WYq2eyd61rfznsckTUiNIfUCm9wUBfMvYY3xkxRQjgIG5oGae4pkMuRgvx7yVMgQTPkPgcIdQpk9rJEvxnv4sxa+/rXxTaob0oraOz9E2ci0sFQ3P9SwfYym3WPtuv5Y6UBSLkchcr9Jox7KTAbyDzRQ3MdhKU6uBWRz5ZSvn9ioWz20LtFuKrFJVVSeVXKXKqhVKnZY6s1r75sgDkDyhUxOA+V4heRDt5AWt89bxkv5likGleIkn2BxNGbm10+n13+epePxVRWopTmbmF8gWJ6WFPHWFeIpMud4skZJXReWhx9IOOxYn2Sh/f5OltLKKuqWesGgGwtbn8GCfhpQBUfCby0nljbDKa1XkdvrmxJl3jqNbbsuSfpoOWOLsfUQS+NNG9btLtsyq7jyfNofHsDU+euY4D34BVahyn/uWdx5s5lytwj2Swnk8mu49WzmbZnC/LURtL984O//OK33Zye1mAArPFMoG4BCGjzNtkk14AFYJIbwGQX3wIw2S1gkstvAZjkBjDZxa9bAMyDGX9cwJQFG0lTwEJvshvEePI/tRZpGyC25wGofL52d32y5atbAPpdTsio3BFmBAQLh7kCqa+0dLF/veiibgFIuZwLcmsIh2L10tchyaFes9AoLl5IBVa1mLoFINnOHeLImVXVbn1W7pHWD8W/xJP1IF5dAvCIS8P7lZcFeXs9dFKUZFDjAuHpP8QT3FyLvj91vwbwb1W5nIgn37FvBoSPztAlfn1gQDhlnsuG8GuobIl1NwKoy7Skx68dERPK0P7Ko4GUJ/rhVre23wg2qqk7AFJL+aTE5K6SrxSWx3DqotThUeC7zeb6ppkS1fCvrgDQi5na28IqRP6mhvsk8k33LV7QtKfHz0nwg1peC9QVAKkOrlGRq+tKqAjiMOqT/9y0Rj12+lJ+H8FmBmpSXdiKv/Bt5xxicovqtj3qQKLbRKVoYCSol6Dfbe7i1FodBWreVozx9yzhEKdRvo2ye10ubEqx1HLnFVRVv7RZWD4/x/2Gcldfavk1D8CGL/LegUZWjez517xApfZoNfKbB0TQpc0u/1hrI0FN24u5DB8TVqrIu+2XvxqWP6rO4TcVkg537OWypcqtCVx9rQIgqaW8Q2PyXYT3GX+fWhUkcE/VQEJVMg66uOm33CIrx17FjKIINWk3PVfR7EzjRypykDX+aJmVwFtk9LCmZbxUC9OhmgPALHqTnSxzRK6wOz7RMv7trRFe1y16eMu1vBbRFo5qatRbmNW+1BIOpIHHFWmw8/4Id57oyuZd+ZScz2CEW1lbrhD9LvMGPX4mw34+NTd8RdkSwm6bkgQ9KZ7gobCLDrO8mrEhf+rTwRdFJGFuedVMw8PsrVorS/VXzQk+GOW1QM3YUZ/LnLTKzwXMY8/2VwsaGLo7cGFzglujCkFN2JL5+qdcFm8Ld2gOWuyvhjSg8OtG0YWz3LEvbkZBjJqwp3VXMnvKdHpGFr5RUJxtQ34NDL+AuQXVM+IJvpc/R+VT1AQAyXYucWLyFfMKTOVVZGssRQPDdwe+3exyehTvDkTeoNRlSm+Ge3Dk70rpCJu3ihpQtPlNnSq3Rm9LNPIA9Lrs7XnyIxH2qmIX2qpL0YDxGPX0spYEN5ZSTDnyRh6AVAd/DfIfKsQi39hy9FC9lKm8Ek/o/4maOJG3qWQnX0fkIuvzEzXTCd6e4cszPekBPWTucl4InrP8KSMPQE8nv3WQd5VfFWNrGHPbW1GzkFPV14FnUNZFbmEnzBy+GHSAiDSqICObB9Xq7GE9blTVk1oTPFiNvhyvzmrpJJAO3rycmVNn0C9SHW/nkW08NUG2RB9FuL95Xx7iuW0hMjuHoyFETYMj1HYhSdhPMhxNTBahvBthrlRrJ01RD726JcH1UToUi1r3jQZDejr4uIPcX5UAV+b1Q/QnONzcAE/MdnkrELURTGRiJa3P8L6Yw+miXKQisZFmVswABBXVrzUJl0UprmjF5C/ULvyL7h2ciyMrKh3gVmGTI7q8B768p8vWKH2xCtXj6PTDwQMOwOFuVfmLkB8Xz9s0z9PVr/yZRQdGaDs00gCkOvmyiCyuKADK8+roF+Iu99eL4WdbZu9S9vEa+GdUjjB/q5QRqOqj8d34myi5SFdK9rxfh+wEZuHW67LaD3JVuXnrgIge0eTyi3o1/hE9+weMHj/FkYONrithCIo+E5dt3qEuAwUbRJkyVELuopruA9DFGlQOKaqAAjMppDSjn2pdxgP5spp7CZkBFiA4NOZLXdm/Dw7C1Eb6XofX3pPH0JIue6KsBDmwEoag6Np4G++I0gszlZC7KAsYHgEeAzm4qAIKyTR0UrksnqBzvC+/eXc4leZwhHYceYfAjAqOTIVIgwqDqvqWeNwbb+BLEy06e5ZwmBPjUUScgiopIrGqbo47tEqEokZEHAB5DCgrAMNbnS9s3qof3TXHHVazcOxbyt6eQyeIceiKrM6yFrx+/E5zZiHC4rjLfbng9l3NO7gBkcvKLpio9vcyc/cb2VwEP2XJUnaZi2310AhQGQDU04tbu7k5V1t7ExzkZbhdkP2KlaVa+bYf5Cn9it7csh/tuV6O37yMPbYMysOAGdnK97MABNdtpQBA9c1mh91yTRP8ub7Hz6iPtwYGRPXSpi5uyT69Ni9q9u7LTYp8rqy7QhaAiAFgDmc87WpKkMieHqx1mTE9wz0Sk+OiOtcPrs3tez9vOsL+TS7/k53XvKugMVlpAShcq2XJUaERwMuoHtuW4D+yhTCLQ2mQ+2Hboq0sElahUEE9T29rSXB+NvDrL6bJaZNUWV0l7AgQvNMrAYAqr3uqC9u6eTarZdLTzhXiyHV1Y/wjAopmnK28rWk563daNJs1VydrEdkleC8VmFJV+/vtIjiQ1ioBAPDSgOjCeS4vZRtDXxf/piqnBGpsLSUy0760Htu8jNVZu0YGgJsRubBsUz47AgS3lAoB8AKiC+Mur+xkDC5OUlkjyMF1NwIYQVU/mX1J3d/u7SShIkssAMHttGwpKwTA7zZv1YXZ+//+oZfHHxHZtZyLwnyvy5UNPtUL4glWZI8AqXYSErMAlM2oCym4EgAI/NbbqsdmB3E1APR6rFWRt4UJwIjBi/rz7w0qJEVGHQoN+Ty1qjllhl0E/PinYf881c+1JviGBaByjoAF92HVAVD+pAwtCIv9Eo/+woswiKc/8Rzubsjwe1F6BgdZ23ItfduVsxIn9d8saHRoGoixv6PmPjQnINIc5rTEArDDHIvt24INutAM9QCAcUXwlM2oPtrQyOLZS3mhkCuUfjzUq2hypnM1npwHNCFsv8xSqE5H0lsALAC+BiacApU6AhiHNE8fUFixPsnD+3zdvKNV/K/vat6pU/i0Il+gxKmRBcACUF4AhpzQuuNvsExuJW32XYo3/R05zdqkP80+Xsy4bMs7ii3TAmABKB8Awp8yGU20dvPP5bhU43tvtvNeiXEXxkGviMtCFgALQLkASKvoOXG4eyIffGPE27vAjA1d/v830MngyF/ywbPRZf6gmscnCg8ZYwGwAJQDgC2qem1Lgq6JpiappeylDoc7wpGKHOXH8Bm5iuL5M6XfAQ9kMtzb18gvJ3pytH8p7047sgrBj7gWdEfDAmABCB0AFb0j3sn/HW+XR29iamqd71/0KYG9TajHcacv5mEJpVdV18QGuXz2cp7PeZnFnFekOUEdvkMBwZMsABaAsAH4Q0x04WyX57K//mY7N/kFdpdp3CciHyh4zq4kM+j5rb/l3lxv75ry+1xu90TOCurJaQGwAIQHwNAzQFc3jxPxzGxfelO4B+T9xeza+JMipX+bA09nPMFXc40EySXsITF5hIARtC0AFoDQAPAjHXSxIJdhPusyZb7HE/6Xv8jfdvcJYat6+tl4YtsCO2tb1Xdk6+CfPJELg6wDLAAWgFAA8C/Ue3pVvJvrs+3bxDWdPoP/p1K6S/WOu736cibDEXO+xJ+y6+t1+UtP5fEgL2haACwAoQAAvInocXGXX2YbZMrlKFTMu1jxIj/+O2XbMRLoyrjLyWPWGkMerK8gske+UcACYAEIC4BfDogeNy/HC4gpl9tQOScM49+5DPViwvtmuzyTXXayg8WIfMUCEFzr+XQVvKSQU9aCM5yn+mCLw8dzHXqlOiSF0ByyWoZi/Xi6rCVBR3bZ61zmN6qMmR5lp7MjgB0BSh4BzJTEQ5e1ddGebWAblnJM2pHVZQvrLnLPuvXeWdkOdn4kC+XPgsyaCDwLgAUgFABQPbMlYXxydvz8EORLuZIGWV7wnn+e4WLUjtAvnK16XPbF9hcvZurcVn6uyEETDe0WAAtAyQCYAjzVM1oTfDMbgGQnNznD75qFOQUa5VL6pDl4y47voytoTK31PUWPNvWOB4EFwAJQVgBSndxUrof9hiHICcDwucMDiJibZBaAAF8fuwge705wgAsxonpGc44RoKedy5yYXF/Gly2fdHKMAGYKNKeV/8wX7tyOAHYEKHkEGHJR0EuzH3/2T2XbOcUT+Va5niASYVUTeqq4bBr9kXt9MdNnN5kQLxMHt7IAWABKBsC3f/T7LV18InukNY/S9apsNKeyAUbhwpIM3Tb7WryLS7NdIt5ymdXoSV++EO4WAAtAKACI8POB6Xrc3KuMs9rOv2Qn3xPkhMKsO39qAU88PbypmzVj6mznbBz5l3zzWguABSAUAIA/eKLHteY4le1p51REbhdh+kQL0vwmvyOFmXY56GMvvsGRuV5aTHbwMxH5cL4yLQAWgLAAwPP0tNZuvp1tdH1X05ZulB+K8KHQAFDdOOjxgXnLeDG7vrVX8fYZ0+TVINMuC4AFIDwAVH/RkuCQXO7Q2165PFg9+b4Kb8s3Lcn31QbSil4bfw4318WYVAdLcKQ7yOGbBcACEBoAZiWcUT21rZvvjFkMD/npm6nQvypMCWDkOyXZfvJr5v2ijzfBUbmeGO2/mrmZKWKiPR8QpA4LgAUgPAD8kvTphkGOmXUNfx4DwdB7vGepSIfAgiAG6pc4klDNS/V6+5ZNXLHLlzE7S9mQSLKDC0TEvHHmBBlpLAAWgJABwNOMuvFlLBvv8npSWRBD/k2FQyaapuwcQUs3Ocq5TXO4d7y3dde77BvzeEpFpgVda1gALAChAjB8b/fPafS0OQl+OsFbww19cLLncYkgB+SK8+nfMlPWgd7uDHLD7OX0jFden8ucTIZ7cOSokS61I0DQMXYoXRB9FVZiSKlr4T7AaFGHIViP6okt3Tw6kRp6Xfb2PB4Wkd1zTGnw3RzgR/mCa6U6+S7IopFYoUE7044AdgQIdQQYbcQCj2U8vaC127+xNW5M0GQn3YIszQbA5E+LLmxzR4VNz0qUWk6LbuVGQc40H4pCv2QWAAtA2QAYXsD+QVQvb3a2nQa7eLlGg94OLtKhhevOi1qPR+IxXSQuG3Lkk75raPW28E8qcmKxD2hYACwAZQVgWL0DqLpbNnFTrt2bng4ucnIAgMfDAzE9fl4WACYy9AY4POPJahWmFvrVHw2TBcACUAkA/Pu7qC5t7mJ59kJ2XADg4QEZC4A5Wc40ym8QSn63zAJgAagYAIJ2N7m4JQPgMsdTed68IVbqPoMFwAJQMQAUTcRdukICwESObrMAlKoBC0DlADAvxXSFNgJYAMKz/REbCLnEkIqrtXOAnGKbNQB6XbPLF0sdAYy/T3qKvCTmobwSf3YKZEeA0EYAcz3LcVjleST9qP4yfLg49F8iyg/iORzlCl0Er7uS2Y1TuV6FmX5IIvMz9Snq3zmI8WGQ+dYbtLCvQ9DDw8JKDSF1TYwAQlLRS+Kd3DXRmXouV4ZCARiy93FqUehtZ091+JGI7J1P/XYEsCNAKCOAonfG2zhvPEe1iQyx0HOAfEZt/v7WUvaZEhNzAj2h67UFwAIQCgC5wqIEMVSTZlwAlIcGHP277IOwIOWqy5Q+5SFFDpsovQXAAhAKAI7qlU0JbghinNlp+jq5wUMuz/53VZ7C0aNbXFKFlvuqy7QW5Tcw8TTIAmABCAUA4L/Mqe1c1wSkDfYYtpnHr1/C/IZGeRQdet1xp5+Sdhz9xGyXH5p/D1LuyNqgbwlLvJgkbFiU4J8OuwgeLzKcx1oVeduwEY6vUdUHcfhXzC6QQybt5V6oNjioCpoZpKXR4RKdIHqDSSeqyx3hcY+h4Fe5yjVl+ovjNLNo4EjgErMnlK9T7QhgR4DtI8CmrXrsrtfy2mgLN45nvUEBGCIkox6bRHJ7fvpl+zdd/G3LmQiNeb9R/hkCW/AYGHeHyZwyDN2DnGoc5IK6RlsALABDGlCex9OF8WWYcCLbfwaAlPIyyJ55R4C8lhytBP67Bp6e09bN7TvJDJJqJyExWRLkLKEoqUS1v5eZu9/I5qLylyFTvtGyDFUGK7IS5wDAS7JVFzZfy0ujW+W6OJ/P8KA4ckxkFRRMjTulGrmdo6qLWhOsygagr5OEShkBUNX+fgtAoK6rCADKy+LpwuasQFNmUZnq5DZBPhOosTWSyAfArC8yelC8m6fGjAAd3CWOnG5HgAh0aEUAEPodRxc2dYyNs5layieJiXn8ouB4PhFQ35gm7LibqU/HXd4vQ2uMHdM+RXpdkiDhv2s2UoudAgU3jQoBoJ6nn25J8K3s7cZ1VzK/cbo8BLwreKujndJsLXlbObR1OU+Mcc5rZ3/HkafLKoEFILh6KwGA/wn09OF4gqPHfBHNW18dfEaRFSLEgrc8milNVGkVXfkrOOMjLunsVvZ2cLGK3FTW1lsAgqu3EgAMtyatg3pQyzX8Ort1eh6NqV1YIkhH2V58DK6SklKq0hNz9NAmF3OnYKffepemmHIPyMdKqiRfZgtAPg1lz0nlMeDg4LmKS6mqqzb0c0qu7bk3L2fmtFl0qifnOg7xkb324mqqfC6BAU91TczjwqZlmCuVY37JJRwuDfIgMKOsLbQABFdvBUeAbcEb/MXwyc0uJsDsmJ9ezNRknL8Uh0tRFokjYtoX6S1Sc+SGvuDAP+JwX/aLkiNCGtl6W3gakXcG750iU1oAgiuukgAMtUqfHhAOncgL0/e5uYEZPX2c1ugwLwP7IGV4Bim4mnZOaRoorBN4LZPhv1ti23a3XHSCUI1Tkhm+5sTk/LJtfY5uoQUgeM9WHACzP47++6atfHbX5SY25/i/nS6mjBv7LbisoaUcNSTlc6LzH/Pr4HQVuYUQrlkGksECEEhNQ99jf1+6MmuAnVrl6bc3xThnftYLjMFbHv2U3zmJ2DHv4SPq+TfIKjeTswAEN45qAeD7rIneoxmua+nmV8FbXDspezr8MO6LRaS8i95slVgAghtJtQDwW+g/Rcobnqrb2sVt2WcEwaWITkpfn0v5IDHuVJF9io0rWpJEFoDg6qsqACPN9KM86P9XjxUNaZ6YNXXbI9QuW4NLUeWUCskvsHtsOvuqcqIipwvMGPLMrsLPAhBc6ZEAYEdzzcnpWoW3QJ8R5QmHsaepwaUrX8rtoagd9lI4DJVdBXYDP5xKdX8WgOD6jxgA2xtupkNR2vjJpdFR74sV/HZA8B4qIqUFILjSogpAcAmqm7Jq05yJxLYABDcKC0BwXdVMSgtA8K7yAejkMUTK7gsUvFU2ZSkaUHRTXGgTly2llBNm3qpsBgQRwACQ7OSnMUc+XGsOaEHkm5xp9JV1Pey7z9ejs5MWXQCGriV+VUQuroifyuS0yIpKLaJrmnblI3I+gxWteILKIg1Abwfn4sgKC0BUzKW0dgj6gybhE5LjQk5pJRefO7IAGJFSSziQmPxXrV9GKb576iinf7qu3XHzXFTWfeRqShlpAIzHYrKDPzgiC6qpJFt3KBrYguoZ8QT3TvR+cig1FVBIpAEwciQ7uVFE/sFOgwro1YglHYrfSDLj6F/NyXEls5rNjTwAPe0cKo6sFphVTUXZuovXwBAAel9LghOKL6U8OSMPQP81zM1skfsRDimPCmyp5daACplG9IBZLuUNu1KEIJEHwMjU4/L3DnKTnQYV0cNRyKJ6T3MXp0Vp8TuilpoAQG9iau96ngTZLwr9adsQXAMKKRE9Me7ycPBclUtZEwAYdaxfwsFOTB4UIV459diaStKA2e709PaX3uTCA2+NzuHXaJlqBgB1aUjBF0Wl04TEL6ljbOaya2DIbVyfUTi8mOeeyt7A4QpqBgB/LXAVzTKVR8SR99v1QKVMpLh6VPl9rFE/2tSeOxhXcaWGn6umADDi97vMS3vcjshx5v9rToDw+zB6Jaqm1eOk+POskpVkotfAHS2qSfvZ4LLLoCercXivmBfZ7S9CGtD/QTil2eWRfLGJotDomjWedS7zG5TlIJ8cHdOyZgWKgjWU0gb//TN93vM4z0SkE3eC99JKqSfkvDVtL4+4NLzPY7Eg1/nL4qjH6wy58yJTnDF91bviczlPPh8dX/8g+qlpAEYE7GlnfxEWI3K8QGsQwW2aEDSgbFb0CXW4bf16vheliy5BpasLAIyw+hWm9/Wx27aAVuercpEg00bcqEeiJNSNsEF7txzphp56VRH9oedxIw6/irv01sJ8P5c66s4m/MC159GQmsup0sCxiOyB0qZKswgzUaZplCI6l8NIQyhTzLQGBkT8J01Tah7sVv29Oqz20jzU1s3voujaUKjodQdAtgKSLnFJ00YDzQqzxGNqxqv9J48K7eiC0zvoFIetXobNoqQGlU2tL7A26tuahcpZ9wBMoJDJLHsQO4l6/K8gMuRNY40gr4psgnrWgAWgnnvXypZXAxaAvCqyCepZAxaAeu5dK1teDVgA8qrIJqhnDVgA6rl3rWx5NfC/zGV/wHfd+nEAAAAASUVORK5CYII="
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 — the wallet gets an instant disconnect instead of landing in
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
  };