@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.js CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  ConnectionManager: () => ConnectionManager,
36
36
  ConnectionStatus: () => ConnectionStatus,
37
37
  DEFAULT_RELAY_URL: () => DEFAULT_RELAY_URL,
38
+ DIGEST_LEN: () => DIGEST_LEN,
38
39
  FP_LEN: () => FP_LEN,
39
40
  KeyExchange: () => KeyExchange,
40
41
  KeyExchangeMessageType: () => KeyExchangeMessageType,
@@ -42,18 +43,34 @@ __export(index_exports, {
42
43
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
43
44
  QRLConnect: () => QRLConnectProvider,
44
45
  QRLConnectProvider: () => QRLConnectProvider,
46
+ QRL_CONNECT_PROVIDER_INFO: () => QRL_CONNECT_PROVIDER_INFO,
45
47
  RESTRICTED_METHODS: () => RESTRICTED_METHODS,
48
+ SCHEME_TAG_MSG: () => SCHEME_TAG_MSG,
49
+ SCHEME_TAG_TYPED: () => SCHEME_TAG_TYPED,
50
+ SCHEME_VERSION_MSG: () => SCHEME_VERSION_MSG,
51
+ SCHEME_VERSION_TYPED: () => SCHEME_VERSION_TYPED,
46
52
  SocketClient: () => SocketClient,
47
53
  UNRESTRICTED_METHODS: () => UNRESTRICTED_METHODS,
54
+ bytesToHex: () => bytesToHex,
48
55
  cidFromString: () => cidFromString,
49
56
  cidRandom: () => cidRandom,
50
57
  cidToString: () => cidToString,
51
58
  computeFingerprint: () => computeFingerprint,
59
+ computeMessageDigest: () => computeMessageDigest,
60
+ computeTypedDataDigest: () => computeTypedDataDigest,
61
+ concatBytes: () => concatBytes,
62
+ encodeField: () => encodeField,
63
+ encodeType: () => encodeType,
52
64
  fingerprintEquals: () => fingerprintEquals,
53
65
  generateConnectionURI: () => generateConnectionURI,
54
66
  getAppStoreUrl: () => getAppStoreUrl,
67
+ hashStruct: () => hashStruct,
68
+ hexToBytes: () => hexToBytes,
55
69
  isMobileBrowser: () => isMobileBrowser,
56
- parseConnectionURI: () => parseConnectionURI
70
+ parseConnectionURI: () => parseConnectionURI,
71
+ typeHash: () => typeHash,
72
+ verifyMessage: () => verifyMessage,
73
+ verifyTypedData: () => verifyTypedData
57
74
  });
58
75
  module.exports = __toCommonJS(index_exports);
59
76
 
@@ -62,68 +79,144 @@ var import_eventemitter34 = __toESM(require("eventemitter3"));
62
79
 
63
80
  // src/ConnectionManager.ts
64
81
  var import_eventemitter33 = __toESM(require("eventemitter3"));
65
- var import_uuid = require("uuid");
66
82
 
67
83
  // src/KeyExchange.ts
68
84
  var import_eventemitter3 = __toESM(require("eventemitter3"));
69
85
 
70
- // src/PQCrypto.ts
86
+ // src/crypto/primitives.ts
71
87
  var import_ml_kem = require("@noble/post-quantum/ml-kem.js");
72
- var textEncoder = new TextEncoder();
73
- var LABEL = textEncoder.encode("pq-pair/v1");
74
- var LABEL_AEAD_SUFFIX = textEncoder.encode(" aead");
75
- var DIR_DAPP_TX = new Uint8Array([0, 0, 0, 1]);
76
- var DIR_WALLET_TX = new Uint8Array([0, 0, 0, 2]);
77
- var ML_KEM_768_CT_LEN = 1088;
78
- var AEAD_KEY_LEN = 32;
79
- function subtle() {
88
+ var import_sha3 = require("@noble/hashes/sha3.js");
89
+ var mldsa = __toESM(require("@theqrl/mldsa87"));
90
+ function webCrypto() {
80
91
  const c = globalThis.crypto;
81
- if (!c || !c.subtle) {
82
- throw new Error("PQCrypto: WebCrypto SubtleCrypto is not available in this environment");
92
+ if (!c) {
93
+ throw new Error("crypto: WebCrypto is not available in this environment");
94
+ }
95
+ return c;
96
+ }
97
+ function subtle() {
98
+ const c = webCrypto();
99
+ if (!c.subtle) {
100
+ throw new Error("crypto: WebCrypto SubtleCrypto is not available in this environment");
83
101
  }
84
102
  return c.subtle;
85
103
  }
86
104
  function bs(u) {
87
- return u;
105
+ if (u.buffer instanceof ArrayBuffer) {
106
+ return new Uint8Array(u.buffer, u.byteOffset, u.byteLength);
107
+ }
108
+ const copy = new Uint8Array(u.byteLength);
109
+ copy.set(u);
110
+ return copy;
88
111
  }
89
- function kemKeygen() {
112
+ function mlkemKeygen() {
90
113
  const { publicKey, secretKey } = import_ml_kem.ml_kem768.keygen();
91
114
  return { pk: publicKey, sk: secretKey };
92
115
  }
93
- function kemEncaps(pk) {
116
+ function mlkemEncaps(pk) {
94
117
  const { cipherText, sharedSecret } = import_ml_kem.ml_kem768.encapsulate(pk);
95
118
  return { ct: cipherText, ss: sharedSecret };
96
119
  }
97
- function kemDecaps(sk, ct) {
120
+ function mlkemDecaps(sk, ct) {
98
121
  return import_ml_kem.ml_kem768.decapsulate(ct, sk);
99
122
  }
100
- async function transcriptHash(cid, pk, ct) {
101
- const buf = concat(LABEL, cid, pk, ct);
102
- return new Uint8Array(await subtle().digest("SHA-256", bs(buf)));
123
+ function shake256Digest(data, dkLen) {
124
+ return (0, import_sha3.shake256)(data, { dkLen });
103
125
  }
104
- async function deriveAeadKey(ss, htx) {
105
- const ikm = await subtle().importKey("raw", bs(ss), "HKDF", false, ["deriveKey"]);
106
- const info = concat(LABEL, LABEL_AEAD_SUFFIX, htx);
126
+ async function sha256(data) {
127
+ return new Uint8Array(await subtle().digest("SHA-256", bs(data)));
128
+ }
129
+ function mldsaVerify(signature, digest, publicKey, ctx) {
130
+ return mldsa.cryptoSignVerify(signature, digest, publicKey, ctx);
131
+ }
132
+ async function hkdfAesGcmKey(ikm, salt, info) {
133
+ const ikmKey = await subtle().importKey("raw", bs(ikm), "HKDF", false, ["deriveKey"]);
107
134
  return subtle().deriveKey(
108
- { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(32), info: bs(info) },
109
- ikm,
135
+ { name: "HKDF", hash: "SHA-256", salt: bs(salt), info: bs(info) },
136
+ ikmKey,
110
137
  { name: "AES-GCM", length: 256 },
111
138
  true,
112
139
  ["encrypt", "decrypt"]
113
140
  );
114
141
  }
115
- async function importRawAeadKey(raw) {
116
- if (raw.length !== AEAD_KEY_LEN) {
117
- throw new Error(`PQCrypto: raw AEAD key must be ${AEAD_KEY_LEN} bytes`);
118
- }
142
+ async function importAesGcmKey(raw) {
119
143
  return subtle().importKey("raw", bs(raw), { name: "AES-GCM", length: 256 }, true, [
120
144
  "encrypt",
121
145
  "decrypt"
122
146
  ]);
123
147
  }
148
+ async function exportAesGcmKey(key) {
149
+ return new Uint8Array(await subtle().exportKey("raw", key));
150
+ }
151
+ async function aesGcmEncrypt(key, iv, aad2, plaintext) {
152
+ const ct = await subtle().encrypt(
153
+ { name: "AES-GCM", iv: bs(iv), additionalData: bs(aad2) },
154
+ key,
155
+ bs(plaintext)
156
+ );
157
+ return new Uint8Array(ct);
158
+ }
159
+ async function aesGcmDecrypt(key, iv, aad2, ciphertext) {
160
+ const pt = await subtle().decrypt(
161
+ { name: "AES-GCM", iv: bs(iv), additionalData: bs(aad2) },
162
+ key,
163
+ bs(ciphertext)
164
+ );
165
+ return new Uint8Array(pt);
166
+ }
167
+ function randomBytes(n) {
168
+ const c = webCrypto();
169
+ if (typeof c.getRandomValues !== "function") {
170
+ throw new Error("crypto: crypto.getRandomValues is not available");
171
+ }
172
+ return c.getRandomValues(new Uint8Array(n));
173
+ }
174
+ function randomUuid() {
175
+ const b = randomBytes(16);
176
+ b[6] = (b[6] ?? 0) & 15 | 64;
177
+ b[8] = (b[8] ?? 0) & 63 | 128;
178
+ const hex = Array.from(b, (v) => v.toString(16).padStart(2, "0")).join("");
179
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
180
+ }
181
+ function constantTimeEquals(a, b) {
182
+ if (a.length !== b.length) return false;
183
+ let d = 0;
184
+ for (let i = 0; i < a.length; i++) d |= (a[i] ?? 0) ^ (b[i] ?? 0);
185
+ return d === 0;
186
+ }
187
+
188
+ // src/PQCrypto.ts
189
+ var textEncoder = new TextEncoder();
190
+ var LABEL = textEncoder.encode("pq-pair/v1");
191
+ var LABEL_AEAD_SUFFIX = textEncoder.encode(" aead");
192
+ var DIR_DAPP_TX = new Uint8Array([0, 0, 0, 1]);
193
+ var DIR_WALLET_TX = new Uint8Array([0, 0, 0, 2]);
194
+ var ML_KEM_768_CT_LEN = 1088;
195
+ var AEAD_KEY_LEN = 32;
196
+ function kemKeygen() {
197
+ return mlkemKeygen();
198
+ }
199
+ function kemEncaps(pk) {
200
+ return mlkemEncaps(pk);
201
+ }
202
+ function kemDecaps(sk, ct) {
203
+ return mlkemDecaps(sk, ct);
204
+ }
205
+ async function transcriptHash(cid, pk, ct) {
206
+ return sha256(concat(LABEL, cid, pk, ct));
207
+ }
208
+ async function deriveAeadKey(ss, htx) {
209
+ const info = concat(LABEL, LABEL_AEAD_SUFFIX, htx);
210
+ return hkdfAesGcmKey(ss, new Uint8Array(32), info);
211
+ }
212
+ async function importRawAeadKey(raw) {
213
+ if (raw.length !== AEAD_KEY_LEN) {
214
+ throw new Error(`PQCrypto: raw AEAD key must be ${AEAD_KEY_LEN} bytes`);
215
+ }
216
+ return importAesGcmKey(raw);
217
+ }
124
218
  async function exportRawAeadKey(key) {
125
- const buf = await subtle().exportKey("raw", key);
126
- return new Uint8Array(buf);
219
+ return exportAesGcmKey(key);
127
220
  }
128
221
  function nonce(dir, seq) {
129
222
  if (dir.length !== 4) {
@@ -145,20 +238,10 @@ function aad(htx, seq) {
145
238
  return out;
146
239
  }
147
240
  async function seal(key, dir, seq, htx, pt) {
148
- const ct = await subtle().encrypt(
149
- { name: "AES-GCM", iv: bs(nonce(dir, seq)), additionalData: bs(aad(htx, seq)) },
150
- key,
151
- bs(pt)
152
- );
153
- return new Uint8Array(ct);
241
+ return aesGcmEncrypt(key, nonce(dir, seq), aad(htx, seq), pt);
154
242
  }
155
243
  async function open(key, dir, seq, htx, ct) {
156
- const pt = await subtle().decrypt(
157
- { name: "AES-GCM", iv: bs(nonce(dir, seq)), additionalData: bs(aad(htx, seq)) },
158
- key,
159
- bs(ct)
160
- );
161
- return new Uint8Array(pt);
244
+ return aesGcmDecrypt(key, nonce(dir, seq), aad(htx, seq), ct);
162
245
  }
163
246
  function zeroize(b) {
164
247
  b.fill(0);
@@ -179,7 +262,7 @@ function toBase64(bytes) {
179
262
  let bin = "";
180
263
  for (let i = 0; i < bytes.length; i += CHUNK) {
181
264
  const slice = bytes.subarray(i, i + CHUNK);
182
- bin += String.fromCharCode.apply(null, slice);
265
+ bin += String.fromCharCode.apply(null, Array.from(slice));
183
266
  }
184
267
  return btoa(bin);
185
268
  }
@@ -189,12 +272,6 @@ function fromBase64(b64) {
189
272
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
190
273
  return out;
191
274
  }
192
- function constantTimeEquals(a, b) {
193
- if (a.length !== b.length) return false;
194
- let d = 0;
195
- for (let i = 0; i < a.length; i++) d |= a[i] ^ b[i];
196
- return d === 0;
197
- }
198
275
 
199
276
  // src/config.ts
200
277
  var DEFAULT_RELAY_URL = "https://qrlwallet.com";
@@ -204,15 +281,13 @@ var SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
204
281
  var STORAGE_KEY_PREFIX = "@qrlwallet/connect";
205
282
  var REQUEST_TIMEOUT_MS = 5 * 60 * 1e3;
206
283
  var WALLET_UNRESPONSIVE_MS = 30 * 1e3;
284
+ var RECONNECT_WALLET_PROBE_MS = 12 * 1e3;
207
285
  var RESTRICTED_METHODS = /* @__PURE__ */ new Set([
208
286
  "qrl_requestAccounts",
209
287
  "qrl_sendTransaction",
210
288
  "qrl_signTransaction",
211
- "qrl_sign",
212
- "personal_sign",
289
+ "qrl_signMessage",
213
290
  "qrl_signTypedData",
214
- "qrl_signTypedData_v3",
215
- "qrl_signTypedData_v4",
216
291
  "wallet_addQrlChain",
217
292
  "wallet_switchQrlChain"
218
293
  ]);
@@ -316,6 +391,13 @@ var KeyExchange = class extends import_eventemitter3.default {
316
391
  this.awaitingSynAck = false;
317
392
  this.awaitingAck = false;
318
393
  this.keysExchanged = false;
394
+ // The ACK we produced for the current handshake. A wallet that lost its
395
+ // transport right after sending SYNACK re-sends the identical SYNACK on
396
+ // rejoin; we answer with this cached ACK so the handshake converges
397
+ // instead of stalling (onSynAck ignores duplicates and would otherwise
398
+ // never reply). Cleared on reset; not persisted (a reloaded dApp has a
399
+ // completed session and the wallet is no longer awaiting an ACK).
400
+ this.lastAck = null;
319
401
  this.isOriginator = isOriginator;
320
402
  if (restored) {
321
403
  this.session = restored;
@@ -346,7 +428,7 @@ var KeyExchange = class extends import_eventemitter3.default {
346
428
  */
347
429
  async onSynAck(cid, msg) {
348
430
  if (!this.isOriginator) {
349
- warn("KeyExchange", "Responder received SYNACK \u2014 ignoring");
431
+ warn("KeyExchange", "Responder received SYNACK - ignoring");
350
432
  return null;
351
433
  }
352
434
  if (!this.awaitingSynAck || !this.keypair) {
@@ -389,11 +471,13 @@ var KeyExchange = class extends import_eventemitter3.default {
389
471
  this.step = "key_handshake_ACK" /* ACK */;
390
472
  this.emit("keys_exchanged");
391
473
  this.emit("step_change", this.step);
392
- return {
474
+ const ack = {
393
475
  type: "key_handshake_ACK" /* ACK */,
394
476
  c1: toBase64(c1),
395
477
  v: PROTOCOL_VERSION
396
478
  };
479
+ this.lastAck = ack;
480
+ return ack;
397
481
  }
398
482
  /**
399
483
  * Responder (wallet) side: begin handshake from a scanned QR.
@@ -434,7 +518,7 @@ var KeyExchange = class extends import_eventemitter3.default {
434
518
  */
435
519
  async onAck(msg) {
436
520
  if (this.isOriginator) {
437
- warn("KeyExchange", "Originator received ACK \u2014 ignoring");
521
+ warn("KeyExchange", "Originator received ACK - ignoring");
438
522
  return;
439
523
  }
440
524
  if (!this.awaitingAck) {
@@ -461,23 +545,17 @@ var KeyExchange = class extends import_eventemitter3.default {
461
545
  /** Encrypt a string for the counterparty. Returns base64. */
462
546
  async encryptMessage(data) {
463
547
  if (!this.session) {
464
- throw new Error("KeyExchange: cannot encrypt \u2014 session not established");
548
+ throw new Error("KeyExchange: cannot encrypt - session not established");
465
549
  }
550
+ const seq = this.session.sendSeq++;
466
551
  const pt = textEncoder2.encode(data);
467
- const ct = await seal(
468
- this.session.key,
469
- this.session.sendDir,
470
- this.session.sendSeq,
471
- this.session.htx,
472
- pt
473
- );
474
- this.session.sendSeq++;
552
+ const ct = await seal(this.session.key, this.session.sendDir, seq, this.session.htx, pt);
475
553
  return toBase64(ct);
476
554
  }
477
555
  /** Decrypt a base64 ciphertext from the counterparty. */
478
556
  async decryptMessage(b64) {
479
557
  if (!this.session) {
480
- throw new Error("KeyExchange: cannot decrypt \u2014 session not established");
558
+ throw new Error("KeyExchange: cannot decrypt - session not established");
481
559
  }
482
560
  const ct = fromBase64(b64);
483
561
  const pt = await open(
@@ -503,12 +581,17 @@ var KeyExchange = class extends import_eventemitter3.default {
503
581
  this.keysExchanged = false;
504
582
  this.awaitingSynAck = false;
505
583
  this.awaitingAck = false;
584
+ this.lastAck = null;
506
585
  this.step = "key_handshake_SYN" /* SYN */;
507
586
  if (emit) this.emit("step_change", this.step);
508
587
  }
509
588
  areKeysExchanged() {
510
589
  return this.keysExchanged;
511
590
  }
591
+ /** The ACK produced for the current handshake, for duplicate-SYNACK replies. */
592
+ getLastAck() {
593
+ return this.lastAck;
594
+ }
512
595
  getSession() {
513
596
  return this.session;
514
597
  }
@@ -575,7 +658,10 @@ var SocketClient = class extends import_eventemitter32.default {
575
658
  * Connect to the relay server.
576
659
  */
577
660
  connect() {
578
- if (this.socket?.connected) return;
661
+ if (this.socket) {
662
+ if (!this.socket.connected) this.socket.connect();
663
+ return;
664
+ }
579
665
  log("Socket", `Connecting to ${this.relayUrl}`);
580
666
  this.socket = (0, import_socket.io)(this.relayUrl, {
581
667
  path: RELAY_PATH,
@@ -593,7 +679,7 @@ var SocketClient = class extends import_eventemitter32.default {
593
679
  const reconnectChannelId = this.channelId;
594
680
  this.joinChannelNow(reconnectChannelId).then((result) => {
595
681
  this.settlePendingJoin(reconnectChannelId, { result });
596
- this.emit("reconnected");
682
+ this.emit("reconnected", result);
597
683
  }).catch((err) => {
598
684
  const reconnectErr = err instanceof Error ? err : new Error(String(err));
599
685
  this.settlePendingJoin(reconnectChannelId, { error: reconnectErr });
@@ -611,8 +697,14 @@ var SocketClient = class extends import_eventemitter32.default {
611
697
  this.emit("message", data);
612
698
  });
613
699
  this.socket.on("participants_changed", (data) => {
614
- log("Socket", `Participants changed: ${data.event}`);
615
- this.emit("participants_changed", data);
700
+ if (typeof data !== "object" || data === null) return;
701
+ const rec = { ...data };
702
+ const event = typeof rec.event === "string" ? rec.event : "";
703
+ log("Socket", `Participants changed: ${event}`);
704
+ this.emit("participants_changed", {
705
+ event,
706
+ ...typeof rec.clientType === "string" ? { clientType: rec.clientType } : {}
707
+ });
616
708
  });
617
709
  this.socket.on("connect_error", (err) => {
618
710
  warn("Socket", `Connection error: ${err.message}`);
@@ -625,7 +717,7 @@ var SocketClient = class extends import_eventemitter32.default {
625
717
  * it doesn't fire after the fact.
626
718
  */
627
719
  settlePendingJoin(channelId, outcome) {
628
- if (!this.pendingJoin || this.pendingJoin.channelId !== channelId) return;
720
+ if (this.pendingJoin?.channelId !== channelId) return;
629
721
  clearTimeout(this.pendingJoin.watchdog);
630
722
  const pending = this.pendingJoin;
631
723
  this.pendingJoin = null;
@@ -692,12 +784,14 @@ var SocketClient = class extends import_eventemitter32.default {
692
784
  if (response.success) {
693
785
  log("Socket", `Joined channel ${channelId}`);
694
786
  resolve({
695
- bufferedMessages: response.bufferedMessages || [],
696
- channelPublicKey: response.channelPublicKey ?? null
787
+ bufferedMessages: response.bufferedMessages ?? [],
788
+ channelPublicKey: response.channelPublicKey ?? null,
789
+ participants: response.participants ?? [],
790
+ terminated: response.terminated === true
697
791
  });
698
792
  } else {
699
793
  error("Socket", `Failed to join channel: ${response.error}`);
700
- reject(new Error(response.error || "Failed to join channel"));
794
+ reject(new Error(response.error ?? "Failed to join channel"));
701
795
  }
702
796
  }
703
797
  );
@@ -720,7 +814,7 @@ var SocketClient = class extends import_eventemitter32.default {
720
814
  if (response?.success) {
721
815
  resolve({ success: true, buffered: response.buffered });
722
816
  } else {
723
- reject(new Error(response?.error || "Failed to send message"));
817
+ reject(new Error(response?.error ?? "Failed to send message"));
724
818
  }
725
819
  }
726
820
  );
@@ -776,26 +870,26 @@ var DECODE = (() => {
776
870
  })();
777
871
  function decodeChar(charCode) {
778
872
  if (charCode < 0 || charCode >= 128) return -1;
779
- return DECODE[charCode];
873
+ return DECODE[charCode] ?? -1;
780
874
  }
781
875
  function base45Encode(bytes) {
782
876
  const n = bytes.length;
783
877
  let out = "";
784
878
  let i = 0;
785
879
  while (i + 2 <= n) {
786
- const v = bytes[i] << 8 | bytes[i + 1];
880
+ const v = (bytes[i] ?? 0) << 8 | (bytes[i + 1] ?? 0);
787
881
  const c2 = Math.floor(v / 2025);
788
882
  const r = v - c2 * 2025;
789
883
  const c1 = Math.floor(r / 45);
790
884
  const c0 = r - c1 * 45;
791
- out += ALPHABET[c0] + ALPHABET[c1] + ALPHABET[c2];
885
+ out += `${ALPHABET[c0] ?? ""}${ALPHABET[c1] ?? ""}${ALPHABET[c2] ?? ""}`;
792
886
  i += 2;
793
887
  }
794
888
  if (i < n) {
795
- const v = bytes[i];
889
+ const v = bytes[i] ?? 0;
796
890
  const c1 = Math.floor(v / 45);
797
891
  const c0 = v - c1 * 45;
798
- out += ALPHABET[c0] + ALPHABET[c1];
892
+ out += `${ALPHABET[c0] ?? ""}${ALPHABET[c1] ?? ""}`;
799
893
  }
800
894
  return out;
801
895
  }
@@ -844,29 +938,18 @@ var FP_LABEL = new TextEncoder().encode("pq-fp/v2");
844
938
  var CID_LEN = 16;
845
939
  var FP_LEN = 32;
846
940
  var BLOB_LEN = 4 + CID_LEN + FP_LEN;
847
- function bs2(u) {
848
- return u;
849
- }
850
941
  async function computeFingerprint(cid, pk) {
851
942
  if (cid.length !== CID_LEN) {
852
943
  throw new Error(`qrUri: cid must be ${CID_LEN} bytes`);
853
944
  }
854
- const c = globalThis.crypto;
855
- if (!c || !c.subtle) {
856
- throw new Error("qrUri: WebCrypto SubtleCrypto is not available");
857
- }
858
945
  const buf = new Uint8Array(FP_LABEL.length + cid.length + pk.length);
859
946
  buf.set(FP_LABEL, 0);
860
947
  buf.set(cid, FP_LABEL.length);
861
948
  buf.set(pk, FP_LABEL.length + cid.length);
862
- const digest = await c.subtle.digest("SHA-256", bs2(buf));
863
- return new Uint8Array(digest);
949
+ return sha256(buf);
864
950
  }
865
951
  function fingerprintEquals(a, b) {
866
- if (a.length !== b.length) return false;
867
- let diff = 0;
868
- for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
869
- return diff === 0;
952
+ return constantTimeEquals(a, b);
870
953
  }
871
954
  async function generateConnectionURI(cid, pk, relayUrl) {
872
955
  if (cid.length !== CID_LEN) {
@@ -905,7 +988,7 @@ async function parseConnectionURI(uri) {
905
988
  }
906
989
  if (params.has("channelId") || params.has("pubKey")) {
907
990
  throw new Error(
908
- "qrUri: legacy v1 URI detected \u2014 this wallet and this dApp must both run protocol v2"
991
+ "qrUri: legacy v1 URI detected - this wallet and this dApp must both run protocol v2"
909
992
  );
910
993
  }
911
994
  const q = params.get("q");
@@ -921,7 +1004,7 @@ async function parseConnectionURI(uri) {
921
1004
  }
922
1005
  if (blob.length !== BLOB_LEN) {
923
1006
  if (blob.length === 1208 && startsWith(blob, PQP1_MAGIC)) {
924
- throw new Error("qrUri: legacy PQP1 URI detected \u2014 regenerate the QR with a v2.0+ dApp SDK");
1007
+ throw new Error("qrUri: legacy PQP1 URI detected - regenerate the QR with a v2.0+ dApp SDK");
925
1008
  }
926
1009
  throw new Error(`qrUri: expected ${BLOB_LEN}-byte blob, got ${blob.length}`);
927
1010
  }
@@ -930,8 +1013,8 @@ async function parseConnectionURI(uri) {
930
1013
  }
931
1014
  const cid = blob.slice(4, 4 + CID_LEN);
932
1015
  const fp = blob.slice(4 + CID_LEN, 4 + CID_LEN + FP_LEN);
933
- const relayUrl = params.get("r") || void 0;
934
- return { cid, fp, relayUrl };
1016
+ const r = params.get("r");
1017
+ return { cid, fp, relayUrl: r === null || r === "" ? void 0 : r };
935
1018
  }
936
1019
  function cidToString(cid) {
937
1020
  if (cid.length !== CID_LEN) {
@@ -939,7 +1022,7 @@ function cidToString(cid) {
939
1022
  }
940
1023
  let hex = "";
941
1024
  for (let i = 0; i < CID_LEN; i++) {
942
- hex += cid[i].toString(16).padStart(2, "0");
1025
+ hex += (cid[i] ?? 0).toString(16).padStart(2, "0");
943
1026
  }
944
1027
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
945
1028
  }
@@ -955,15 +1038,79 @@ function cidFromString(s) {
955
1038
  return out;
956
1039
  }
957
1040
  function cidRandom() {
958
- const c = globalThis.crypto;
959
- if (!c || typeof c.getRandomValues !== "function") {
960
- throw new Error("qrUri: crypto.getRandomValues is not available");
961
- }
962
- return c.getRandomValues(new Uint8Array(CID_LEN));
1041
+ return randomBytes(CID_LEN);
963
1042
  }
964
1043
 
965
1044
  // src/ConnectionManager.ts
966
1045
  var DAPP_PARTICIPANT_CONFLICT_ERROR_MSG = "dapp participant is already connected";
1046
+ function isRecord(v) {
1047
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1048
+ }
1049
+ function isStringArray(v) {
1050
+ return Array.isArray(v) && v.every((x) => typeof x === "string");
1051
+ }
1052
+ function isRelayMessage(v) {
1053
+ 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);
1054
+ }
1055
+ var MESSAGE_TYPE_BY_VALUE = Object.fromEntries(
1056
+ Object.values(MessageType).map((m) => [m, m])
1057
+ );
1058
+ function parseJsonRpcResponse(msg) {
1059
+ if (typeof msg.id !== "string" && typeof msg.id !== "number") return null;
1060
+ const out = {
1061
+ jsonrpc: typeof msg.jsonrpc === "string" ? msg.jsonrpc : "2.0",
1062
+ id: msg.id
1063
+ };
1064
+ if ("result" in msg) out.result = msg.result;
1065
+ if (msg.error !== void 0) {
1066
+ if (!isRecord(msg.error) || typeof msg.error.message !== "string") return null;
1067
+ out.error = {
1068
+ code: typeof msg.error.code === "number" ? msg.error.code : -32e3,
1069
+ message: msg.error.message,
1070
+ data: msg.error.data
1071
+ };
1072
+ }
1073
+ return out;
1074
+ }
1075
+ function parsePersistedKex(v) {
1076
+ if (!isRecord(v)) return null;
1077
+ const { cid, kAeadRaw, htx, sendDir, recvDir, sendSeq, recvSeq } = v;
1078
+ 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) {
1079
+ return null;
1080
+ }
1081
+ return { cid, kAeadRaw, htx, sendDir, recvDir, sendSeq, recvSeq };
1082
+ }
1083
+ function parseDAppMetadata(v) {
1084
+ if (!isRecord(v) || typeof v.name !== "string" || typeof v.url !== "string") return null;
1085
+ const meta = { name: v.name, url: v.url };
1086
+ if (typeof v.icon === "string") meta.icon = v.icon;
1087
+ if (typeof v.redirectUrl === "string") meta.redirectUrl = v.redirectUrl;
1088
+ return meta;
1089
+ }
1090
+ function parseStoredSession(raw) {
1091
+ let v;
1092
+ try {
1093
+ v = JSON.parse(raw);
1094
+ } catch {
1095
+ return null;
1096
+ }
1097
+ if (!isRecord(v) || v.version !== 3) return null;
1098
+ if (typeof v.channelId !== "string" || typeof v.chainId !== "string") return null;
1099
+ if (typeof v.createdAt !== "number" || typeof v.lastActivity !== "number") return null;
1100
+ const keyExchange = parsePersistedKex(v.keyExchange);
1101
+ const dappMetadata = parseDAppMetadata(v.dappMetadata);
1102
+ if (!keyExchange || !dappMetadata || !isStringArray(v.connectedAccounts)) return null;
1103
+ return {
1104
+ version: 3,
1105
+ channelId: v.channelId,
1106
+ keyExchange,
1107
+ dappMetadata,
1108
+ connectedAccounts: v.connectedAccounts,
1109
+ chainId: v.chainId,
1110
+ createdAt: v.createdAt,
1111
+ lastActivity: v.lastActivity
1112
+ };
1113
+ }
967
1114
  var TERMINATE_SEND_TIMEOUT_MS = 800;
968
1115
  var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.default {
969
1116
  constructor(options) {
@@ -972,14 +1119,16 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
972
1119
  this.status = "disconnected" /* DISCONNECTED */;
973
1120
  this.connectedAccounts = [];
974
1121
  this.unresponsiveTimer = null;
1122
+ this.reconnectProbeTimer = null;
975
1123
  this.failedReconnects = 0;
976
1124
  this.walletPresent = false;
977
1125
  this.pendingRestore = null;
978
1126
  this.messageQueue = Promise.resolve();
1127
+ this.outboundQueue = Promise.resolve();
979
1128
  this.dappMetadata = options.dappMetadata;
980
- this.relayUrl = options.relayUrl || DEFAULT_RELAY_URL;
981
- this.chainId = options.chainId || "0x0";
982
- this.storageKey = options.storageKey || `${STORAGE_KEY_PREFIX}:session`;
1129
+ this.relayUrl = options.relayUrl ?? DEFAULT_RELAY_URL;
1130
+ this.chainId = options.chainId ?? "0x0";
1131
+ this.storageKey = options.storageKey ?? `${STORAGE_KEY_PREFIX}:session`;
983
1132
  const stored = this.readStoredSession();
984
1133
  if (stored) {
985
1134
  this.channelId = stored.channelId;
@@ -989,7 +1138,7 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
989
1138
  this.pendingRestore = stored;
990
1139
  log("ConnectionManager", `Found persisted session for channel ${this.channelId}`);
991
1140
  } else {
992
- this.channelId = (0, import_uuid.v4)();
1141
+ this.channelId = randomUuid();
993
1142
  }
994
1143
  this.socketClient = new SocketClient(this.relayUrl, "dapp");
995
1144
  this.setupSocketListeners();
@@ -1001,13 +1150,23 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1001
1150
  this.failedReconnects = 0;
1002
1151
  }
1003
1152
  });
1004
- this.socketClient.on("reconnected", () => {
1153
+ this.socketClient.on("reconnected", (result) => {
1154
+ if (result?.terminated) {
1155
+ log("ConnectionManager", "Channel terminated, observed on auto-reconnect");
1156
+ this.handleSessionTerminated();
1157
+ return;
1158
+ }
1159
+ if (result) {
1160
+ this.walletPresent = result.participants.includes("wallet");
1161
+ }
1005
1162
  if (this.keyExchange?.areKeysExchanged()) {
1006
1163
  if (this.walletPresent) {
1164
+ this.clearReconnectProbe();
1007
1165
  this.setStatus("connected" /* CONNECTED */);
1008
1166
  this.failedReconnects = 0;
1009
1167
  } else {
1010
1168
  this.setStatus("waiting" /* WAITING */);
1169
+ this.armReconnectProbe();
1011
1170
  }
1012
1171
  } else {
1013
1172
  this.setStatus("waiting" /* WAITING */);
@@ -1034,11 +1193,17 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1034
1193
  this.walletPresent = true;
1035
1194
  log("ConnectionManager", "Wallet joined channel");
1036
1195
  this.clearUnresponsiveTimer();
1196
+ this.clearReconnectProbe();
1037
1197
  if (this.keyExchange?.areKeysExchanged()) {
1038
1198
  this.failedReconnects = 0;
1039
1199
  this.setStatus("connected" /* CONNECTED */);
1040
1200
  }
1041
1201
  }
1202
+ if (data.event === "close") {
1203
+ log("ConnectionManager", "Wallet closed the channel (explicit terminate)");
1204
+ this.handleSessionTerminated();
1205
+ return;
1206
+ }
1042
1207
  if (data.event === "disconnect" || data.event === "leave") {
1043
1208
  if (data.clientType === "wallet" || !data.clientType) {
1044
1209
  this.walletPresent = false;
@@ -1054,6 +1219,7 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1054
1219
  if (!this.keyExchange) return;
1055
1220
  this.keyExchange.on("keys_exchanged", () => {
1056
1221
  log("ConnectionManager", "Key exchange complete");
1222
+ this.clearReconnectProbe();
1057
1223
  this.setStatus("connected" /* CONNECTED */);
1058
1224
  this.walletPresent = true;
1059
1225
  this.failedReconnects = 0;
@@ -1074,6 +1240,7 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1074
1240
  */
1075
1241
  async getConnectionURI(retryOnConflict = true) {
1076
1242
  this.setStatus("connecting" /* CONNECTING */);
1243
+ this.clearReconnectProbe();
1077
1244
  this.pendingRestore = null;
1078
1245
  this.walletPresent = false;
1079
1246
  if (this.keyExchange) {
@@ -1083,7 +1250,7 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1083
1250
  this.setupKeyExchangeListeners();
1084
1251
  }
1085
1252
  const pk = this.keyExchange.initiate();
1086
- this.channelId = (0, import_uuid.v4)();
1253
+ this.channelId = randomUuid();
1087
1254
  this.socketClient.setPublicKey(toBase64(pk));
1088
1255
  this.socketClient.connect();
1089
1256
  try {
@@ -1094,7 +1261,7 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1094
1261
  "ConnectionManager",
1095
1262
  "Channel already has an active dApp participant. Rotating to a fresh channel."
1096
1263
  );
1097
- this.channelId = (0, import_uuid.v4)();
1264
+ this.channelId = randomUuid();
1098
1265
  return this.getConnectionURI(false);
1099
1266
  }
1100
1267
  this.setStatus("disconnected" /* DISCONNECTED */);
@@ -1128,17 +1295,39 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1128
1295
  this.setStatus("disconnected" /* DISCONNECTED */);
1129
1296
  return false;
1130
1297
  }
1298
+ return this.joinAndSettle();
1299
+ }
1300
+ /**
1301
+ * (Re)open the socket, join the persisted channel, drain buffered relay
1302
+ * messages, and settle status. Shared by the cold-restore reconnect() and
1303
+ * the warm resume() path whose socket was torn down by the reconnect probe.
1304
+ * Assumes channelId and a hydrated keyExchange are already in place, and
1305
+ * that the caller has reset walletPresent + set RECONNECTING status.
1306
+ */
1307
+ async joinAndSettle() {
1131
1308
  this.socketClient.connect();
1132
1309
  try {
1133
- const { bufferedMessages } = await this.socketClient.joinChannel(this.channelId);
1310
+ const { bufferedMessages, participants, terminated } = await this.socketClient.joinChannel(
1311
+ this.channelId
1312
+ );
1313
+ if (terminated) {
1314
+ log("ConnectionManager", "Stored session was terminated by the wallet; dropping it");
1315
+ this.handleSessionTerminated();
1316
+ return false;
1317
+ }
1318
+ if (participants.includes("wallet")) {
1319
+ this.walletPresent = true;
1320
+ }
1134
1321
  for (const msg of bufferedMessages) {
1135
1322
  this.enqueueRelayMessage(msg);
1136
1323
  }
1137
1324
  await this.messageQueue;
1138
- if (this.keyExchange?.areKeysExchanged()) {
1139
- this.setStatus(this.walletPresent ? "connected" /* CONNECTED */ : "waiting" /* WAITING */);
1325
+ if (this.keyExchange?.areKeysExchanged() && this.walletPresent) {
1326
+ this.clearReconnectProbe();
1327
+ this.setStatus("connected" /* CONNECTED */);
1140
1328
  } else {
1141
1329
  this.setStatus("waiting" /* WAITING */);
1330
+ this.armReconnectProbe();
1142
1331
  }
1143
1332
  return true;
1144
1333
  } catch (err) {
@@ -1147,6 +1336,65 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1147
1336
  return false;
1148
1337
  }
1149
1338
  }
1339
+ /**
1340
+ * Nudge the connection back to life after the dApp tab / wallet app was
1341
+ * backgrounded. Idempotent and safe to call from visibilitychange / online
1342
+ * / pageshow handlers. If a restored session has not yet been hydrated,
1343
+ * runs the full reconnect(); otherwise just re-opens the socket, whose
1344
+ * own connect handler re-joins the channel and drains the relay buffer.
1345
+ */
1346
+ resume() {
1347
+ if (this.status === "connected" /* CONNECTED */) return;
1348
+ if (this.pendingRestore && !this.keyExchange?.areKeysExchanged()) {
1349
+ void this.reconnect();
1350
+ return;
1351
+ }
1352
+ if (this.channelId && this.keyExchange?.areKeysExchanged()) {
1353
+ if (this.socketClient.getChannelId()) {
1354
+ this.socketClient.connect();
1355
+ } else {
1356
+ this.setStatus("reconnecting" /* RECONNECTING */);
1357
+ this.walletPresent = false;
1358
+ void this.joinAndSettle();
1359
+ }
1360
+ }
1361
+ }
1362
+ /**
1363
+ * A session was terminated for good (wallet sent a relay 'close', or the
1364
+ * join ack reported a tombstone). Clear local state and surface
1365
+ * DISCONNECTED so the consumer drops to a fresh-pairing UI.
1366
+ */
1367
+ handleSessionTerminated() {
1368
+ this.clearReconnectProbe();
1369
+ this.clearUnresponsiveTimer();
1370
+ this.walletPresent = false;
1371
+ this.pendingRestore = null;
1372
+ this.keyExchange = null;
1373
+ this.socketClient.leaveChannel();
1374
+ this.socketClient.disconnect();
1375
+ this.clearSession();
1376
+ this.setStatus("disconnected" /* DISCONNECTED */);
1377
+ }
1378
+ armReconnectProbe() {
1379
+ this.clearReconnectProbe();
1380
+ this.reconnectProbeTimer = setTimeout(() => {
1381
+ this.reconnectProbeTimer = null;
1382
+ if (this.walletPresent) return;
1383
+ warn(
1384
+ "ConnectionManager",
1385
+ `No wallet rejoined within ${RECONNECT_WALLET_PROBE_MS}ms; treating reconnect as dead`
1386
+ );
1387
+ this.socketClient.leaveChannel();
1388
+ this.socketClient.disconnect();
1389
+ this.setStatus("disconnected" /* DISCONNECTED */);
1390
+ }, RECONNECT_WALLET_PROBE_MS);
1391
+ }
1392
+ clearReconnectProbe() {
1393
+ if (this.reconnectProbeTimer) {
1394
+ clearTimeout(this.reconnectProbeTimer);
1395
+ this.reconnectProbeTimer = null;
1396
+ }
1397
+ }
1150
1398
  /**
1151
1399
  * Send a JSON-RPC request to the wallet (async, fire-and-forget).
1152
1400
  */
@@ -1183,8 +1431,8 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1183
1431
  try {
1184
1432
  const raw = localStorage.getItem(this.storageKey);
1185
1433
  if (!raw) return false;
1186
- const session = JSON.parse(raw);
1187
- if (session.version !== 2) return false;
1434
+ const session = parseStoredSession(raw);
1435
+ if (!session) return false;
1188
1436
  return Date.now() - session.createdAt <= SESSION_TTL_MS;
1189
1437
  } catch {
1190
1438
  return false;
@@ -1199,8 +1447,7 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1199
1447
  */
1200
1448
  async flushTerminate() {
1201
1449
  if (!this.keyExchange?.areKeysExchanged()) return;
1202
- const send = this.sendEncrypted({ type: "terminate" /* TERMINATE */ }).catch(() => {
1203
- });
1450
+ const send = this.sendEncrypted({ type: "terminate" /* TERMINATE */ }).catch(() => void 0);
1204
1451
  const timeout = new Promise((resolve) => setTimeout(resolve, TERMINATE_SEND_TIMEOUT_MS));
1205
1452
  await Promise.race([send, timeout]);
1206
1453
  }
@@ -1211,6 +1458,7 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1211
1458
  */
1212
1459
  async resetForNewChannel() {
1213
1460
  this.clearUnresponsiveTimer();
1461
+ this.clearReconnectProbe();
1214
1462
  this.walletPresent = false;
1215
1463
  await this.flushTerminate();
1216
1464
  this.socketClient.leaveChannel();
@@ -1218,7 +1466,7 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1218
1466
  this.clearSession();
1219
1467
  this.connectedAccounts = [];
1220
1468
  this.pendingRestore = null;
1221
- this.channelId = (0, import_uuid.v4)();
1469
+ this.channelId = randomUuid();
1222
1470
  this.keyExchange = null;
1223
1471
  this.socketClient = new SocketClient(this.relayUrl, "dapp");
1224
1472
  this.setupSocketListeners();
@@ -1226,6 +1474,7 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1226
1474
  }
1227
1475
  async disconnect() {
1228
1476
  this.clearUnresponsiveTimer();
1477
+ this.clearReconnectProbe();
1229
1478
  this.walletPresent = false;
1230
1479
  await this.flushTerminate();
1231
1480
  this.socketClient.leaveChannel();
@@ -1233,36 +1482,58 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1233
1482
  this.setStatus("disconnected" /* DISCONNECTED */);
1234
1483
  this.clearSession();
1235
1484
  this.connectedAccounts = [];
1485
+ this.pendingRestore = null;
1486
+ this.keyExchange = null;
1236
1487
  }
1237
1488
  // ── Internals ──────────────────────────────────────────────
1238
1489
  enqueueRelayMessage(data) {
1239
- this.messageQueue = this.messageQueue.then(() => this.handleRelayMessage(data)).catch((err) => error("ConnectionManager", "messageQueue handler error:", err));
1490
+ if (!isRelayMessage(data)) {
1491
+ warn("ConnectionManager", "Dropping malformed relay envelope");
1492
+ return;
1493
+ }
1494
+ this.messageQueue = this.messageQueue.then(() => this.handleRelayMessage(data)).catch((err) => {
1495
+ error("ConnectionManager", "messageQueue handler error:", err);
1496
+ });
1240
1497
  }
1241
1498
  async handleRelayMessage(data) {
1242
1499
  if (data.clientType === "wallet") {
1243
1500
  this.walletPresent = true;
1501
+ this.clearReconnectProbe();
1244
1502
  if (this.keyExchange?.areKeysExchanged() && this.status !== "connected" /* CONNECTED */) {
1245
1503
  this.failedReconnects = 0;
1246
1504
  this.setStatus("connected" /* CONNECTED */);
1247
1505
  }
1248
1506
  }
1249
1507
  const message = data.message;
1250
- if (typeof message === "object" && message !== null) {
1251
- const msg = message;
1252
- if (msg.type === "key_handshake_SYNACK" /* SYNACK */) {
1253
- await this.handleSynAck(message);
1508
+ if (isRecord(message)) {
1509
+ if (message.type === "key_handshake_SYNACK" /* SYNACK */) {
1510
+ if (typeof message.ct === "string" && typeof message.c0 === "string") {
1511
+ await this.handleSynAck({
1512
+ type: "key_handshake_SYNACK" /* SYNACK */,
1513
+ ct: message.ct,
1514
+ c0: message.c0,
1515
+ v: typeof message.v === "number" ? message.v : 0
1516
+ });
1517
+ } else {
1518
+ warn("ConnectionManager", "Dropping malformed SYNACK");
1519
+ }
1254
1520
  return;
1255
1521
  }
1256
- if (msg.type === "key_handshake_SYN" /* SYN */ || msg.type === "key_handshake_ACK" /* ACK */) {
1257
- warn("ConnectionManager", `Unexpected ${msg.type} on dApp side \u2014 ignoring`);
1522
+ if (message.type === "key_handshake_SYN" /* SYN */ || message.type === "key_handshake_ACK" /* ACK */) {
1523
+ warn("ConnectionManager", `Unexpected ${message.type} on dApp side, ignoring`);
1258
1524
  return;
1259
1525
  }
1260
1526
  }
1261
1527
  if (typeof message === "string" && this.keyExchange?.areKeysExchanged()) {
1262
1528
  try {
1263
1529
  const decrypted = await this.keyExchange.decryptMessage(message);
1530
+ await this.persistSession();
1264
1531
  const parsed = JSON.parse(decrypted);
1265
- this.handleDecryptedMessage(parsed);
1532
+ if (isRecord(parsed)) {
1533
+ this.handleDecryptedMessage(parsed);
1534
+ } else {
1535
+ warn("ConnectionManager", "Dropping non-object decrypted payload");
1536
+ }
1266
1537
  } catch (err) {
1267
1538
  error("ConnectionManager", "Failed to decrypt message:", err);
1268
1539
  }
@@ -1282,16 +1553,21 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1282
1553
  }
1283
1554
  if (response) {
1284
1555
  this.sendPlaintext(response);
1556
+ return;
1557
+ }
1558
+ const cachedAck = this.keyExchange.getLastAck();
1559
+ if (this.keyExchange.areKeysExchanged() && cachedAck) {
1560
+ log("ConnectionManager", "Duplicate SYNACK after handshake; re-sending cached ACK");
1561
+ this.sendPlaintext(cachedAck);
1285
1562
  }
1286
1563
  }
1287
1564
  handleDecryptedMessage(msg) {
1288
1565
  this.clearUnresponsiveTimer();
1289
- const type = msg.type;
1290
- switch (type) {
1566
+ const type = typeof msg.type === "string" ? msg.type : "";
1567
+ switch (MESSAGE_TYPE_BY_VALUE[type]) {
1291
1568
  case "wallet_info" /* WALLET_INFO */: {
1292
- const info = msg;
1293
- const nextAccounts = info.accounts || [];
1294
- const nextChainId = info.chainId || this.chainId;
1569
+ const nextAccounts = isStringArray(msg.accounts) ? msg.accounts : [];
1570
+ const nextChainId = typeof msg.chainId === "string" && msg.chainId ? msg.chainId : this.chainId;
1295
1571
  const accountsChanged = !this.areArraysEqual(this.connectedAccounts, nextAccounts);
1296
1572
  const chainChanged = this.chainId !== nextChainId;
1297
1573
  this.connectedAccounts = nextAccounts;
@@ -1306,7 +1582,11 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1306
1582
  break;
1307
1583
  }
1308
1584
  case "jsonrpc" /* JSONRPC */: {
1309
- const response = msg;
1585
+ const response = parseJsonRpcResponse(msg);
1586
+ if (!response) {
1587
+ warn("ConnectionManager", "Dropping malformed JSON-RPC response");
1588
+ break;
1589
+ }
1310
1590
  this.emit("jsonrpc_response", response);
1311
1591
  break;
1312
1592
  }
@@ -1328,11 +1608,28 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1328
1608
  error("ConnectionManager", "Failed to send plaintext:", err);
1329
1609
  });
1330
1610
  }
1331
- async sendEncrypted(message) {
1611
+ /**
1612
+ * Serialize every encrypt+persist+send on a single outbound queue.
1613
+ * Callers like sendJsonRpc() are fire-and-forget, so without the queue two
1614
+ * rapid provider.request() calls would interleave inside encryptMessage()
1615
+ * across its await and could complete out of order; the receiver's
1616
+ * contiguous-seq check would then drop the late one. (Nonce reuse itself is
1617
+ * already prevented by the synchronous seq reservation in KeyExchange.)
1618
+ */
1619
+ sendEncrypted(message) {
1620
+ const task = this.outboundQueue.then(() => this.sendEncryptedNow(message));
1621
+ this.outboundQueue = task.then(
1622
+ () => void 0,
1623
+ () => void 0
1624
+ );
1625
+ return task;
1626
+ }
1627
+ async sendEncryptedNow(message) {
1332
1628
  if (!this.keyExchange?.areKeysExchanged()) {
1333
1629
  throw new Error("sendEncrypted: not connected");
1334
1630
  }
1335
1631
  const encrypted = await this.keyExchange.encryptMessage(JSON.stringify(message));
1632
+ await this.persistSession();
1336
1633
  await this.socketClient.sendMessage({
1337
1634
  id: this.channelId,
1338
1635
  clientType: "dapp",
@@ -1373,7 +1670,7 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1373
1670
  const persistedKex = await this.keyExchange.exportPersisted();
1374
1671
  if (!persistedKex) return;
1375
1672
  const session = {
1376
- version: 2,
1673
+ version: 3,
1377
1674
  channelId: this.channelId,
1378
1675
  keyExchange: persistedKex,
1379
1676
  dappMetadata: this.dappMetadata,
@@ -1393,13 +1690,13 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1393
1690
  try {
1394
1691
  const raw = localStorage.getItem(this.storageKey);
1395
1692
  if (!raw) return null;
1396
- const session = JSON.parse(raw);
1397
- if (session.version !== 2) {
1398
- log("ConnectionManager", "Dropping legacy (pre-v2) session from storage");
1693
+ const session = parseStoredSession(raw);
1694
+ if (!session) {
1695
+ log("ConnectionManager", "Dropping legacy or malformed session from storage");
1399
1696
  localStorage.removeItem(this.storageKey);
1400
1697
  return null;
1401
1698
  }
1402
- if (!session.createdAt || Date.now() - session.createdAt > SESSION_TTL_MS) {
1699
+ if (Date.now() - session.createdAt > SESSION_TTL_MS) {
1403
1700
  localStorage.removeItem(this.storageKey);
1404
1701
  return null;
1405
1702
  }
@@ -1429,11 +1726,27 @@ function getAppStoreUrl() {
1429
1726
  }
1430
1727
 
1431
1728
  // src/QRLConnectProvider.ts
1432
- var requestCounter = 0;
1729
+ var QRL_CONNECT_PROVIDER_INFO = {
1730
+ name: "MyQRLWallet (Mobile)",
1731
+ rdns: "com.qrlwallet.connect",
1732
+ icon: "data:image/png;base64,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"
1733
+ };
1734
+ var EIP6963_ANNOUNCE_EVENT = "eip6963:announceProvider";
1735
+ var EIP6963_REQUEST_EVENT = "eip6963:requestProvider";
1433
1736
  var QRLConnectProvider = class extends import_eventemitter34.default {
1434
1737
  constructor(options) {
1435
1738
  super();
1436
1739
  this.pendingRequests = /* @__PURE__ */ new Map();
1740
+ this.eip6963Detail = null;
1741
+ this.eip6963RequestListener = null;
1742
+ this.resumeListener = null;
1743
+ this.resumeDebounce = null;
1744
+ // Random per-instance prefix keeps request ids unique across page loads.
1745
+ // A bare counter restarts at 1 on reload, and the relay buffers messages
1746
+ // for 5 minutes, so a stale buffered response could otherwise be matched
1747
+ // to a fresh request that drew the same small id.
1748
+ this.requestIdPrefix = randomUuid().slice(0, 8);
1749
+ this.requestCounter = 0;
1437
1750
  this.isQRLConnect = true;
1438
1751
  this.options = options;
1439
1752
  if (options.debug) {
@@ -1447,8 +1760,83 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
1447
1760
  });
1448
1761
  this.setupConnectionListeners();
1449
1762
  if (options.autoReconnect !== false) {
1450
- this.connectionManager.reconnect();
1763
+ void this.connectionManager.reconnect();
1764
+ }
1765
+ this.setupResumeListeners();
1766
+ if (options.announceProvider !== false) {
1767
+ this.startEip6963Announce();
1768
+ }
1769
+ }
1770
+ startEip6963Announce() {
1771
+ if (typeof window === "undefined" || typeof CustomEvent === "undefined") {
1772
+ return;
1773
+ }
1774
+ const overrides = this.options.providerInfo ?? {};
1775
+ this.eip6963Detail = Object.freeze({
1776
+ info: Object.freeze({
1777
+ uuid: randomUuid(),
1778
+ name: overrides.name ?? QRL_CONNECT_PROVIDER_INFO.name,
1779
+ icon: overrides.icon ?? QRL_CONNECT_PROVIDER_INFO.icon,
1780
+ rdns: overrides.rdns ?? QRL_CONNECT_PROVIDER_INFO.rdns
1781
+ }),
1782
+ provider: this
1783
+ });
1784
+ const announce = () => {
1785
+ if (!this.eip6963Detail) return;
1786
+ window.dispatchEvent(new CustomEvent(EIP6963_ANNOUNCE_EVENT, { detail: this.eip6963Detail }));
1787
+ };
1788
+ this.eip6963RequestListener = announce;
1789
+ window.addEventListener(EIP6963_REQUEST_EVENT, announce);
1790
+ announce();
1791
+ }
1792
+ /**
1793
+ * Re-open the relay socket when the dApp tab regains focus or the network
1794
+ * comes back. Debounced because visibilitychange + online can fire together.
1795
+ * No-op when already connected (ConnectionManager.resume guards that).
1796
+ */
1797
+ setupResumeListeners() {
1798
+ if (typeof window === "undefined" || typeof document === "undefined") return;
1799
+ if (this.resumeListener) return;
1800
+ const resume = () => {
1801
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
1802
+ if (this.resumeDebounce) clearTimeout(this.resumeDebounce);
1803
+ this.resumeDebounce = setTimeout(() => {
1804
+ this.resumeDebounce = null;
1805
+ this.connectionManager.resume();
1806
+ }, 300);
1807
+ };
1808
+ this.resumeListener = resume;
1809
+ document.addEventListener("visibilitychange", resume);
1810
+ window.addEventListener("online", resume);
1811
+ window.addEventListener("pageshow", resume);
1812
+ }
1813
+ /**
1814
+ * Remove the foreground/online resume listeners. Called on disconnect so a
1815
+ * torn-down session can't be silently revived by a later tab focus; the
1816
+ * listeners are re-armed by getConnectionURI()/newConnection() if the same
1817
+ * provider is re-paired. Safe in any env.
1818
+ */
1819
+ teardownResumeListeners() {
1820
+ if (typeof window !== "undefined" && typeof document !== "undefined" && this.resumeListener) {
1821
+ document.removeEventListener("visibilitychange", this.resumeListener);
1822
+ window.removeEventListener("online", this.resumeListener);
1823
+ window.removeEventListener("pageshow", this.resumeListener);
1824
+ }
1825
+ this.resumeListener = null;
1826
+ if (this.resumeDebounce) {
1827
+ clearTimeout(this.resumeDebounce);
1828
+ this.resumeDebounce = null;
1829
+ }
1830
+ }
1831
+ /**
1832
+ * Stop announcing this provider over EIP-6963. Safe to call from any env.
1833
+ */
1834
+ stopEip6963Announce() {
1835
+ if (typeof window !== "undefined" && this.eip6963RequestListener) {
1836
+ window.removeEventListener(EIP6963_REQUEST_EVENT, this.eip6963RequestListener);
1451
1837
  }
1838
+ this.eip6963RequestListener = null;
1839
+ this.eip6963Detail = null;
1452
1840
  }
1453
1841
  setupConnectionListeners() {
1454
1842
  this.connectionManager.on("status_changed", (status) => {
@@ -1506,6 +1894,7 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
1506
1894
  * Generate a connection URI for QR code display or deep link redirect.
1507
1895
  */
1508
1896
  async getConnectionURI() {
1897
+ this.setupResumeListeners();
1509
1898
  return this.connectionManager.getConnectionURI();
1510
1899
  }
1511
1900
  /**
@@ -1538,7 +1927,7 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
1538
1927
  if (this.connectionManager.getStatus() !== "connected" /* CONNECTED */) {
1539
1928
  throw new Error("Not connected to QRL Wallet");
1540
1929
  }
1541
- const id = ++requestCounter;
1930
+ const id = `${this.requestIdPrefix}-${++this.requestCounter}`;
1542
1931
  return new Promise((resolve, reject) => {
1543
1932
  const pending = {
1544
1933
  id,
@@ -1619,7 +2008,7 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
1619
2008
  /**
1620
2009
  * Disconnect from wallet and clean up. Returns once the TERMINATE has
1621
2010
  * either been flushed to the relay or the 800ms best-effort window has
1622
- * elapsed — the wallet gets an instant disconnect instead of landing in
2011
+ * elapsed - the wallet gets an instant disconnect instead of landing in
1623
2012
  * its stale-session grace period.
1624
2013
  */
1625
2014
  async disconnect() {
@@ -1627,9 +2016,373 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
1627
2016
  pending.reject(new Error("Disconnected"));
1628
2017
  }
1629
2018
  this.pendingRequests.clear();
2019
+ this.teardownResumeListeners();
1630
2020
  await this.connectionManager.disconnect();
1631
2021
  }
1632
2022
  };
2023
+
2024
+ // src/signing/ctx.ts
2025
+ var SCHEME_VERSION_MSG = "QRL-SIGN-MSG-v1";
2026
+ var SCHEME_VERSION_TYPED = "QRL-SIGN-TYPED-v1";
2027
+ var SCHEME_TAG_MSG = new TextEncoder().encode(SCHEME_VERSION_MSG);
2028
+ var SCHEME_TAG_TYPED = new TextEncoder().encode(SCHEME_VERSION_TYPED);
2029
+ var DIGEST_LEN = 64;
2030
+
2031
+ // src/signing/bytes.ts
2032
+ function hexToBytes(hex) {
2033
+ if (typeof hex !== "string") throw new Error("hex must be a string");
2034
+ if (!/^0x([0-9a-fA-F]{2})*$/.test(hex)) {
2035
+ throw new Error("hex must be 0x-prefixed even-length");
2036
+ }
2037
+ const n = (hex.length - 2) / 2;
2038
+ const out = new Uint8Array(n);
2039
+ for (let i = 0; i < n; i++) {
2040
+ out[i] = parseInt(hex.slice(2 + i * 2, 4 + i * 2), 16);
2041
+ }
2042
+ return out;
2043
+ }
2044
+ function bytesToHex(bytes) {
2045
+ if (!(bytes instanceof Uint8Array)) {
2046
+ throw new Error("bytes must be a Uint8Array");
2047
+ }
2048
+ let s = "0x";
2049
+ for (const v of bytes) s += v.toString(16).padStart(2, "0");
2050
+ return s;
2051
+ }
2052
+ function concatBytes(...parts) {
2053
+ return concatBytesArr(parts);
2054
+ }
2055
+ function concatBytesArr(parts) {
2056
+ let total = 0;
2057
+ for (const p of parts) total += p.length;
2058
+ const out = new Uint8Array(total);
2059
+ let off = 0;
2060
+ for (const p of parts) {
2061
+ out.set(p, off);
2062
+ off += p.length;
2063
+ }
2064
+ return out;
2065
+ }
2066
+
2067
+ // src/signing/messageDigest.ts
2068
+ function computeMessageDigest(messageBytes) {
2069
+ if (!(messageBytes instanceof Uint8Array)) {
2070
+ throw new Error("messageBytes must be a Uint8Array");
2071
+ }
2072
+ return shake256Digest(concatBytes(SCHEME_TAG_MSG, messageBytes), DIGEST_LEN);
2073
+ }
2074
+
2075
+ // src/signing/typedData.ts
2076
+ var SLOT = 32;
2077
+ function isMessageObject(v) {
2078
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2079
+ }
2080
+ var ATOMIC_RE = /^(?:(address|bool|string|bytes)|(u?int)(\d+)|bytes(\d+)|(.+?)\[(\d*)\])$/;
2081
+ var MAX_TYPE_DEPTH = 12;
2082
+ function parseFieldType(type, types, depth = 0) {
2083
+ if (depth > MAX_TYPE_DEPTH) throw new Error(`type nesting too deep: ${type}`);
2084
+ if (Object.prototype.hasOwnProperty.call(types, type)) {
2085
+ return { kind: "ref", name: type };
2086
+ }
2087
+ const m = ATOMIC_RE.exec(type);
2088
+ if (!m) throw new Error(`unknown type: ${type}`);
2089
+ const [, atomic, intKind, intWidthStr, bytesWidthStr, innerType, sizeStr] = m;
2090
+ if (atomic === "address" || atomic === "bool" || atomic === "string" || atomic === "bytes") {
2091
+ return { kind: atomic };
2092
+ }
2093
+ if (intKind) {
2094
+ const width = Number(intWidthStr);
2095
+ if (!Number.isInteger(width) || width < 8 || width > 256 || width % 8 !== 0) {
2096
+ throw new Error(`invalid int width: ${type}`);
2097
+ }
2098
+ return { kind: intKind === "uint" ? "uintN" : "intN", width };
2099
+ }
2100
+ if (bytesWidthStr) {
2101
+ const width = Number(bytesWidthStr);
2102
+ if (!Number.isInteger(width) || width < 1 || width > 32) {
2103
+ throw new Error(`invalid bytesN width: ${type}`);
2104
+ }
2105
+ return { kind: "bytesN", width };
2106
+ }
2107
+ if (innerType !== void 0) {
2108
+ parseFieldType(innerType, types, depth + 1);
2109
+ if (sizeStr) {
2110
+ const size = Number(sizeStr);
2111
+ if (!Number.isInteger(size) || size <= 0) {
2112
+ throw new Error(`invalid array size: ${type}`);
2113
+ }
2114
+ return { kind: "array", inner: innerType, size };
2115
+ }
2116
+ return { kind: "array", inner: innerType };
2117
+ }
2118
+ throw new Error(`unhandled type: ${type}`);
2119
+ }
2120
+ function baseTypeName(type) {
2121
+ return type.replace(/(\[\d*\])+$/, "");
2122
+ }
2123
+ function collectDependencies(primary, types) {
2124
+ if (!Object.prototype.hasOwnProperty.call(types, primary)) {
2125
+ throw new Error(`unknown primary type: ${primary}`);
2126
+ }
2127
+ const visited = /* @__PURE__ */ new Set();
2128
+ const visit = (name, path) => {
2129
+ if (path.includes(name)) {
2130
+ throw new Error(`cyclic type reference: ${[...path, name].join(" -> ")}`);
2131
+ }
2132
+ if (visited.has(name)) return;
2133
+ visited.add(name);
2134
+ const def = types[name];
2135
+ if (!def) throw new Error(`unknown type: ${name}`);
2136
+ for (const f of def) {
2137
+ const base = baseTypeName(f.type);
2138
+ if (Object.prototype.hasOwnProperty.call(types, base)) {
2139
+ visit(base, [...path, name]);
2140
+ }
2141
+ parseFieldType(f.type, types);
2142
+ }
2143
+ };
2144
+ visit(primary, []);
2145
+ return visited;
2146
+ }
2147
+ function validateTypeMap(types) {
2148
+ for (const [name, def] of Object.entries(types)) {
2149
+ const defUnknown = def;
2150
+ if (!Array.isArray(defUnknown) || def.length === 0) {
2151
+ throw new Error(`empty or invalid struct: ${name}`);
2152
+ }
2153
+ const seen = /* @__PURE__ */ new Set();
2154
+ for (const f of def) {
2155
+ if (!f || typeof f.name !== "string" || !f.name) {
2156
+ throw new Error(`bad field in ${name}`);
2157
+ }
2158
+ if (typeof f.type !== "string" || !f.type) {
2159
+ throw new Error(`bad field type in ${name}.${f.name}`);
2160
+ }
2161
+ if (seen.has(f.name)) {
2162
+ throw new Error(`duplicate field "${f.name}" in ${name}`);
2163
+ }
2164
+ seen.add(f.name);
2165
+ }
2166
+ }
2167
+ }
2168
+ function encodeType(primary, types) {
2169
+ const deps = collectDependencies(primary, types);
2170
+ const others = [...deps].filter((n) => n !== primary).sort();
2171
+ return [primary, ...others].map((name) => {
2172
+ const fields = types[name];
2173
+ if (!fields) throw new Error(`unknown type: ${name}`);
2174
+ const inner = fields.map((f) => `${f.type} ${f.name}`).join(",");
2175
+ return `${name}(${inner})`;
2176
+ }).join("");
2177
+ }
2178
+ function typeHash(primary, types) {
2179
+ return shake256Digest(new TextEncoder().encode(encodeType(primary, types)), DIGEST_LEN);
2180
+ }
2181
+ function parseQAddress(addr) {
2182
+ if (typeof addr !== "string" || !/^Q[0-9a-fA-F]{40}$/.test(addr)) {
2183
+ throw new Error(`invalid Q-address: ${addr}`);
2184
+ }
2185
+ return hexToBytes("0x" + addr.slice(1).toLowerCase());
2186
+ }
2187
+ function padLeft32(bytes) {
2188
+ if (bytes.length > SLOT) throw new Error("cannot pad: bytes > 32");
2189
+ const out = new Uint8Array(SLOT);
2190
+ out.set(bytes, SLOT - bytes.length);
2191
+ return out;
2192
+ }
2193
+ function padRight32(bytes) {
2194
+ if (bytes.length > SLOT) throw new Error("cannot pad: bytes > 32");
2195
+ const out = new Uint8Array(SLOT);
2196
+ out.set(bytes, 0);
2197
+ return out;
2198
+ }
2199
+ function bigIntToSlot(value, width, signed) {
2200
+ if (signed) {
2201
+ const limit = 1n << BigInt(width - 1);
2202
+ if (value >= limit || value < -limit) {
2203
+ throw new Error(`int${width} out of range: ${value}`);
2204
+ }
2205
+ } else {
2206
+ if (value < 0n) throw new Error(`uint${width} negative: ${value}`);
2207
+ const limit = 1n << BigInt(width);
2208
+ if (value >= limit) throw new Error(`uint${width} out of range: ${value}`);
2209
+ }
2210
+ const slotMask = (1n << 256n) - 1n;
2211
+ const repr = value < 0n ? value + (1n << 256n) & slotMask : value;
2212
+ const out = new Uint8Array(SLOT);
2213
+ let v = repr;
2214
+ for (let i = SLOT - 1; i >= 0; i--) {
2215
+ out[i] = Number(v & 0xffn);
2216
+ v >>= 8n;
2217
+ }
2218
+ return out;
2219
+ }
2220
+ function parseIntValue(v, typeLabel) {
2221
+ if (typeof v === "bigint") return v;
2222
+ if (typeof v === "string") {
2223
+ if (/^-?0x[0-9a-fA-F]+$/i.test(v)) {
2224
+ const isNegative = v.startsWith("-");
2225
+ const abs = BigInt(isNegative ? v.slice(1) : v);
2226
+ return isNegative ? -abs : abs;
2227
+ }
2228
+ if (!/^-?(0|[1-9]\d*)$/.test(v)) {
2229
+ throw new Error(`invalid ${typeLabel} string: ${v}`);
2230
+ }
2231
+ return BigInt(v);
2232
+ }
2233
+ if (typeof v === "number") {
2234
+ if (!Number.isInteger(v) || !Number.isSafeInteger(v)) {
2235
+ throw new Error(`unsafe ${typeLabel} number: ${v}, pass as string`);
2236
+ }
2237
+ return BigInt(v);
2238
+ }
2239
+ throw new Error(`unsupported ${typeLabel} value: ${typeof v}`);
2240
+ }
2241
+ function encodeField(type, value, types) {
2242
+ const parsed = parseFieldType(type, types);
2243
+ switch (parsed.kind) {
2244
+ case "ref":
2245
+ if (!isMessageObject(value)) {
2246
+ throw new Error(`struct field expects object: ${type}`);
2247
+ }
2248
+ return hashStruct(parsed.name, value, types);
2249
+ case "address":
2250
+ if (typeof value !== "string") {
2251
+ throw new Error(`address field expects string: ${typeof value}`);
2252
+ }
2253
+ return padLeft32(parseQAddress(value));
2254
+ case "bool":
2255
+ if (typeof value !== "boolean") {
2256
+ throw new Error(`bool field expects boolean: ${typeof value}`);
2257
+ }
2258
+ return padLeft32(new Uint8Array([value ? 1 : 0]));
2259
+ case "string":
2260
+ if (typeof value !== "string") {
2261
+ throw new Error(`string field expects string: ${typeof value}`);
2262
+ }
2263
+ return shake256Digest(new TextEncoder().encode(value), DIGEST_LEN);
2264
+ case "bytes":
2265
+ if (typeof value !== "string") {
2266
+ throw new Error(`bytes field expects 0x-hex string: ${typeof value}`);
2267
+ }
2268
+ return shake256Digest(hexToBytes(value), DIGEST_LEN);
2269
+ case "uintN":
2270
+ return bigIntToSlot(parseIntValue(value, `uint${parsed.width}`), parsed.width, false);
2271
+ case "intN":
2272
+ return bigIntToSlot(parseIntValue(value, `int${parsed.width}`), parsed.width, true);
2273
+ case "bytesN": {
2274
+ if (typeof value !== "string") {
2275
+ throw new Error(`bytes${parsed.width} expects 0x-hex string: ${typeof value}`);
2276
+ }
2277
+ const raw = hexToBytes(value);
2278
+ if (raw.length !== parsed.width) {
2279
+ throw new Error(`bytes${parsed.width} requires ${parsed.width} bytes, got ${raw.length}`);
2280
+ }
2281
+ return padRight32(raw);
2282
+ }
2283
+ case "array": {
2284
+ if (!Array.isArray(value)) {
2285
+ throw new Error(`array field expects array: ${typeof value}`);
2286
+ }
2287
+ if (parsed.size !== void 0 && value.length !== parsed.size) {
2288
+ throw new Error(`fixed array ${type} requires length ${parsed.size}, got ${value.length}`);
2289
+ }
2290
+ const chunks = value.map((v) => encodeField(parsed.inner, v, types));
2291
+ return shake256Digest(concatBytesArr(chunks), DIGEST_LEN);
2292
+ }
2293
+ }
2294
+ }
2295
+ function hashStruct(primary, data, types) {
2296
+ const fields = types[primary];
2297
+ if (!fields) throw new Error(`unknown struct: ${primary}`);
2298
+ const expected = new Set(fields.map((f) => f.name));
2299
+ for (const k of Object.keys(data)) {
2300
+ if (!expected.has(k)) throw new Error(`unknown field in ${primary}: ${k}`);
2301
+ }
2302
+ const parts = [typeHash(primary, types)];
2303
+ for (const f of fields) {
2304
+ if (!(f.name in data)) throw new Error(`missing field ${primary}.${f.name}`);
2305
+ parts.push(encodeField(f.type, data[f.name], types));
2306
+ }
2307
+ return shake256Digest(concatBytesArr(parts), DIGEST_LEN);
2308
+ }
2309
+ var RESERVED_DOMAIN_FIELDS = {
2310
+ name: "string",
2311
+ version: "string",
2312
+ chainId: "uint256",
2313
+ verifyingContract: "address",
2314
+ salt: "bytes32"
2315
+ };
2316
+ function validateDomainTypes(types) {
2317
+ const def = types.QRLDomain;
2318
+ if (!def) throw new Error("QRLDomain type is required");
2319
+ let hasName = false;
2320
+ for (const f of def) {
2321
+ const expected = RESERVED_DOMAIN_FIELDS[f.name];
2322
+ if (!expected) {
2323
+ throw new Error(
2324
+ `QRLDomain field "${f.name}" not in reserved set (name, version, chainId, verifyingContract, salt)`
2325
+ );
2326
+ }
2327
+ if (f.type !== expected) {
2328
+ throw new Error(`QRLDomain field "${f.name}" must be type "${expected}", got "${f.type}"`);
2329
+ }
2330
+ if (f.name === "name") hasName = true;
2331
+ }
2332
+ if (!hasName) throw new Error("QRLDomain.name is required");
2333
+ }
2334
+ function validatePayloadReachability(primary, types) {
2335
+ const reachable = /* @__PURE__ */ new Set();
2336
+ for (const root of ["QRLDomain", primary]) {
2337
+ for (const t of collectDependencies(root, types)) reachable.add(t);
2338
+ }
2339
+ for (const k of Object.keys(types)) {
2340
+ if (!reachable.has(k)) throw new Error(`unused referenced type: ${k}`);
2341
+ }
2342
+ }
2343
+ function computeTypedDataDigest(payload) {
2344
+ if (!payload || typeof payload !== "object") {
2345
+ throw new Error("invalid typed data payload");
2346
+ }
2347
+ validateTypeMap(payload.types);
2348
+ validateDomainTypes(payload.types);
2349
+ validatePayloadReachability(payload.primaryType, payload.types);
2350
+ const domainHash = hashStruct("QRLDomain", payload.domain, payload.types);
2351
+ const messageHash = hashStruct(payload.primaryType, payload.message, payload.types);
2352
+ return shake256Digest(concatBytes(SCHEME_TAG_TYPED, domainHash, messageHash), DIGEST_LEN);
2353
+ }
2354
+
2355
+ // src/signing/verify.ts
2356
+ function bytesOrHex(v) {
2357
+ if (v instanceof Uint8Array) return v;
2358
+ if (typeof v === "string") return hexToBytes(v);
2359
+ throw new Error("expected Uint8Array or 0x-hex string");
2360
+ }
2361
+ function verifyMessage({
2362
+ signature,
2363
+ publicKey,
2364
+ messageBytes
2365
+ }) {
2366
+ try {
2367
+ const sig = bytesOrHex(signature);
2368
+ const pk = bytesOrHex(publicKey);
2369
+ const msg = bytesOrHex(messageBytes);
2370
+ const digest = computeMessageDigest(msg);
2371
+ return mldsaVerify(sig, digest, pk, SCHEME_TAG_MSG);
2372
+ } catch {
2373
+ return false;
2374
+ }
2375
+ }
2376
+ function verifyTypedData({ signature, publicKey, payload }) {
2377
+ try {
2378
+ const sig = bytesOrHex(signature);
2379
+ const pk = bytesOrHex(publicKey);
2380
+ const digest = computeTypedDataDigest(payload);
2381
+ return mldsaVerify(sig, digest, pk, SCHEME_TAG_TYPED);
2382
+ } catch {
2383
+ return false;
2384
+ }
2385
+ }
1633
2386
  // Annotate the CommonJS export names for ESM import in node:
1634
2387
  0 && (module.exports = {
1635
2388
  BLOB_LEN,
@@ -1637,6 +2390,7 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
1637
2390
  ConnectionManager,
1638
2391
  ConnectionStatus,
1639
2392
  DEFAULT_RELAY_URL,
2393
+ DIGEST_LEN,
1640
2394
  FP_LEN,
1641
2395
  KeyExchange,
1642
2396
  KeyExchangeMessageType,
@@ -1644,16 +2398,32 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
1644
2398
  PROTOCOL_VERSION,
1645
2399
  QRLConnect,
1646
2400
  QRLConnectProvider,
2401
+ QRL_CONNECT_PROVIDER_INFO,
1647
2402
  RESTRICTED_METHODS,
2403
+ SCHEME_TAG_MSG,
2404
+ SCHEME_TAG_TYPED,
2405
+ SCHEME_VERSION_MSG,
2406
+ SCHEME_VERSION_TYPED,
1648
2407
  SocketClient,
1649
2408
  UNRESTRICTED_METHODS,
2409
+ bytesToHex,
1650
2410
  cidFromString,
1651
2411
  cidRandom,
1652
2412
  cidToString,
1653
2413
  computeFingerprint,
2414
+ computeMessageDigest,
2415
+ computeTypedDataDigest,
2416
+ concatBytes,
2417
+ encodeField,
2418
+ encodeType,
1654
2419
  fingerprintEquals,
1655
2420
  generateConnectionURI,
1656
2421
  getAppStoreUrl,
2422
+ hashStruct,
2423
+ hexToBytes,
1657
2424
  isMobileBrowser,
1658
- parseConnectionURI
2425
+ parseConnectionURI,
2426
+ typeHash,
2427
+ verifyMessage,
2428
+ verifyTypedData
1659
2429
  });