@qrlwallet/connect 0.1.0 → 3.0.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
@@ -5,94 +5,153 @@ import EventEmitter4 from "eventemitter3";
5
5
  import EventEmitter3 from "eventemitter3";
6
6
  import { v4 as uuidv4 } from "uuid";
7
7
 
8
- // src/ECIESClient.ts
9
- import { PrivateKey, decrypt, encrypt } from "eciesjs";
8
+ // src/KeyExchange.ts
9
+ import EventEmitter from "eventemitter3";
10
10
 
11
- // src/utils/logger.ts
12
- var debugEnabled = false;
13
- function setDebug(enabled) {
14
- debugEnabled = enabled;
11
+ // src/PQCrypto.ts
12
+ 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() {
21
+ const c = globalThis.crypto;
22
+ if (!c || !c.subtle) {
23
+ throw new Error("PQCrypto: WebCrypto SubtleCrypto is not available in this environment");
24
+ }
25
+ return c.subtle;
15
26
  }
16
- function log(tag, ...args) {
17
- if (debugEnabled) {
18
- console.log(`[QRLConnect:${tag}]`, ...args);
19
- }
27
+ function bs(u) {
28
+ return u;
20
29
  }
21
- function warn(tag, ...args) {
22
- console.warn(`[QRLConnect:${tag}]`, ...args);
30
+ function kemKeygen() {
31
+ const { publicKey, secretKey } = ml_kem768.keygen();
32
+ return { pk: publicKey, sk: secretKey };
23
33
  }
24
- function error(tag, ...args) {
25
- console.error(`[QRLConnect:${tag}]`, ...args);
34
+ function kemEncaps(pk) {
35
+ const { cipherText, sharedSecret } = ml_kem768.encapsulate(pk);
36
+ return { ct: cipherText, ss: sharedSecret };
37
+ }
38
+ function kemDecaps(sk, ct) {
39
+ return ml_kem768.decapsulate(ct, sk);
40
+ }
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)));
44
+ }
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);
48
+ return subtle().deriveKey(
49
+ { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(32), info: bs(info) },
50
+ ikm,
51
+ { name: "AES-GCM", length: 256 },
52
+ true,
53
+ ["encrypt", "decrypt"]
54
+ );
55
+ }
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
+ }
60
+ return subtle().importKey("raw", bs(raw), { name: "AES-GCM", length: 256 }, true, [
61
+ "encrypt",
62
+ "decrypt"
63
+ ]);
64
+ }
65
+ async function exportRawAeadKey(key) {
66
+ const buf = await subtle().exportKey("raw", key);
67
+ return new Uint8Array(buf);
68
+ }
69
+ function nonce(dir, seq) {
70
+ if (dir.length !== 4) {
71
+ throw new Error("PQCrypto: direction tag must be 4 bytes");
72
+ }
73
+ const n = new Uint8Array(12);
74
+ n.set(dir, 0);
75
+ new DataView(n.buffer, n.byteOffset, n.byteLength).setBigUint64(4, BigInt(seq), true);
76
+ return n;
77
+ }
78
+ function aad(htx, seq) {
79
+ const out = new Uint8Array(htx.length + 8);
80
+ out.set(htx, 0);
81
+ new DataView(out.buffer, out.byteOffset, out.byteLength).setBigUint64(
82
+ htx.length,
83
+ BigInt(seq),
84
+ true
85
+ );
86
+ return out;
87
+ }
88
+ 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);
95
+ }
96
+ 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);
103
+ }
104
+ function zeroize(b) {
105
+ b.fill(0);
106
+ }
107
+ function concat(...parts) {
108
+ let total = 0;
109
+ for (const p of parts) total += p.length;
110
+ const out = new Uint8Array(total);
111
+ let offset = 0;
112
+ for (const p of parts) {
113
+ out.set(p, offset);
114
+ offset += p.length;
115
+ }
116
+ return out;
26
117
  }
27
-
28
- // src/ECIESClient.ts
29
118
  function toBase64(bytes) {
30
- let binary = "";
31
- for (let i = 0; i < bytes.length; i++) {
32
- binary += String.fromCharCode(bytes[i]);
119
+ const CHUNK = 32768;
120
+ let bin = "";
121
+ for (let i = 0; i < bytes.length; i += CHUNK) {
122
+ const slice = bytes.subarray(i, i + CHUNK);
123
+ bin += String.fromCharCode.apply(null, slice);
33
124
  }
34
- return btoa(binary);
125
+ return btoa(bin);
35
126
  }
36
- function fromBase64(base64) {
37
- const binary = atob(base64);
38
- const bytes = new Uint8Array(binary.length);
39
- for (let i = 0; i < binary.length; i++) {
40
- bytes[i] = binary.charCodeAt(i);
41
- }
42
- return bytes;
127
+ function fromBase64(b64) {
128
+ const bin = atob(b64);
129
+ const out = new Uint8Array(bin.length);
130
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
131
+ return out;
132
+ }
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;
43
138
  }
44
- var ECIESClient = class {
45
- constructor(existingPrivateKey) {
46
- if (existingPrivateKey) {
47
- this.privateKey = PrivateKey.fromHex(existingPrivateKey);
48
- log("ECIES", "Restored from existing private key");
49
- } else {
50
- this.privateKey = new PrivateKey();
51
- log("ECIES", "Generated new ECIES keypair");
52
- }
53
- }
54
- /** Get public key as hex string */
55
- getPublicKey() {
56
- return this.privateKey.publicKey.toHex();
57
- }
58
- /** Get private key as hex string (for session persistence) */
59
- getPrivateKeyHex() {
60
- return this.privateKey.toHex();
61
- }
62
- /** Encrypt a string message for the given public key. Returns base64. */
63
- encrypt(data, otherPublicKey) {
64
- const encoded = new TextEncoder().encode(data);
65
- const encrypted = encrypt(otherPublicKey, encoded);
66
- return toBase64(encrypted);
67
- }
68
- /** Decrypt a base64-encoded message using our private key. */
69
- decrypt(encryptedBase64) {
70
- const encryptedBytes = fromBase64(encryptedBase64);
71
- const decrypted = decrypt(this.privateKey.toHex(), encryptedBytes);
72
- return new TextDecoder().decode(decrypted);
73
- }
74
- };
75
-
76
- // src/KeyExchange.ts
77
- import EventEmitter from "eventemitter3";
78
139
 
79
140
  // src/config.ts
80
141
  var DEFAULT_RELAY_URL = "https://qrlwallet.com";
81
142
  var RELAY_PATH = "/relay";
82
- var PROTOCOL_VERSION = 1;
143
+ var PROTOCOL_VERSION = 2;
83
144
  var SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
84
145
  var STORAGE_KEY_PREFIX = "@qrlwallet/connect";
85
146
  var REQUEST_TIMEOUT_MS = 5 * 60 * 1e3;
86
147
  var WALLET_UNRESPONSIVE_MS = 30 * 1e3;
148
+ var RECONNECT_WALLET_PROBE_MS = 12 * 1e3;
87
149
  var RESTRICTED_METHODS = /* @__PURE__ */ new Set([
88
150
  "qrl_requestAccounts",
89
151
  "qrl_sendTransaction",
90
152
  "qrl_signTransaction",
91
- "qrl_sign",
92
- "personal_sign",
153
+ "qrl_signMessage",
93
154
  "qrl_signTypedData",
94
- "qrl_signTypedData_v3",
95
- "qrl_signTypedData_v4",
96
155
  "wallet_addQrlChain",
97
156
  "wallet_switchQrlChain"
98
157
  ]);
@@ -165,154 +224,300 @@ var ConnectionStatus = /* @__PURE__ */ ((ConnectionStatus2) => {
165
224
  return ConnectionStatus2;
166
225
  })(ConnectionStatus || {});
167
226
 
227
+ // src/utils/logger.ts
228
+ var debugEnabled = false;
229
+ function setDebug(enabled) {
230
+ debugEnabled = enabled;
231
+ }
232
+ function log(tag, ...args) {
233
+ if (debugEnabled) {
234
+ console.log(`[QRLConnect:${tag}]`, ...args);
235
+ }
236
+ }
237
+ function warn(tag, ...args) {
238
+ console.warn(`[QRLConnect:${tag}]`, ...args);
239
+ }
240
+ function error(tag, ...args) {
241
+ console.error(`[QRLConnect:${tag}]`, ...args);
242
+ }
243
+
168
244
  // src/KeyExchange.ts
245
+ var textEncoder2 = new TextEncoder();
246
+ var textDecoder = new TextDecoder();
247
+ var HELLO_WALLET = textEncoder2.encode("hello/wallet/v1");
248
+ var HELLO_DAPP = textEncoder2.encode("hello/dapp/v1");
169
249
  var KeyExchange = class extends EventEmitter {
170
- constructor(ecies, isOriginator, otherPublicKey) {
250
+ constructor(isOriginator, restored) {
171
251
  super();
172
- this.otherPublicKey = null;
173
- this.keysExchanged = false;
252
+ this.keypair = null;
253
+ this.session = null;
254
+ this.step = "key_handshake_SYN" /* SYN */;
174
255
  this.awaitingSynAck = false;
175
256
  this.awaitingAck = false;
176
- this.step = "key_handshake_SYN" /* SYN */;
177
- this.ecies = ecies;
257
+ this.keysExchanged = false;
178
258
  this.isOriginator = isOriginator;
179
- if (otherPublicKey) {
180
- this.otherPublicKey = otherPublicKey;
259
+ if (restored) {
260
+ this.session = restored;
181
261
  this.keysExchanged = true;
182
- log("KeyExchange", "Restored with existing other public key");
262
+ this.step = "key_handshake_ACK" /* ACK */;
263
+ log("KeyExchange", "Hydrated from persisted session");
183
264
  }
184
265
  }
185
- /** Get the SYN message to start the handshake (originator/dApp side). */
186
- createSYN() {
266
+ /**
267
+ * Originator (dApp) side: begin a fresh handshake by generating an
268
+ * ephemeral ML-KEM-768 keypair. Returns pk to embed in the QR URI.
269
+ */
270
+ initiate() {
271
+ if (!this.isOriginator) {
272
+ throw new Error("KeyExchange: responder cannot initiate");
273
+ }
274
+ this.resetInternal(false);
275
+ this.keypair = kemKeygen();
187
276
  this.step = "key_handshake_SYN" /* SYN */;
188
277
  this.awaitingSynAck = true;
189
278
  this.emit("step_change", this.step);
279
+ return this.keypair.pk;
280
+ }
281
+ /**
282
+ * Originator: handle a SYNACK received over the relay.
283
+ * Returns an ACK wire message to send, or null if the SYNACK was a
284
+ * duplicate (handshake already complete).
285
+ */
286
+ async onSynAck(cid, msg) {
287
+ if (!this.isOriginator) {
288
+ warn("KeyExchange", "Responder received SYNACK \u2014 ignoring");
289
+ return null;
290
+ }
291
+ if (!this.awaitingSynAck || !this.keypair) {
292
+ return null;
293
+ }
294
+ this.awaitingSynAck = false;
295
+ const ct = fromBase64(msg.ct);
296
+ const c0 = fromBase64(msg.c0);
297
+ if (ct.length !== ML_KEM_768_CT_LEN) {
298
+ throw new Error(`KeyExchange: bad ct length ${ct.length}`);
299
+ }
300
+ const ss = kemDecaps(this.keypair.sk, ct);
301
+ const htx = await transcriptHash(cid, this.keypair.pk, ct);
302
+ const key = await deriveAeadKey(ss, htx);
303
+ let hello;
304
+ try {
305
+ hello = await open(key, DIR_WALLET_TX, 0, htx, c0);
306
+ } catch {
307
+ throw new Error(
308
+ "KeyExchange: wallet hello AEAD tag failed (tampered SYNACK, wrong QR, or protocol mismatch)"
309
+ );
310
+ }
311
+ if (!constantTimeEquals(hello, HELLO_WALLET)) {
312
+ throw new Error("KeyExchange: wallet hello mismatch");
313
+ }
314
+ zeroize(ss);
315
+ zeroize(this.keypair.sk);
316
+ this.keypair = null;
317
+ const c1 = await seal(key, DIR_DAPP_TX, 0, htx, HELLO_DAPP);
318
+ this.session = {
319
+ cid,
320
+ key,
321
+ htx,
322
+ sendDir: DIR_DAPP_TX,
323
+ recvDir: DIR_WALLET_TX,
324
+ sendSeq: 1,
325
+ recvSeq: 1
326
+ };
327
+ this.keysExchanged = true;
328
+ this.step = "key_handshake_ACK" /* ACK */;
329
+ this.emit("keys_exchanged");
330
+ this.emit("step_change", this.step);
190
331
  return {
191
- type: "key_handshake_SYN" /* SYN */,
192
- pubkey: this.ecies.getPublicKey(),
332
+ type: "key_handshake_ACK" /* ACK */,
333
+ c1: toBase64(c1),
193
334
  v: PROTOCOL_VERSION
194
335
  };
195
336
  }
196
337
  /**
197
- * Process an incoming key exchange message.
198
- * Returns a response message to send back, or null if handshake is complete.
338
+ * Responder (wallet) side: begin handshake from a scanned QR.
339
+ * Returns the SYNACK wire message to emit on the relay.
199
340
  */
200
- onMessage(message) {
201
- log("KeyExchange", `Received ${message.type}, isOriginator=${this.isOriginator}`);
202
- switch (message.type) {
203
- case "key_handshake_SYN" /* SYN */: {
204
- if (this.isOriginator) {
205
- warn("KeyExchange", "Originator received SYN - ignoring");
206
- return null;
207
- }
208
- if (message.pubkey) {
209
- this.otherPublicKey = message.pubkey;
210
- }
211
- this.step = "key_handshake_SYNACK" /* SYNACK */;
212
- this.awaitingAck = true;
213
- this.emit("step_change", this.step);
214
- return {
215
- type: "key_handshake_SYNACK" /* SYNACK */,
216
- pubkey: this.ecies.getPublicKey(),
217
- v: PROTOCOL_VERSION
218
- };
219
- }
220
- case "key_handshake_SYNACK" /* SYNACK */: {
221
- if (!this.isOriginator) {
222
- warn("KeyExchange", "Non-originator received SYNACK - ignoring");
223
- return null;
224
- }
225
- if (message.pubkey) {
226
- this.otherPublicKey = message.pubkey;
227
- }
228
- if (!this.awaitingSynAck) {
229
- return {
230
- type: "key_handshake_ACK" /* ACK */,
231
- v: PROTOCOL_VERSION
232
- };
233
- }
234
- this.awaitingSynAck = false;
235
- this.step = "key_handshake_ACK" /* ACK */;
236
- if (!this.keysExchanged) {
237
- this.keysExchanged = true;
238
- this.emit("keys_exchanged");
239
- }
240
- this.emit("step_change", this.step);
241
- return {
242
- type: "key_handshake_ACK" /* ACK */,
243
- v: PROTOCOL_VERSION
244
- };
245
- }
246
- case "key_handshake_ACK" /* ACK */: {
247
- if (this.isOriginator) {
248
- warn("KeyExchange", "Originator received ACK - ignoring");
249
- return null;
250
- }
251
- if (!this.awaitingAck) {
252
- return null;
253
- }
254
- this.awaitingAck = false;
255
- if (!this.keysExchanged) {
256
- this.keysExchanged = true;
257
- this.emit("keys_exchanged");
258
- }
259
- log("KeyExchange", "Key exchange complete (wallet side)");
260
- return null;
261
- }
262
- default:
263
- warn("KeyExchange", `Unknown key exchange type: ${message.type}`);
264
- return null;
341
+ async receiveQR(cid, pk) {
342
+ if (this.isOriginator) {
343
+ throw new Error("KeyExchange: originator cannot consume a QR");
265
344
  }
345
+ this.resetInternal(false);
346
+ const { ct, ss } = kemEncaps(pk);
347
+ const htx = await transcriptHash(cid, pk, ct);
348
+ const key = await deriveAeadKey(ss, htx);
349
+ const c0 = await seal(key, DIR_WALLET_TX, 0, htx, HELLO_WALLET);
350
+ zeroize(ss);
351
+ this.session = {
352
+ cid,
353
+ key,
354
+ htx,
355
+ sendDir: DIR_WALLET_TX,
356
+ recvDir: DIR_DAPP_TX,
357
+ sendSeq: 1,
358
+ recvSeq: 1
359
+ };
360
+ this.awaitingAck = true;
361
+ this.step = "key_handshake_SYNACK" /* SYNACK */;
362
+ this.emit("step_change", this.step);
363
+ return {
364
+ type: "key_handshake_SYNACK" /* SYNACK */,
365
+ ct: toBase64(ct),
366
+ c0: toBase64(c0),
367
+ v: PROTOCOL_VERSION
368
+ };
266
369
  }
267
- /** Encrypt a message for the counterparty. */
268
- encryptMessage(data) {
269
- if (!this.otherPublicKey) {
270
- throw new Error("Cannot encrypt: key exchange not complete");
370
+ /**
371
+ * Responder: verify an incoming ACK and finalize the session.
372
+ * Duplicates are silently ignored (handshake is idempotent).
373
+ */
374
+ async onAck(msg) {
375
+ if (this.isOriginator) {
376
+ warn("KeyExchange", "Originator received ACK \u2014 ignoring");
377
+ return;
271
378
  }
272
- return this.ecies.encrypt(data, this.otherPublicKey);
273
- }
274
- /** Decrypt a message from the counterparty. */
275
- decryptMessage(encryptedBase64) {
276
- return this.ecies.decrypt(encryptedBase64);
277
- }
278
- /** Reset key exchange state for a fresh handshake (e.g. new QR code). */
379
+ if (!this.awaitingAck) {
380
+ return;
381
+ }
382
+ if (!this.session) {
383
+ throw new Error("KeyExchange: onAck called without a session");
384
+ }
385
+ this.awaitingAck = false;
386
+ const c1 = fromBase64(msg.c1);
387
+ let hello;
388
+ try {
389
+ hello = await open(this.session.key, DIR_DAPP_TX, 0, this.session.htx, c1);
390
+ } catch {
391
+ throw new Error("KeyExchange: dApp hello AEAD tag failed");
392
+ }
393
+ if (!constantTimeEquals(hello, HELLO_DAPP)) {
394
+ throw new Error("KeyExchange: dApp hello mismatch");
395
+ }
396
+ this.keysExchanged = true;
397
+ this.step = "key_handshake_ACK" /* ACK */;
398
+ this.emit("keys_exchanged");
399
+ }
400
+ /** Encrypt a string for the counterparty. Returns base64. */
401
+ async encryptMessage(data) {
402
+ if (!this.session) {
403
+ throw new Error("KeyExchange: cannot encrypt \u2014 session not established");
404
+ }
405
+ const pt = textEncoder2.encode(data);
406
+ const ct = await seal(
407
+ this.session.key,
408
+ this.session.sendDir,
409
+ this.session.sendSeq,
410
+ this.session.htx,
411
+ pt
412
+ );
413
+ this.session.sendSeq++;
414
+ return toBase64(ct);
415
+ }
416
+ /** Decrypt a base64 ciphertext from the counterparty. */
417
+ async decryptMessage(b64) {
418
+ if (!this.session) {
419
+ throw new Error("KeyExchange: cannot decrypt \u2014 session not established");
420
+ }
421
+ const ct = fromBase64(b64);
422
+ const pt = await open(
423
+ this.session.key,
424
+ this.session.recvDir,
425
+ this.session.recvSeq,
426
+ this.session.htx,
427
+ ct
428
+ );
429
+ this.session.recvSeq++;
430
+ return textDecoder.decode(pt);
431
+ }
432
+ /** Reset all state for a fresh handshake. Zeroes any live secret-key buffer. */
279
433
  reset() {
280
- this.otherPublicKey = null;
434
+ this.resetInternal(true);
435
+ }
436
+ resetInternal(emit) {
437
+ if (this.keypair) {
438
+ zeroize(this.keypair.sk);
439
+ this.keypair = null;
440
+ }
441
+ this.session = null;
281
442
  this.keysExchanged = false;
282
443
  this.awaitingSynAck = false;
283
444
  this.awaitingAck = false;
284
445
  this.step = "key_handshake_SYN" /* SYN */;
285
- log("KeyExchange", "Reset for fresh handshake");
446
+ if (emit) this.emit("step_change", this.step);
286
447
  }
287
448
  areKeysExchanged() {
288
449
  return this.keysExchanged;
289
450
  }
290
- getOtherPublicKey() {
291
- return this.otherPublicKey;
451
+ getSession() {
452
+ return this.session;
292
453
  }
293
454
  getCurrentStep() {
294
455
  return this.step;
295
456
  }
457
+ /** Export the current session as a JSON-safe persisted record. */
458
+ async exportPersisted() {
459
+ if (!this.session) return null;
460
+ return {
461
+ cid: toBase64(this.session.cid),
462
+ kAeadRaw: toBase64(await exportRawAeadKey(this.session.key)),
463
+ htx: toBase64(this.session.htx),
464
+ sendDir: toBase64(this.session.sendDir),
465
+ recvDir: toBase64(this.session.recvDir),
466
+ sendSeq: this.session.sendSeq,
467
+ recvSeq: this.session.recvSeq
468
+ };
469
+ }
470
+ /** Rehydrate a Session from its persisted form. */
471
+ static async sessionFromPersisted(p) {
472
+ const key = await importRawAeadKey(fromBase64(p.kAeadRaw));
473
+ return {
474
+ cid: fromBase64(p.cid),
475
+ key,
476
+ htx: fromBase64(p.htx),
477
+ sendDir: fromBase64(p.sendDir),
478
+ recvDir: fromBase64(p.recvDir),
479
+ sendSeq: p.sendSeq,
480
+ recvSeq: p.recvSeq
481
+ };
482
+ }
296
483
  };
297
484
 
298
485
  // src/SocketClient.ts
299
486
  import { io } from "socket.io-client";
300
487
  import EventEmitter2 from "eventemitter3";
488
+ var PENDING_JOIN_TIMEOUT_MS = 2e4;
301
489
  var SocketClient = class extends EventEmitter2 {
302
490
  constructor(relayUrl, clientType) {
303
491
  super();
304
492
  this.socket = null;
305
493
  this.channelId = null;
494
+ // The dApp's own PK (base64) that must be uploaded with every join
495
+ // attempt so the relay can bind it to the channel. Wallet-side leaves
496
+ // this undefined.
497
+ this.publicKeyBase64 = null;
306
498
  this.seq = 0;
307
499
  this.pendingJoin = null;
308
500
  this.relayUrl = relayUrl;
309
501
  this.clientType = clientType;
310
502
  }
503
+ /**
504
+ * dApp-only: stash the PK so it gets uploaded on every join_channel
505
+ * attempt (including auto-rejoins after reconnect).
506
+ */
507
+ setPublicKey(publicKeyBase64) {
508
+ if (this.clientType !== "dapp") {
509
+ throw new Error("SocketClient.setPublicKey is only valid for dApp clients");
510
+ }
511
+ this.publicKeyBase64 = publicKeyBase64;
512
+ }
311
513
  /**
312
514
  * Connect to the relay server.
313
515
  */
314
516
  connect() {
315
- if (this.socket?.connected) return;
517
+ if (this.socket) {
518
+ if (!this.socket.connected) this.socket.connect();
519
+ return;
520
+ }
316
521
  log("Socket", `Connecting to ${this.relayUrl}`);
317
522
  this.socket = io(this.relayUrl, {
318
523
  path: RELAY_PATH,
@@ -329,17 +534,11 @@ var SocketClient = class extends EventEmitter2 {
329
534
  if (this.channelId) {
330
535
  const reconnectChannelId = this.channelId;
331
536
  this.joinChannelNow(reconnectChannelId).then((result) => {
332
- if (this.pendingJoin && this.pendingJoin.channelId === reconnectChannelId) {
333
- this.pendingJoin.resolve(result);
334
- this.pendingJoin = null;
335
- }
336
- this.emit("reconnected");
537
+ this.settlePendingJoin(reconnectChannelId, { result });
538
+ this.emit("reconnected", result);
337
539
  }).catch((err) => {
338
540
  const reconnectErr = err instanceof Error ? err : new Error(String(err));
339
- if (this.pendingJoin && this.pendingJoin.channelId === reconnectChannelId) {
340
- this.pendingJoin.reject(reconnectErr);
341
- this.pendingJoin = null;
342
- }
541
+ this.settlePendingJoin(reconnectChannelId, { error: reconnectErr });
343
542
  error("Socket", `Rejoin failed: ${reconnectErr.message}`);
344
543
  this.emit("error", reconnectErr);
345
544
  });
@@ -363,7 +562,24 @@ var SocketClient = class extends EventEmitter2 {
363
562
  });
364
563
  }
365
564
  /**
366
- * Join a relay channel.
565
+ * Resolve or reject the currently-pending `joinChannel` promise, if it
566
+ * matches the given channelId. Clears the watchdog timer on settle so
567
+ * it doesn't fire after the fact.
568
+ */
569
+ settlePendingJoin(channelId, outcome) {
570
+ if (!this.pendingJoin || this.pendingJoin.channelId !== channelId) return;
571
+ clearTimeout(this.pendingJoin.watchdog);
572
+ const pending = this.pendingJoin;
573
+ this.pendingJoin = null;
574
+ if ("result" in outcome) {
575
+ pending.resolve(outcome.result);
576
+ } else {
577
+ pending.reject(outcome.error);
578
+ }
579
+ }
580
+ /**
581
+ * Join a relay channel. Resolves with any buffered messages plus the
582
+ * dApp's public key bound to the channel (for wallet joins).
367
583
  */
368
584
  async joinChannel(channelId) {
369
585
  if (!this.socket) {
@@ -377,6 +593,7 @@ var SocketClient = class extends EventEmitter2 {
377
593
  if (this.pendingJoin.channelId === channelId) {
378
594
  return this.pendingJoin.promise;
379
595
  }
596
+ clearTimeout(this.pendingJoin.watchdog);
380
597
  this.pendingJoin.reject(new Error("Join request superseded by a newer channel"));
381
598
  this.pendingJoin = null;
382
599
  }
@@ -386,11 +603,17 @@ var SocketClient = class extends EventEmitter2 {
386
603
  resolvePending = resolve;
387
604
  rejectPending = reject;
388
605
  });
606
+ const watchdog = setTimeout(() => {
607
+ this.settlePendingJoin(channelId, {
608
+ error: new Error(`joinChannel timed out after ${PENDING_JOIN_TIMEOUT_MS}ms`)
609
+ });
610
+ }, PENDING_JOIN_TIMEOUT_MS);
389
611
  this.pendingJoin = {
390
612
  channelId,
391
613
  promise,
392
614
  resolve: resolvePending,
393
- reject: rejectPending
615
+ reject: rejectPending,
616
+ watchdog
394
617
  };
395
618
  return promise;
396
619
  }
@@ -400,13 +623,22 @@ var SocketClient = class extends EventEmitter2 {
400
623
  reject(new Error("Socket not connected"));
401
624
  return;
402
625
  }
626
+ const payload = { channelId, clientType: this.clientType };
627
+ if (this.clientType === "dapp" && this.publicKeyBase64) {
628
+ payload.publicKey = this.publicKeyBase64;
629
+ }
403
630
  this.socket.emit(
404
631
  "join_channel",
405
- { channelId, clientType: this.clientType },
632
+ payload,
406
633
  (response) => {
407
634
  if (response.success) {
408
635
  log("Socket", `Joined channel ${channelId}`);
409
- resolve({ bufferedMessages: response.bufferedMessages || [] });
636
+ resolve({
637
+ bufferedMessages: response.bufferedMessages || [],
638
+ channelPublicKey: response.channelPublicKey ?? null,
639
+ participants: response.participants || [],
640
+ terminated: response.terminated === true
641
+ });
410
642
  } else {
411
643
  error("Socket", `Failed to join channel: ${response.error}`);
412
644
  reject(new Error(response.error || "Failed to join channel"));
@@ -443,6 +675,7 @@ var SocketClient = class extends EventEmitter2 {
443
675
  */
444
676
  leaveChannel() {
445
677
  if (this.pendingJoin) {
678
+ clearTimeout(this.pendingJoin.watchdog);
446
679
  this.pendingJoin.reject(new Error("Channel left before join completed"));
447
680
  this.pendingJoin = null;
448
681
  }
@@ -456,6 +689,7 @@ var SocketClient = class extends EventEmitter2 {
456
689
  */
457
690
  disconnect() {
458
691
  if (this.pendingJoin) {
692
+ clearTimeout(this.pendingJoin.watchdog);
459
693
  this.pendingJoin.reject(new Error("Socket disconnected before join completed"));
460
694
  this.pendingJoin = null;
461
695
  }
@@ -475,111 +709,267 @@ var SocketClient = class extends EventEmitter2 {
475
709
  }
476
710
  };
477
711
 
712
+ // src/utils/base45.ts
713
+ var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
714
+ var DECODE = (() => {
715
+ const table = new Int8Array(128).fill(-1);
716
+ for (let i = 0; i < ALPHABET.length; i++) {
717
+ table[ALPHABET.charCodeAt(i)] = i;
718
+ }
719
+ return table;
720
+ })();
721
+ function decodeChar(charCode) {
722
+ if (charCode < 0 || charCode >= 128) return -1;
723
+ return DECODE[charCode];
724
+ }
725
+ function base45Encode(bytes) {
726
+ const n = bytes.length;
727
+ let out = "";
728
+ let i = 0;
729
+ while (i + 2 <= n) {
730
+ const v = bytes[i] << 8 | bytes[i + 1];
731
+ const c2 = Math.floor(v / 2025);
732
+ const r = v - c2 * 2025;
733
+ const c1 = Math.floor(r / 45);
734
+ const c0 = r - c1 * 45;
735
+ out += ALPHABET[c0] + ALPHABET[c1] + ALPHABET[c2];
736
+ i += 2;
737
+ }
738
+ if (i < n) {
739
+ const v = bytes[i];
740
+ const c1 = Math.floor(v / 45);
741
+ const c0 = v - c1 * 45;
742
+ out += ALPHABET[c0] + ALPHABET[c1];
743
+ }
744
+ return out;
745
+ }
746
+ function base45Decode(s) {
747
+ const len = s.length;
748
+ if (len === 0) return new Uint8Array(0);
749
+ const full = Math.floor(len / 3);
750
+ const tail = len - full * 3;
751
+ if (tail !== 0 && tail !== 2) {
752
+ throw new Error("base45: invalid input length");
753
+ }
754
+ const outLen = full * 2 + (tail === 2 ? 1 : 0);
755
+ const out = new Uint8Array(outLen);
756
+ for (let g = 0; g < full; g++) {
757
+ const d0 = decodeChar(s.charCodeAt(g * 3));
758
+ const d1 = decodeChar(s.charCodeAt(g * 3 + 1));
759
+ const d2 = decodeChar(s.charCodeAt(g * 3 + 2));
760
+ if (d0 < 0 || d1 < 0 || d2 < 0) {
761
+ throw new Error("base45: invalid character");
762
+ }
763
+ const v = d0 + d1 * 45 + d2 * 2025;
764
+ if (v > 65535) {
765
+ throw new Error("base45: decoded group exceeds 0xFFFF");
766
+ }
767
+ out[g * 2] = v >> 8 & 255;
768
+ out[g * 2 + 1] = v & 255;
769
+ }
770
+ if (tail === 2) {
771
+ const d0 = decodeChar(s.charCodeAt(full * 3));
772
+ const d1 = decodeChar(s.charCodeAt(full * 3 + 1));
773
+ if (d0 < 0 || d1 < 0) {
774
+ throw new Error("base45: invalid character");
775
+ }
776
+ const v = d0 + d1 * 45;
777
+ if (v > 255) {
778
+ throw new Error("base45: decoded byte exceeds 0xFF");
779
+ }
780
+ out[outLen - 1] = v;
781
+ }
782
+ return out;
783
+ }
784
+
478
785
  // src/utils/qrUri.ts
479
- function generateConnectionURI(params) {
480
- const searchParams = new URLSearchParams({
481
- channelId: params.channelId,
482
- pubKey: params.pubKey,
483
- name: params.name,
484
- url: params.url,
485
- chainId: params.chainId,
486
- relay: params.relayUrl
487
- });
488
- if (params.icon) {
489
- searchParams.set("icon", params.icon);
490
- }
491
- return `qrlconnect://?${searchParams.toString()}`;
786
+ var MAGIC = new Uint8Array([80, 81, 80, 50]);
787
+ var FP_LABEL = new TextEncoder().encode("pq-fp/v2");
788
+ var CID_LEN = 16;
789
+ var FP_LEN = 32;
790
+ var BLOB_LEN = 4 + CID_LEN + FP_LEN;
791
+ function bs2(u) {
792
+ return u;
793
+ }
794
+ async function computeFingerprint(cid, pk) {
795
+ if (cid.length !== CID_LEN) {
796
+ throw new Error(`qrUri: cid must be ${CID_LEN} bytes`);
797
+ }
798
+ const c = globalThis.crypto;
799
+ if (!c || !c.subtle) {
800
+ throw new Error("qrUri: WebCrypto SubtleCrypto is not available");
801
+ }
802
+ const buf = new Uint8Array(FP_LABEL.length + cid.length + pk.length);
803
+ buf.set(FP_LABEL, 0);
804
+ buf.set(cid, FP_LABEL.length);
805
+ buf.set(pk, FP_LABEL.length + cid.length);
806
+ const digest = await c.subtle.digest("SHA-256", bs2(buf));
807
+ return new Uint8Array(digest);
808
+ }
809
+ function fingerprintEquals(a, b) {
810
+ if (a.length !== b.length) return false;
811
+ let diff = 0;
812
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
813
+ return diff === 0;
814
+ }
815
+ async function generateConnectionURI(cid, pk, relayUrl) {
816
+ if (cid.length !== CID_LEN) {
817
+ throw new Error(`qrUri: cid must be ${CID_LEN} bytes (got ${cid.length})`);
818
+ }
819
+ const fp = await computeFingerprint(cid, pk);
820
+ const blob = new Uint8Array(BLOB_LEN);
821
+ blob.set(MAGIC, 0);
822
+ blob.set(cid, 4);
823
+ blob.set(fp, 4 + CID_LEN);
824
+ const params = new URLSearchParams({ q: base45Encode(blob) });
825
+ if (relayUrl) params.set("r", relayUrl);
826
+ return `qrlconnect://?${params.toString()}`;
827
+ }
828
+ var PQP1_MAGIC = new Uint8Array([80, 81, 80, 49]);
829
+ function startsWith(buf, prefix) {
830
+ if (buf.length < prefix.length) return false;
831
+ for (let i = 0; i < prefix.length; i++) {
832
+ if (buf[i] !== prefix[i]) return false;
833
+ }
834
+ return true;
492
835
  }
493
- function parseConnectionURI(uri) {
836
+ async function parseConnectionURI(uri) {
837
+ if (typeof uri !== "string" || uri.length === 0) {
838
+ throw new Error("qrUri: empty URI");
839
+ }
840
+ if (!/^qrlconnect:/i.test(uri)) {
841
+ throw new Error("qrUri: not a qrlconnect URI");
842
+ }
843
+ let params;
494
844
  try {
495
- const queryString = uri.replace(/^qrlconnect:\/?\/?/, "");
496
- const params = new URLSearchParams(queryString);
497
- const channelId = params.get("channelId");
498
- const pubKey = params.get("pubKey");
499
- const name = params.get("name");
500
- const url = params.get("url");
501
- const chainId = params.get("chainId");
502
- const relayUrl = params.get("relay");
503
- if (!channelId || !pubKey || !name || !url || !chainId || !relayUrl) {
504
- return null;
505
- }
506
- return {
507
- channelId,
508
- pubKey,
509
- name,
510
- url,
511
- icon: params.get("icon") || void 0,
512
- chainId,
513
- relayUrl
514
- };
845
+ const swapped = new URL(uri.replace(/^qrlconnect:\/?\/?/i, "https://qrlconnect/"));
846
+ params = swapped.searchParams;
515
847
  } catch {
516
- return null;
848
+ throw new Error("qrUri: malformed URI");
849
+ }
850
+ if (params.has("channelId") || params.has("pubKey")) {
851
+ throw new Error(
852
+ "qrUri: legacy v1 URI detected \u2014 this wallet and this dApp must both run protocol v2"
853
+ );
854
+ }
855
+ const q = params.get("q");
856
+ if (!q) {
857
+ throw new Error("qrUri: missing q parameter");
858
+ }
859
+ let blob;
860
+ try {
861
+ blob = base45Decode(q);
862
+ } catch (err) {
863
+ const msg = err instanceof Error ? err.message : String(err);
864
+ throw new Error(`qrUri: base45 decode failed: ${msg}`);
865
+ }
866
+ if (blob.length !== BLOB_LEN) {
867
+ if (blob.length === 1208 && startsWith(blob, PQP1_MAGIC)) {
868
+ throw new Error("qrUri: legacy PQP1 URI detected \u2014 regenerate the QR with a v2.0+ dApp SDK");
869
+ }
870
+ throw new Error(`qrUri: expected ${BLOB_LEN}-byte blob, got ${blob.length}`);
871
+ }
872
+ if (!startsWith(blob, MAGIC)) {
873
+ throw new Error("qrUri: bad PQP2 magic");
874
+ }
875
+ const cid = blob.slice(4, 4 + CID_LEN);
876
+ const fp = blob.slice(4 + CID_LEN, 4 + CID_LEN + FP_LEN);
877
+ const relayUrl = params.get("r") || void 0;
878
+ return { cid, fp, relayUrl };
879
+ }
880
+ function cidToString(cid) {
881
+ if (cid.length !== CID_LEN) {
882
+ throw new Error(`cidToString: expected ${CID_LEN}-byte cid`);
883
+ }
884
+ let hex = "";
885
+ for (let i = 0; i < CID_LEN; i++) {
886
+ hex += cid[i].toString(16).padStart(2, "0");
887
+ }
888
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
889
+ }
890
+ function cidFromString(s) {
891
+ const hex = s.replace(/-/g, "");
892
+ if (!/^[0-9a-f]{32}$/i.test(hex)) {
893
+ throw new Error("cidFromString: not a 128-bit hex string");
894
+ }
895
+ const out = new Uint8Array(CID_LEN);
896
+ for (let i = 0; i < CID_LEN; i++) {
897
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
517
898
  }
899
+ return out;
900
+ }
901
+ function cidRandom() {
902
+ const c = globalThis.crypto;
903
+ if (!c || typeof c.getRandomValues !== "function") {
904
+ throw new Error("qrUri: crypto.getRandomValues is not available");
905
+ }
906
+ return c.getRandomValues(new Uint8Array(CID_LEN));
518
907
  }
519
908
 
520
909
  // src/ConnectionManager.ts
521
910
  var DAPP_PARTICIPANT_CONFLICT_ERROR_MSG = "dapp participant is already connected";
911
+ var TERMINATE_SEND_TIMEOUT_MS = 800;
522
912
  var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
523
913
  constructor(options) {
524
914
  super();
915
+ this.keyExchange = null;
525
916
  this.status = "disconnected" /* DISCONNECTED */;
526
917
  this.connectedAccounts = [];
527
918
  this.unresponsiveTimer = null;
528
- this.synRetryTimer = null;
919
+ this.reconnectProbeTimer = null;
529
920
  this.failedReconnects = 0;
530
- this.synSent = false;
531
- this.synAttempts = 0;
532
921
  this.walletPresent = false;
922
+ this.pendingRestore = null;
923
+ this.messageQueue = Promise.resolve();
533
924
  this.dappMetadata = options.dappMetadata;
534
925
  this.relayUrl = options.relayUrl || DEFAULT_RELAY_URL;
535
926
  this.chainId = options.chainId || "0x0";
536
927
  this.storageKey = options.storageKey || `${STORAGE_KEY_PREFIX}:session`;
537
- const session = this.loadSession();
538
- if (session) {
539
- this.channelId = session.channelId;
540
- this.ecies = new ECIESClient(session.privateKey);
541
- this.keyExchange = new KeyExchange(
542
- this.ecies,
543
- true,
544
- // dApp is always originator
545
- session.otherPublicKey || void 0
546
- );
547
- this.connectedAccounts = session.connectedAccounts;
548
- log("ConnectionManager", `Restored session for channel ${this.channelId}`);
928
+ const stored = this.readStoredSession();
929
+ if (stored) {
930
+ this.channelId = stored.channelId;
931
+ this.connectedAccounts = stored.connectedAccounts;
932
+ this.chainId = stored.chainId;
933
+ this.dappMetadata = stored.dappMetadata;
934
+ this.pendingRestore = stored;
935
+ log("ConnectionManager", `Found persisted session for channel ${this.channelId}`);
549
936
  } else {
550
937
  this.channelId = uuidv4();
551
- this.ecies = new ECIESClient();
552
- this.keyExchange = new KeyExchange(this.ecies, true);
553
938
  }
554
939
  this.socketClient = new SocketClient(this.relayUrl, "dapp");
555
940
  this.setupSocketListeners();
556
- this.setupKeyExchangeListeners();
557
941
  }
942
+ // ── Setup ──────────────────────────────────────────────────
558
943
  setupSocketListeners() {
559
944
  this.socketClient.on("connected", () => {
560
945
  if (this.status === "reconnecting" /* RECONNECTING */) {
561
946
  this.failedReconnects = 0;
562
- log("ConnectionManager", "Reconnected to relay");
563
947
  }
564
948
  });
565
- this.socketClient.on("reconnected", () => {
566
- if (this.keyExchange.areKeysExchanged()) {
949
+ this.socketClient.on("reconnected", (result) => {
950
+ if (result?.terminated) {
951
+ log("ConnectionManager", "Channel terminated, observed on auto-reconnect");
952
+ this.handleSessionTerminated();
953
+ return;
954
+ }
955
+ if (result) {
956
+ this.walletPresent = result.participants.includes("wallet");
957
+ }
958
+ if (this.keyExchange?.areKeysExchanged()) {
567
959
  if (this.walletPresent) {
960
+ this.clearReconnectProbe();
568
961
  this.setStatus("connected" /* CONNECTED */);
569
962
  this.failedReconnects = 0;
570
963
  } else {
571
964
  this.setStatus("waiting" /* WAITING */);
965
+ this.armReconnectProbe();
572
966
  }
573
- this.synAttempts = 0;
574
- this.clearSynRetryTimer();
575
967
  } else {
576
- this.sendSYN();
968
+ this.setStatus("waiting" /* WAITING */);
577
969
  }
578
970
  });
579
- this.socketClient.on("disconnected", (_reason) => {
971
+ this.socketClient.on("disconnected", () => {
580
972
  this.walletPresent = false;
581
- this.synSent = false;
582
- this.clearSynRetryTimer();
583
973
  if (this.status === "connected" /* CONNECTED */) {
584
974
  this.setStatus("reconnecting" /* RECONNECTING */);
585
975
  this.failedReconnects++;
@@ -589,7 +979,7 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
589
979
  }
590
980
  });
591
981
  this.socketClient.on("message", (data) => {
592
- this.handleRelayMessage(data);
982
+ this.enqueueRelayMessage(data);
593
983
  });
594
984
  this.socketClient.on("error", (err) => {
595
985
  this.emit("error", err);
@@ -599,20 +989,21 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
599
989
  this.walletPresent = true;
600
990
  log("ConnectionManager", "Wallet joined channel");
601
991
  this.clearUnresponsiveTimer();
602
- if (this.keyExchange.areKeysExchanged()) {
992
+ this.clearReconnectProbe();
993
+ if (this.keyExchange?.areKeysExchanged()) {
603
994
  this.failedReconnects = 0;
604
995
  this.setStatus("connected" /* CONNECTED */);
605
- } else if (!this.synSent) {
606
- this.sendSYN();
607
996
  }
608
997
  }
998
+ if (data.event === "close") {
999
+ log("ConnectionManager", "Wallet closed the channel (explicit terminate)");
1000
+ this.handleSessionTerminated();
1001
+ return;
1002
+ }
609
1003
  if (data.event === "disconnect" || data.event === "leave") {
610
1004
  if (data.clientType === "wallet" || !data.clientType) {
611
1005
  this.walletPresent = false;
612
- if (!this.keyExchange.areKeysExchanged()) {
613
- this.synSent = false;
614
- this.clearSynRetryTimer();
615
- } else if (this.status === "connected" /* CONNECTED */ || this.status === "reconnecting" /* RECONNECTING */) {
1006
+ if (this.keyExchange?.areKeysExchanged() && (this.status === "connected" /* CONNECTED */ || this.status === "reconnecting" /* RECONNECTING */)) {
616
1007
  this.setStatus("waiting" /* WAITING */);
617
1008
  }
618
1009
  log("ConnectionManager", "Wallet left channel");
@@ -621,15 +1012,15 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
621
1012
  });
622
1013
  }
623
1014
  setupKeyExchangeListeners() {
1015
+ if (!this.keyExchange) return;
624
1016
  this.keyExchange.on("keys_exchanged", () => {
625
1017
  log("ConnectionManager", "Key exchange complete");
1018
+ this.clearReconnectProbe();
626
1019
  this.setStatus("connected" /* CONNECTED */);
627
1020
  this.walletPresent = true;
628
- this.synSent = false;
629
- this.synAttempts = 0;
630
- this.clearSynRetryTimer();
631
- this.saveSession();
632
- this.sendEncrypted({
1021
+ this.failedReconnects = 0;
1022
+ void this.persistSession();
1023
+ void this.sendEncrypted({
633
1024
  type: "originator_info" /* ORIGINATOR_INFO */,
634
1025
  originatorInfo: {
635
1026
  ...this.dappMetadata,
@@ -638,16 +1029,25 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
638
1029
  });
639
1030
  });
640
1031
  }
1032
+ // ── Public API ─────────────────────────────────────────────
641
1033
  /**
642
- * Generate the connection URI and start listening for wallet connection.
1034
+ * Generate a new v2 connection URI. Rotates channel id and keypair.
1035
+ * Returns a `qrlconnect://?q=…` URI safe for QR-rendering or deep-link.
643
1036
  */
644
1037
  async getConnectionURI(retryOnConflict = true) {
645
1038
  this.setStatus("connecting" /* CONNECTING */);
646
- this.keyExchange.reset();
1039
+ this.clearReconnectProbe();
1040
+ this.pendingRestore = null;
647
1041
  this.walletPresent = false;
648
- this.synSent = false;
649
- this.synAttempts = 0;
650
- this.clearSynRetryTimer();
1042
+ if (this.keyExchange) {
1043
+ this.keyExchange.reset();
1044
+ } else {
1045
+ this.keyExchange = new KeyExchange(true);
1046
+ this.setupKeyExchangeListeners();
1047
+ }
1048
+ const pk = this.keyExchange.initiate();
1049
+ this.channelId = uuidv4();
1050
+ this.socketClient.setPublicKey(toBase64(pk));
651
1051
  this.socketClient.connect();
652
1052
  try {
653
1053
  await this.socketClient.joinChannel(this.channelId);
@@ -657,48 +1057,73 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
657
1057
  "ConnectionManager",
658
1058
  "Channel already has an active dApp participant. Rotating to a fresh channel."
659
1059
  );
660
- this.resetForNewChannel();
1060
+ this.channelId = uuidv4();
661
1061
  return this.getConnectionURI(false);
662
1062
  }
663
1063
  this.setStatus("disconnected" /* DISCONNECTED */);
664
1064
  throw err;
665
1065
  }
666
1066
  this.setStatus("waiting" /* WAITING */);
667
- const uri = generateConnectionURI({
668
- channelId: this.channelId,
669
- pubKey: this.ecies.getPublicKey(),
670
- name: this.dappMetadata.name,
671
- url: this.dappMetadata.url,
672
- icon: this.dappMetadata.icon,
673
- chainId: this.chainId,
674
- relayUrl: this.relayUrl
675
- });
676
- log("ConnectionManager", `Generated connection URI for channel ${this.channelId}`);
1067
+ const uri = await generateConnectionURI(
1068
+ cidFromString(this.channelId),
1069
+ pk,
1070
+ this.relayUrl === DEFAULT_RELAY_URL ? void 0 : this.relayUrl
1071
+ );
1072
+ log("ConnectionManager", `Generated v2 connection URI for channel ${this.channelId}`);
677
1073
  return uri;
678
1074
  }
679
1075
  /**
680
1076
  * Reconnect to an existing session.
1077
+ * Returns false if there is nothing to restore.
681
1078
  */
682
1079
  async reconnect() {
683
- const session = this.loadSession();
684
- if (!session) return false;
1080
+ if (!this.pendingRestore) return false;
685
1081
  this.setStatus("reconnecting" /* RECONNECTING */);
686
1082
  this.walletPresent = false;
687
- this.synSent = false;
688
- this.synAttempts = 0;
689
- this.clearSynRetryTimer();
1083
+ try {
1084
+ const session = await KeyExchange.sessionFromPersisted(this.pendingRestore.keyExchange);
1085
+ this.keyExchange = new KeyExchange(true, session);
1086
+ this.setupKeyExchangeListeners();
1087
+ } catch (err) {
1088
+ error("ConnectionManager", "Failed to hydrate persisted session:", err);
1089
+ this.clearSession();
1090
+ this.pendingRestore = null;
1091
+ this.setStatus("disconnected" /* DISCONNECTED */);
1092
+ return false;
1093
+ }
1094
+ return this.joinAndSettle();
1095
+ }
1096
+ /**
1097
+ * (Re)open the socket, join the persisted channel, drain buffered relay
1098
+ * messages, and settle status. Shared by the cold-restore reconnect() and
1099
+ * the warm resume() path whose socket was torn down by the reconnect probe.
1100
+ * Assumes channelId and a hydrated keyExchange are already in place, and
1101
+ * that the caller has reset walletPresent + set RECONNECTING status.
1102
+ */
1103
+ async joinAndSettle() {
690
1104
  this.socketClient.connect();
691
1105
  try {
692
- const { bufferedMessages } = await this.socketClient.joinChannel(this.channelId);
1106
+ const { bufferedMessages, participants, terminated } = await this.socketClient.joinChannel(
1107
+ this.channelId
1108
+ );
1109
+ if (terminated) {
1110
+ log("ConnectionManager", "Stored session was terminated by the wallet; dropping it");
1111
+ this.handleSessionTerminated();
1112
+ return false;
1113
+ }
1114
+ if (participants.includes("wallet")) {
1115
+ this.walletPresent = true;
1116
+ }
693
1117
  for (const msg of bufferedMessages) {
694
- this.handleRelayMessage(msg);
1118
+ this.enqueueRelayMessage(msg);
695
1119
  }
696
- if (this.keyExchange.areKeysExchanged() && this.walletPresent) {
1120
+ await this.messageQueue;
1121
+ if (this.keyExchange?.areKeysExchanged() && this.walletPresent) {
1122
+ this.clearReconnectProbe();
697
1123
  this.setStatus("connected" /* CONNECTED */);
698
- } else if (this.keyExchange.areKeysExchanged()) {
699
- this.setStatus("waiting" /* WAITING */);
700
1124
  } else {
701
- this.sendSYN();
1125
+ this.setStatus("waiting" /* WAITING */);
1126
+ this.armReconnectProbe();
702
1127
  }
703
1128
  return true;
704
1129
  } catch (err) {
@@ -708,10 +1133,69 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
708
1133
  }
709
1134
  }
710
1135
  /**
711
- * Send a JSON-RPC request to the wallet.
1136
+ * Nudge the connection back to life after the dApp tab / wallet app was
1137
+ * backgrounded. Idempotent and safe to call from visibilitychange / online
1138
+ * / pageshow handlers. If a restored session has not yet been hydrated,
1139
+ * runs the full reconnect(); otherwise just re-opens the socket, whose
1140
+ * own connect handler re-joins the channel and drains the relay buffer.
1141
+ */
1142
+ resume() {
1143
+ if (this.status === "connected" /* CONNECTED */) return;
1144
+ if (this.pendingRestore && !this.keyExchange?.areKeysExchanged()) {
1145
+ void this.reconnect();
1146
+ return;
1147
+ }
1148
+ if (this.channelId && this.keyExchange?.areKeysExchanged()) {
1149
+ if (this.socketClient.getChannelId()) {
1150
+ this.socketClient.connect();
1151
+ } else {
1152
+ this.setStatus("reconnecting" /* RECONNECTING */);
1153
+ this.walletPresent = false;
1154
+ void this.joinAndSettle();
1155
+ }
1156
+ }
1157
+ }
1158
+ /**
1159
+ * A session was terminated for good (wallet sent a relay 'close', or the
1160
+ * join ack reported a tombstone). Clear local state and surface
1161
+ * DISCONNECTED so the consumer drops to a fresh-pairing UI.
1162
+ */
1163
+ handleSessionTerminated() {
1164
+ this.clearReconnectProbe();
1165
+ this.clearUnresponsiveTimer();
1166
+ this.walletPresent = false;
1167
+ this.pendingRestore = null;
1168
+ this.keyExchange = null;
1169
+ this.socketClient.leaveChannel();
1170
+ this.socketClient.disconnect();
1171
+ this.clearSession();
1172
+ this.setStatus("disconnected" /* DISCONNECTED */);
1173
+ }
1174
+ armReconnectProbe() {
1175
+ this.clearReconnectProbe();
1176
+ this.reconnectProbeTimer = setTimeout(() => {
1177
+ this.reconnectProbeTimer = null;
1178
+ if (this.walletPresent) return;
1179
+ warn(
1180
+ "ConnectionManager",
1181
+ `No wallet rejoined within ${RECONNECT_WALLET_PROBE_MS}ms; treating reconnect as dead`
1182
+ );
1183
+ this.socketClient.leaveChannel();
1184
+ this.socketClient.disconnect();
1185
+ this.setStatus("disconnected" /* DISCONNECTED */);
1186
+ }, RECONNECT_WALLET_PROBE_MS);
1187
+ }
1188
+ clearReconnectProbe() {
1189
+ if (this.reconnectProbeTimer) {
1190
+ clearTimeout(this.reconnectProbeTimer);
1191
+ this.reconnectProbeTimer = null;
1192
+ }
1193
+ }
1194
+ /**
1195
+ * Send a JSON-RPC request to the wallet (async, fire-and-forget).
712
1196
  */
713
1197
  sendJsonRpc(request) {
714
- if (!this.keyExchange.areKeysExchanged()) {
1198
+ if (!this.keyExchange?.areKeysExchanged()) {
715
1199
  throw new Error("Not connected: key exchange not complete");
716
1200
  }
717
1201
  this.sendEncrypted({
@@ -720,16 +1204,93 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
720
1204
  id: request.id,
721
1205
  method: request.method,
722
1206
  params: request.params
1207
+ }).catch((err) => {
1208
+ error("ConnectionManager", "Failed to send JSON-RPC:", err);
723
1209
  });
724
1210
  this.startUnresponsiveTimer();
725
1211
  }
1212
+ getStatus() {
1213
+ return this.status;
1214
+ }
1215
+ getAccounts() {
1216
+ return this.connectedAccounts;
1217
+ }
1218
+ getChainId() {
1219
+ return this.chainId;
1220
+ }
1221
+ getChannelId() {
1222
+ return this.channelId;
1223
+ }
1224
+ /** Check (sync) if a persisted session exists and has not expired. */
1225
+ hasStoredSession() {
1226
+ if (typeof localStorage === "undefined") return false;
1227
+ try {
1228
+ const raw = localStorage.getItem(this.storageKey);
1229
+ if (!raw) return false;
1230
+ const session = JSON.parse(raw);
1231
+ if (session.version !== 2) return false;
1232
+ return Date.now() - session.createdAt <= SESSION_TTL_MS;
1233
+ } catch {
1234
+ return false;
1235
+ }
1236
+ }
1237
+ /**
1238
+ * Best-effort TERMINATE delivery before we tear the socket down. Without
1239
+ * awaiting, the socket.disconnect() below would win the race against the
1240
+ * outbound emit, the wallet would only see `participants_changed:
1241
+ * disconnect` and enter its stale-session grace period instead of an
1242
+ * instant disconnect. Mirrors the wallet side's pattern.
1243
+ */
1244
+ async flushTerminate() {
1245
+ if (!this.keyExchange?.areKeysExchanged()) return;
1246
+ const send = this.sendEncrypted({ type: "terminate" /* TERMINATE */ }).catch(() => {
1247
+ });
1248
+ const timeout = new Promise((resolve) => setTimeout(resolve, TERMINATE_SEND_TIMEOUT_MS));
1249
+ await Promise.race([send, timeout]);
1250
+ }
726
1251
  /**
727
- * Handle an incoming relay message.
1252
+ * Reset to a fresh channel + keypair. Sends TERMINATE to any live peer,
1253
+ * drops the persisted session, and prepares a clean state for
1254
+ * getConnectionURI() to be called again.
728
1255
  */
729
- handleRelayMessage(data) {
1256
+ async resetForNewChannel() {
1257
+ this.clearUnresponsiveTimer();
1258
+ this.clearReconnectProbe();
1259
+ this.walletPresent = false;
1260
+ await this.flushTerminate();
1261
+ this.socketClient.leaveChannel();
1262
+ this.socketClient.disconnect();
1263
+ this.clearSession();
1264
+ this.connectedAccounts = [];
1265
+ this.pendingRestore = null;
1266
+ this.channelId = uuidv4();
1267
+ this.keyExchange = null;
1268
+ this.socketClient = new SocketClient(this.relayUrl, "dapp");
1269
+ this.setupSocketListeners();
1270
+ this.setStatus("disconnected" /* DISCONNECTED */);
1271
+ }
1272
+ async disconnect() {
1273
+ this.clearUnresponsiveTimer();
1274
+ this.clearReconnectProbe();
1275
+ this.walletPresent = false;
1276
+ await this.flushTerminate();
1277
+ this.socketClient.leaveChannel();
1278
+ this.socketClient.disconnect();
1279
+ this.setStatus("disconnected" /* DISCONNECTED */);
1280
+ this.clearSession();
1281
+ this.connectedAccounts = [];
1282
+ this.pendingRestore = null;
1283
+ this.keyExchange = null;
1284
+ }
1285
+ // ── Internals ──────────────────────────────────────────────
1286
+ enqueueRelayMessage(data) {
1287
+ this.messageQueue = this.messageQueue.then(() => this.handleRelayMessage(data)).catch((err) => error("ConnectionManager", "messageQueue handler error:", err));
1288
+ }
1289
+ async handleRelayMessage(data) {
730
1290
  if (data.clientType === "wallet") {
731
1291
  this.walletPresent = true;
732
- if (this.keyExchange.areKeysExchanged() && this.status !== "connected" /* CONNECTED */) {
1292
+ this.clearReconnectProbe();
1293
+ if (this.keyExchange?.areKeysExchanged() && this.status !== "connected" /* CONNECTED */) {
733
1294
  this.failedReconnects = 0;
734
1295
  this.setStatus("connected" /* CONNECTED */);
735
1296
  }
@@ -737,16 +1298,18 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
737
1298
  const message = data.message;
738
1299
  if (typeof message === "object" && message !== null) {
739
1300
  const msg = message;
740
- if (msg.type === "key_handshake_SYN" /* SYN */ || msg.type === "key_handshake_SYNACK" /* SYNACK */ || msg.type === "key_handshake_ACK" /* ACK */) {
741
- this.handleKeyExchangeMessage(
742
- msg
743
- );
1301
+ if (msg.type === "key_handshake_SYNACK" /* SYNACK */) {
1302
+ await this.handleSynAck(message);
1303
+ return;
1304
+ }
1305
+ if (msg.type === "key_handshake_SYN" /* SYN */ || msg.type === "key_handshake_ACK" /* ACK */) {
1306
+ warn("ConnectionManager", `Unexpected ${msg.type} on dApp side \u2014 ignoring`);
744
1307
  return;
745
1308
  }
746
1309
  }
747
- if (typeof message === "string" && this.keyExchange.areKeysExchanged()) {
1310
+ if (typeof message === "string" && this.keyExchange?.areKeysExchanged()) {
748
1311
  try {
749
- const decrypted = this.keyExchange.decryptMessage(message);
1312
+ const decrypted = await this.keyExchange.decryptMessage(message);
750
1313
  const parsed = JSON.parse(decrypted);
751
1314
  this.handleDecryptedMessage(parsed);
752
1315
  } catch (err) {
@@ -754,9 +1317,18 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
754
1317
  }
755
1318
  }
756
1319
  }
757
- handleKeyExchangeMessage(msg) {
1320
+ async handleSynAck(msg) {
1321
+ if (!this.keyExchange) return;
758
1322
  this.setStatus("key_exchange" /* KEY_EXCHANGE */);
759
- const response = this.keyExchange.onMessage(msg);
1323
+ let response;
1324
+ try {
1325
+ response = await this.keyExchange.onSynAck(cidFromString(this.channelId), msg);
1326
+ } catch (err) {
1327
+ const e = err instanceof Error ? err : new Error(String(err));
1328
+ error("ConnectionManager", "SYNACK processing failed:", e);
1329
+ this.emit("error", e);
1330
+ return;
1331
+ }
760
1332
  if (response) {
761
1333
  this.sendPlaintext(response);
762
1334
  }
@@ -773,17 +1345,13 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
773
1345
  const chainChanged = this.chainId !== nextChainId;
774
1346
  this.connectedAccounts = nextAccounts;
775
1347
  this.chainId = nextChainId;
776
- this.saveSession();
1348
+ void this.persistSession();
777
1349
  this.emit("wallet_info", {
778
1350
  accounts: this.connectedAccounts,
779
1351
  chainId: this.chainId
780
1352
  });
781
- if (accountsChanged) {
782
- this.emit("accounts_changed", this.connectedAccounts);
783
- }
784
- if (chainChanged) {
785
- this.emit("chain_changed", this.chainId);
786
- }
1353
+ if (accountsChanged) this.emit("accounts_changed", this.connectedAccounts);
1354
+ if (chainChanged) this.emit("chain_changed", this.chainId);
787
1355
  break;
788
1356
  }
789
1357
  case "jsonrpc" /* JSONRPC */: {
@@ -793,14 +1361,13 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
793
1361
  }
794
1362
  case "terminate" /* TERMINATE */: {
795
1363
  log("ConnectionManager", "Received terminate from wallet");
796
- this.disconnect();
1364
+ void this.disconnect();
797
1365
  break;
798
1366
  }
799
1367
  default:
800
1368
  log("ConnectionManager", `Unhandled message type: ${type}`);
801
1369
  }
802
1370
  }
803
- /** Send a plaintext (unencrypted) message through the relay. */
804
1371
  sendPlaintext(message) {
805
1372
  this.socketClient.sendMessage({
806
1373
  id: this.channelId,
@@ -810,20 +1377,16 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
810
1377
  error("ConnectionManager", "Failed to send plaintext:", err);
811
1378
  });
812
1379
  }
813
- /** Send an encrypted message through the relay. */
814
- sendEncrypted(message) {
815
- try {
816
- const encrypted = this.keyExchange.encryptMessage(JSON.stringify(message));
817
- this.socketClient.sendMessage({
818
- id: this.channelId,
819
- clientType: "dapp",
820
- message: encrypted
821
- }).catch((err) => {
822
- error("ConnectionManager", "Failed to send encrypted:", err);
823
- });
824
- } catch (err) {
825
- error("ConnectionManager", "Encryption failed:", err);
1380
+ async sendEncrypted(message) {
1381
+ if (!this.keyExchange?.areKeysExchanged()) {
1382
+ throw new Error("sendEncrypted: not connected");
826
1383
  }
1384
+ const encrypted = await this.keyExchange.encryptMessage(JSON.stringify(message));
1385
+ await this.socketClient.sendMessage({
1386
+ id: this.channelId,
1387
+ clientType: "dapp",
1388
+ message: encrypted
1389
+ });
827
1390
  }
828
1391
  startUnresponsiveTimer() {
829
1392
  this.clearUnresponsiveTimer();
@@ -837,46 +1400,6 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
837
1400
  this.unresponsiveTimer = null;
838
1401
  }
839
1402
  }
840
- sendSYN() {
841
- if (!this.socketClient.isConnected()) {
842
- return;
843
- }
844
- if (this.synAttempts >= _ConnectionManager.MAX_SYN_ATTEMPTS) {
845
- this.synSent = false;
846
- this.emit("error", new Error("Key exchange timed out waiting for wallet response"));
847
- return;
848
- }
849
- try {
850
- const syn = this.keyExchange.createSYN();
851
- this.sendPlaintext(syn);
852
- this.synSent = true;
853
- this.synAttempts++;
854
- this.scheduleSynRetry();
855
- log("ConnectionManager", "Sent SYN key exchange message");
856
- } catch (err) {
857
- error("ConnectionManager", "Failed to send SYN:", err);
858
- }
859
- }
860
- scheduleSynRetry() {
861
- this.clearSynRetryTimer();
862
- this.synRetryTimer = setTimeout(() => {
863
- this.synRetryTimer = null;
864
- if (this.keyExchange.areKeysExchanged()) {
865
- return;
866
- }
867
- this.synSent = false;
868
- if (!this.walletPresent || !this.socketClient.isConnected()) {
869
- return;
870
- }
871
- this.sendSYN();
872
- }, _ConnectionManager.SYN_RETRY_MS);
873
- }
874
- clearSynRetryTimer() {
875
- if (this.synRetryTimer) {
876
- clearTimeout(this.synRetryTimer);
877
- this.synRetryTimer = null;
878
- }
879
- }
880
1403
  setStatus(status) {
881
1404
  if (this.status !== status) {
882
1405
  this.status = status;
@@ -889,105 +1412,44 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
889
1412
  }
890
1413
  areArraysEqual(a, b) {
891
1414
  if (a.length !== b.length) return false;
892
- for (let i = 0; i < a.length; i++) {
893
- if (a[i] !== b[i]) return false;
894
- }
1415
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
895
1416
  return true;
896
1417
  }
897
- getStatus() {
898
- return this.status;
899
- }
900
- getAccounts() {
901
- return this.connectedAccounts;
902
- }
903
- getChainId() {
904
- return this.chainId;
905
- }
906
- getChannelId() {
907
- return this.channelId;
908
- }
909
- /** Check if a stored session exists (without loading/restoring it). */
910
- hasStoredSession() {
911
- if (typeof localStorage === "undefined") return false;
912
- try {
913
- const raw = localStorage.getItem(this.storageKey);
914
- if (!raw) return false;
915
- const session = JSON.parse(raw);
916
- return Date.now() - session.createdAt <= SESSION_TTL_MS;
917
- } catch {
918
- return false;
919
- }
920
- }
921
- /**
922
- * Reset the connection and create a new channel for a fresh pairing.
923
- * Disconnects any existing session, generates a new channel ID and keys.
924
- */
925
- resetForNewChannel() {
926
- this.clearUnresponsiveTimer();
927
- this.clearSynRetryTimer();
928
- this.synSent = false;
929
- this.synAttempts = 0;
930
- this.walletPresent = false;
931
- if (this.keyExchange.areKeysExchanged()) {
932
- try {
933
- this.sendEncrypted({ type: "terminate" /* TERMINATE */ });
934
- } catch {
935
- }
936
- }
937
- this.socketClient.leaveChannel();
938
- this.socketClient.disconnect();
939
- this.clearSession();
940
- this.connectedAccounts = [];
941
- this.channelId = uuidv4();
942
- this.ecies = new ECIESClient();
943
- this.keyExchange = new KeyExchange(this.ecies, true);
944
- this.socketClient = new SocketClient(this.relayUrl, "dapp");
945
- this.setupSocketListeners();
946
- this.setupKeyExchangeListeners();
947
- this.setStatus("disconnected" /* DISCONNECTED */);
948
- }
949
- /** Disconnect and clean up. */
950
- disconnect() {
951
- this.clearUnresponsiveTimer();
952
- this.clearSynRetryTimer();
953
- this.synSent = false;
954
- this.synAttempts = 0;
955
- this.walletPresent = false;
956
- if (this.keyExchange.areKeysExchanged()) {
957
- try {
958
- this.sendEncrypted({ type: "terminate" /* TERMINATE */ });
959
- } catch {
960
- }
961
- }
962
- this.socketClient.leaveChannel();
963
- this.socketClient.disconnect();
964
- this.setStatus("disconnected" /* DISCONNECTED */);
965
- this.clearSession();
966
- this.connectedAccounts = [];
967
- }
968
- // --- Session persistence ---
969
- saveSession() {
1418
+ // ── Persistence ────────────────────────────────────────────
1419
+ async persistSession() {
970
1420
  if (typeof localStorage === "undefined") return;
1421
+ if (!this.keyExchange) return;
1422
+ const persistedKex = await this.keyExchange.exportPersisted();
1423
+ if (!persistedKex) return;
971
1424
  const session = {
1425
+ version: 2,
972
1426
  channelId: this.channelId,
973
- privateKey: this.ecies.getPrivateKeyHex(),
974
- otherPublicKey: this.keyExchange.getOtherPublicKey(),
1427
+ keyExchange: persistedKex,
975
1428
  dappMetadata: this.dappMetadata,
976
1429
  connectedAccounts: this.connectedAccounts,
977
1430
  chainId: this.chainId,
978
- createdAt: Date.now(),
1431
+ createdAt: this.pendingRestore?.createdAt ?? Date.now(),
979
1432
  lastActivity: Date.now()
980
1433
  };
981
- localStorage.setItem(this.storageKey, JSON.stringify(session));
1434
+ try {
1435
+ localStorage.setItem(this.storageKey, JSON.stringify(session));
1436
+ } catch (err) {
1437
+ warn("ConnectionManager", "localStorage.setItem failed:", err);
1438
+ }
982
1439
  }
983
- loadSession() {
1440
+ readStoredSession() {
984
1441
  if (typeof localStorage === "undefined") return null;
985
1442
  try {
986
1443
  const raw = localStorage.getItem(this.storageKey);
987
1444
  if (!raw) return null;
988
1445
  const session = JSON.parse(raw);
989
- if (Date.now() - session.createdAt > SESSION_TTL_MS) {
990
- this.clearSession();
1446
+ if (session.version !== 2) {
1447
+ log("ConnectionManager", "Dropping legacy (pre-v2) session from storage");
1448
+ localStorage.removeItem(this.storageKey);
1449
+ return null;
1450
+ }
1451
+ if (!session.createdAt || Date.now() - session.createdAt > SESSION_TTL_MS) {
1452
+ localStorage.removeItem(this.storageKey);
991
1453
  return null;
992
1454
  }
993
1455
  return session;
@@ -1001,8 +1463,6 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
1001
1463
  }
1002
1464
  };
1003
1465
  _ConnectionManager.MAX_RECONNECT_FAILURES = 5;
1004
- _ConnectionManager.SYN_RETRY_MS = 5e3;
1005
- _ConnectionManager.MAX_SYN_ATTEMPTS = 6;
1006
1466
  var ConnectionManager = _ConnectionManager;
1007
1467
 
1008
1468
  // src/utils/platform.ts
@@ -1019,10 +1479,29 @@ function getAppStoreUrl() {
1019
1479
 
1020
1480
  // src/QRLConnectProvider.ts
1021
1481
  var requestCounter = 0;
1482
+ var QRL_CONNECT_PROVIDER_INFO = {
1483
+ name: "MyQRLWallet (Mobile)",
1484
+ rdns: "com.qrlwallet.connect",
1485
+ icon: "data:image/png;base64,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"
1486
+ };
1487
+ var EIP6963_ANNOUNCE_EVENT = "eip6963:announceProvider";
1488
+ var EIP6963_REQUEST_EVENT = "eip6963:requestProvider";
1489
+ function generateUuid() {
1490
+ if (typeof globalThis !== "undefined" && globalThis.crypto?.randomUUID) {
1491
+ return globalThis.crypto.randomUUID();
1492
+ }
1493
+ const r = (n) => Math.floor(Math.random() * n);
1494
+ const hex = (n, len) => n.toString(16).padStart(len, "0");
1495
+ return `${hex(r(4294967296), 8)}-${hex(r(65536), 4)}-${hex(16384 | r(4096), 4)}-${hex(32768 | r(16384), 4)}-${hex(r(4294967296), 8)}${hex(r(65536), 4)}`;
1496
+ }
1022
1497
  var QRLConnectProvider = class extends EventEmitter4 {
1023
1498
  constructor(options) {
1024
1499
  super();
1025
1500
  this.pendingRequests = /* @__PURE__ */ new Map();
1501
+ this.eip6963Detail = null;
1502
+ this.eip6963RequestListener = null;
1503
+ this.resumeListener = null;
1504
+ this.resumeDebounce = null;
1026
1505
  this.isQRLConnect = true;
1027
1506
  this.options = options;
1028
1507
  if (options.debug) {
@@ -1038,6 +1517,81 @@ var QRLConnectProvider = class extends EventEmitter4 {
1038
1517
  if (options.autoReconnect !== false) {
1039
1518
  this.connectionManager.reconnect();
1040
1519
  }
1520
+ this.setupResumeListeners();
1521
+ if (options.announceProvider !== false) {
1522
+ this.startEip6963Announce();
1523
+ }
1524
+ }
1525
+ startEip6963Announce() {
1526
+ if (typeof window === "undefined" || typeof CustomEvent === "undefined") {
1527
+ return;
1528
+ }
1529
+ const overrides = this.options.providerInfo ?? {};
1530
+ this.eip6963Detail = Object.freeze({
1531
+ info: Object.freeze({
1532
+ uuid: generateUuid(),
1533
+ name: overrides.name ?? QRL_CONNECT_PROVIDER_INFO.name,
1534
+ icon: overrides.icon ?? QRL_CONNECT_PROVIDER_INFO.icon,
1535
+ rdns: overrides.rdns ?? QRL_CONNECT_PROVIDER_INFO.rdns
1536
+ }),
1537
+ provider: this
1538
+ });
1539
+ const announce = () => {
1540
+ if (!this.eip6963Detail) return;
1541
+ window.dispatchEvent(new CustomEvent(EIP6963_ANNOUNCE_EVENT, { detail: this.eip6963Detail }));
1542
+ };
1543
+ this.eip6963RequestListener = announce;
1544
+ window.addEventListener(EIP6963_REQUEST_EVENT, announce);
1545
+ announce();
1546
+ }
1547
+ /**
1548
+ * Re-open the relay socket when the dApp tab regains focus or the network
1549
+ * comes back. Debounced because visibilitychange + online can fire together.
1550
+ * No-op when already connected (ConnectionManager.resume guards that).
1551
+ */
1552
+ setupResumeListeners() {
1553
+ if (typeof window === "undefined" || typeof document === "undefined") return;
1554
+ if (this.resumeListener) return;
1555
+ const resume = () => {
1556
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
1557
+ if (this.resumeDebounce) clearTimeout(this.resumeDebounce);
1558
+ this.resumeDebounce = setTimeout(() => {
1559
+ this.resumeDebounce = null;
1560
+ this.connectionManager.resume();
1561
+ }, 300);
1562
+ };
1563
+ this.resumeListener = resume;
1564
+ document.addEventListener("visibilitychange", resume);
1565
+ window.addEventListener("online", resume);
1566
+ window.addEventListener("pageshow", resume);
1567
+ }
1568
+ /**
1569
+ * Remove the foreground/online resume listeners. Called on disconnect so a
1570
+ * torn-down session can't be silently revived by a later tab focus; the
1571
+ * listeners are re-armed by getConnectionURI()/newConnection() if the same
1572
+ * provider is re-paired. Safe in any env.
1573
+ */
1574
+ teardownResumeListeners() {
1575
+ if (typeof window !== "undefined" && typeof document !== "undefined" && this.resumeListener) {
1576
+ document.removeEventListener("visibilitychange", this.resumeListener);
1577
+ window.removeEventListener("online", this.resumeListener);
1578
+ window.removeEventListener("pageshow", this.resumeListener);
1579
+ }
1580
+ this.resumeListener = null;
1581
+ if (this.resumeDebounce) {
1582
+ clearTimeout(this.resumeDebounce);
1583
+ this.resumeDebounce = null;
1584
+ }
1585
+ }
1586
+ /**
1587
+ * Stop announcing this provider over EIP-6963. Safe to call from any env.
1588
+ */
1589
+ stopEip6963Announce() {
1590
+ if (typeof window !== "undefined" && this.eip6963RequestListener) {
1591
+ window.removeEventListener(EIP6963_REQUEST_EVENT, this.eip6963RequestListener);
1592
+ }
1593
+ this.eip6963RequestListener = null;
1594
+ this.eip6963Detail = null;
1041
1595
  }
1042
1596
  setupConnectionListeners() {
1043
1597
  this.connectionManager.on("status_changed", (status) => {
@@ -1095,6 +1649,7 @@ var QRLConnectProvider = class extends EventEmitter4 {
1095
1649
  * Generate a connection URI for QR code display or deep link redirect.
1096
1650
  */
1097
1651
  async getConnectionURI() {
1652
+ this.setupResumeListeners();
1098
1653
  return this.connectionManager.getConnectionURI();
1099
1654
  }
1100
1655
  /**
@@ -1202,35 +1757,420 @@ var QRLConnectProvider = class extends EventEmitter4 {
1202
1757
  pending.reject(new Error("Connection reset"));
1203
1758
  }
1204
1759
  this.pendingRequests.clear();
1205
- this.connectionManager.resetForNewChannel();
1760
+ await this.connectionManager.resetForNewChannel();
1206
1761
  return this.getConnectionURI();
1207
1762
  }
1208
1763
  /**
1209
- * Disconnect from wallet and clean up.
1764
+ * Disconnect from wallet and clean up. Returns once the TERMINATE has
1765
+ * either been flushed to the relay or the 800ms best-effort window has
1766
+ * elapsed — the wallet gets an instant disconnect instead of landing in
1767
+ * its stale-session grace period.
1210
1768
  */
1211
- disconnect() {
1769
+ async disconnect() {
1212
1770
  for (const [, pending] of this.pendingRequests) {
1213
1771
  pending.reject(new Error("Disconnected"));
1214
1772
  }
1215
1773
  this.pendingRequests.clear();
1216
- this.connectionManager.disconnect();
1774
+ this.teardownResumeListeners();
1775
+ await this.connectionManager.disconnect();
1217
1776
  }
1218
1777
  };
1778
+
1779
+ // src/signing/ctx.ts
1780
+ var SCHEME_VERSION_MSG = "QRL-SIGN-MSG-v1";
1781
+ var SCHEME_VERSION_TYPED = "QRL-SIGN-TYPED-v1";
1782
+ var SCHEME_TAG_MSG = new TextEncoder().encode(SCHEME_VERSION_MSG);
1783
+ var SCHEME_TAG_TYPED = new TextEncoder().encode(SCHEME_VERSION_TYPED);
1784
+ var DIGEST_LEN = 64;
1785
+
1786
+ // src/signing/messageDigest.ts
1787
+ import { shake256 } from "@noble/hashes/sha3.js";
1788
+
1789
+ // src/signing/bytes.ts
1790
+ function hexToBytes(hex) {
1791
+ if (typeof hex !== "string") throw new Error("hex must be a string");
1792
+ if (!/^0x([0-9a-fA-F]{2})*$/.test(hex)) {
1793
+ throw new Error("hex must be 0x-prefixed even-length");
1794
+ }
1795
+ const n = (hex.length - 2) / 2;
1796
+ const out = new Uint8Array(n);
1797
+ for (let i = 0; i < n; i++) {
1798
+ out[i] = parseInt(hex.slice(2 + i * 2, 4 + i * 2), 16);
1799
+ }
1800
+ return out;
1801
+ }
1802
+ function bytesToHex(bytes) {
1803
+ if (!(bytes instanceof Uint8Array)) {
1804
+ throw new Error("bytes must be a Uint8Array");
1805
+ }
1806
+ let s = "0x";
1807
+ for (const v of bytes) s += v.toString(16).padStart(2, "0");
1808
+ return s;
1809
+ }
1810
+ function concatBytes(...parts) {
1811
+ return concatBytesArr(parts);
1812
+ }
1813
+ function concatBytesArr(parts) {
1814
+ let total = 0;
1815
+ for (const p of parts) total += p.length;
1816
+ const out = new Uint8Array(total);
1817
+ let off = 0;
1818
+ for (const p of parts) {
1819
+ out.set(p, off);
1820
+ off += p.length;
1821
+ }
1822
+ return out;
1823
+ }
1824
+
1825
+ // src/signing/messageDigest.ts
1826
+ function computeMessageDigest(messageBytes) {
1827
+ if (!(messageBytes instanceof Uint8Array)) {
1828
+ throw new Error("messageBytes must be a Uint8Array");
1829
+ }
1830
+ return shake256(concatBytes(SCHEME_TAG_MSG, messageBytes), { dkLen: DIGEST_LEN });
1831
+ }
1832
+
1833
+ // src/signing/typedData.ts
1834
+ import { shake256 as shake2562 } from "@noble/hashes/sha3.js";
1835
+ var SLOT = 32;
1836
+ var ATOMIC_RE = /^(?:(address|bool|string|bytes)|(u?int)(\d+)|bytes(\d+)|(.+?)\[(\d*)\])$/;
1837
+ var MAX_TYPE_DEPTH = 12;
1838
+ function parseFieldType(type, types, depth = 0) {
1839
+ if (depth > MAX_TYPE_DEPTH) throw new Error(`type nesting too deep: ${type}`);
1840
+ if (Object.prototype.hasOwnProperty.call(types, type)) {
1841
+ return { kind: "ref", name: type };
1842
+ }
1843
+ const m = ATOMIC_RE.exec(type);
1844
+ if (!m) throw new Error(`unknown type: ${type}`);
1845
+ const [, atomic, intKind, intWidthStr, bytesWidthStr, innerType, sizeStr] = m;
1846
+ if (atomic) return { kind: atomic };
1847
+ if (intKind) {
1848
+ const width = Number(intWidthStr);
1849
+ if (!Number.isInteger(width) || width < 8 || width > 256 || width % 8 !== 0) {
1850
+ throw new Error(`invalid int width: ${type}`);
1851
+ }
1852
+ return { kind: intKind === "uint" ? "uintN" : "intN", width };
1853
+ }
1854
+ if (bytesWidthStr) {
1855
+ const width = Number(bytesWidthStr);
1856
+ if (!Number.isInteger(width) || width < 1 || width > 32) {
1857
+ throw new Error(`invalid bytesN width: ${type}`);
1858
+ }
1859
+ return { kind: "bytesN", width };
1860
+ }
1861
+ if (innerType !== void 0) {
1862
+ parseFieldType(innerType, types, depth + 1);
1863
+ const size = sizeStr ? Number(sizeStr) : void 0;
1864
+ if (sizeStr && (!Number.isInteger(size) || size <= 0)) {
1865
+ throw new Error(`invalid array size: ${type}`);
1866
+ }
1867
+ return { kind: "array", inner: innerType, size };
1868
+ }
1869
+ throw new Error(`unhandled type: ${type}`);
1870
+ }
1871
+ function baseTypeName(type) {
1872
+ return type.replace(/(\[\d*\])+$/, "");
1873
+ }
1874
+ function collectDependencies(primary, types) {
1875
+ if (!Object.prototype.hasOwnProperty.call(types, primary)) {
1876
+ throw new Error(`unknown primary type: ${primary}`);
1877
+ }
1878
+ const visited = /* @__PURE__ */ new Set();
1879
+ const visit = (name, path) => {
1880
+ if (path.includes(name)) {
1881
+ throw new Error(`cyclic type reference: ${[...path, name].join(" -> ")}`);
1882
+ }
1883
+ if (visited.has(name)) return;
1884
+ visited.add(name);
1885
+ for (const f of types[name]) {
1886
+ const base = baseTypeName(f.type);
1887
+ if (Object.prototype.hasOwnProperty.call(types, base)) {
1888
+ visit(base, [...path, name]);
1889
+ }
1890
+ parseFieldType(f.type, types);
1891
+ }
1892
+ };
1893
+ visit(primary, []);
1894
+ return visited;
1895
+ }
1896
+ function validateTypeMap(types) {
1897
+ for (const [name, def] of Object.entries(types)) {
1898
+ if (!Array.isArray(def) || def.length === 0) {
1899
+ throw new Error(`empty or invalid struct: ${name}`);
1900
+ }
1901
+ const seen = /* @__PURE__ */ new Set();
1902
+ for (const f of def) {
1903
+ if (!f || typeof f.name !== "string" || !f.name) {
1904
+ throw new Error(`bad field in ${name}`);
1905
+ }
1906
+ if (typeof f.type !== "string" || !f.type) {
1907
+ throw new Error(`bad field type in ${name}.${f.name}`);
1908
+ }
1909
+ if (seen.has(f.name)) {
1910
+ throw new Error(`duplicate field "${f.name}" in ${name}`);
1911
+ }
1912
+ seen.add(f.name);
1913
+ }
1914
+ }
1915
+ }
1916
+ function encodeType(primary, types) {
1917
+ const deps = collectDependencies(primary, types);
1918
+ const others = [...deps].filter((n) => n !== primary).sort();
1919
+ return [primary, ...others].map((name) => {
1920
+ const fields = types[name];
1921
+ const inner = fields.map((f) => `${f.type} ${f.name}`).join(",");
1922
+ return `${name}(${inner})`;
1923
+ }).join("");
1924
+ }
1925
+ function typeHash(primary, types) {
1926
+ return shake2562(new TextEncoder().encode(encodeType(primary, types)), { dkLen: DIGEST_LEN });
1927
+ }
1928
+ function parseQAddress(addr) {
1929
+ if (typeof addr !== "string" || !/^Q[0-9a-fA-F]{40}$/.test(addr)) {
1930
+ throw new Error(`invalid Q-address: ${String(addr)}`);
1931
+ }
1932
+ return hexToBytes("0x" + addr.slice(1).toLowerCase());
1933
+ }
1934
+ function padLeft32(bytes) {
1935
+ if (bytes.length > SLOT) throw new Error("cannot pad: bytes > 32");
1936
+ const out = new Uint8Array(SLOT);
1937
+ out.set(bytes, SLOT - bytes.length);
1938
+ return out;
1939
+ }
1940
+ function padRight32(bytes) {
1941
+ if (bytes.length > SLOT) throw new Error("cannot pad: bytes > 32");
1942
+ const out = new Uint8Array(SLOT);
1943
+ out.set(bytes, 0);
1944
+ return out;
1945
+ }
1946
+ function bigIntToSlot(value, width, signed) {
1947
+ if (signed) {
1948
+ const limit = 1n << BigInt(width - 1);
1949
+ if (value >= limit || value < -limit) {
1950
+ throw new Error(`int${width} out of range: ${value}`);
1951
+ }
1952
+ } else {
1953
+ if (value < 0n) throw new Error(`uint${width} negative: ${value}`);
1954
+ const limit = 1n << BigInt(width);
1955
+ if (value >= limit) throw new Error(`uint${width} out of range: ${value}`);
1956
+ }
1957
+ const slotMask = (1n << 256n) - 1n;
1958
+ const repr = value < 0n ? value + (1n << 256n) & slotMask : value;
1959
+ const out = new Uint8Array(SLOT);
1960
+ let v = repr;
1961
+ for (let i = SLOT - 1; i >= 0; i--) {
1962
+ out[i] = Number(v & 0xffn);
1963
+ v >>= 8n;
1964
+ }
1965
+ return out;
1966
+ }
1967
+ function parseIntValue(v, typeLabel) {
1968
+ if (typeof v === "bigint") return v;
1969
+ if (typeof v === "string") {
1970
+ if (/^-?0x[0-9a-fA-F]+$/i.test(v)) {
1971
+ const isNegative = v.startsWith("-");
1972
+ const abs = BigInt(isNegative ? v.slice(1) : v);
1973
+ return isNegative ? -abs : abs;
1974
+ }
1975
+ if (!/^-?(0|[1-9]\d*)$/.test(v)) {
1976
+ throw new Error(`invalid ${typeLabel} string: ${v}`);
1977
+ }
1978
+ return BigInt(v);
1979
+ }
1980
+ if (typeof v === "number") {
1981
+ if (!Number.isInteger(v) || !Number.isSafeInteger(v)) {
1982
+ throw new Error(`unsafe ${typeLabel} number: ${v}, pass as string`);
1983
+ }
1984
+ return BigInt(v);
1985
+ }
1986
+ throw new Error(`unsupported ${typeLabel} value: ${typeof v}`);
1987
+ }
1988
+ function encodeField(type, value, types) {
1989
+ const parsed = parseFieldType(type, types);
1990
+ switch (parsed.kind) {
1991
+ case "ref":
1992
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1993
+ throw new Error(`struct field expects object: ${type}`);
1994
+ }
1995
+ return hashStruct(parsed.name, value, types);
1996
+ case "address":
1997
+ if (typeof value !== "string") {
1998
+ throw new Error(`address field expects string: ${typeof value}`);
1999
+ }
2000
+ return padLeft32(parseQAddress(value));
2001
+ case "bool":
2002
+ if (typeof value !== "boolean") {
2003
+ throw new Error(`bool field expects boolean: ${typeof value}`);
2004
+ }
2005
+ return padLeft32(new Uint8Array([value ? 1 : 0]));
2006
+ case "string":
2007
+ if (typeof value !== "string") {
2008
+ throw new Error(`string field expects string: ${typeof value}`);
2009
+ }
2010
+ return shake2562(new TextEncoder().encode(value), { dkLen: DIGEST_LEN });
2011
+ case "bytes":
2012
+ if (typeof value !== "string") {
2013
+ throw new Error(`bytes field expects 0x-hex string: ${typeof value}`);
2014
+ }
2015
+ return shake2562(hexToBytes(value), { dkLen: DIGEST_LEN });
2016
+ case "uintN":
2017
+ return bigIntToSlot(parseIntValue(value, `uint${parsed.width}`), parsed.width, false);
2018
+ case "intN":
2019
+ return bigIntToSlot(parseIntValue(value, `int${parsed.width}`), parsed.width, true);
2020
+ case "bytesN": {
2021
+ if (typeof value !== "string") {
2022
+ throw new Error(`bytes${parsed.width} expects 0x-hex string: ${typeof value}`);
2023
+ }
2024
+ const raw = hexToBytes(value);
2025
+ if (raw.length !== parsed.width) {
2026
+ throw new Error(`bytes${parsed.width} requires ${parsed.width} bytes, got ${raw.length}`);
2027
+ }
2028
+ return padRight32(raw);
2029
+ }
2030
+ case "array": {
2031
+ if (!Array.isArray(value)) {
2032
+ throw new Error(`array field expects array: ${typeof value}`);
2033
+ }
2034
+ if (parsed.size !== void 0 && value.length !== parsed.size) {
2035
+ throw new Error(`fixed array ${type} requires length ${parsed.size}, got ${value.length}`);
2036
+ }
2037
+ const chunks = value.map((v) => encodeField(parsed.inner, v, types));
2038
+ return shake2562(concatBytesArr(chunks), { dkLen: DIGEST_LEN });
2039
+ }
2040
+ }
2041
+ }
2042
+ function hashStruct(primary, data, types) {
2043
+ const fields = types[primary];
2044
+ if (!fields) throw new Error(`unknown struct: ${primary}`);
2045
+ const expected = new Set(fields.map((f) => f.name));
2046
+ for (const k of Object.keys(data)) {
2047
+ if (!expected.has(k)) throw new Error(`unknown field in ${primary}: ${k}`);
2048
+ }
2049
+ const parts = [typeHash(primary, types)];
2050
+ for (const f of fields) {
2051
+ if (!(f.name in data)) throw new Error(`missing field ${primary}.${f.name}`);
2052
+ parts.push(encodeField(f.type, data[f.name], types));
2053
+ }
2054
+ return shake2562(concatBytesArr(parts), { dkLen: DIGEST_LEN });
2055
+ }
2056
+ var RESERVED_DOMAIN_FIELDS = {
2057
+ name: "string",
2058
+ version: "string",
2059
+ chainId: "uint256",
2060
+ verifyingContract: "address",
2061
+ salt: "bytes32"
2062
+ };
2063
+ function validateDomainTypes(types) {
2064
+ const def = types.QRLDomain;
2065
+ if (!def) throw new Error("QRLDomain type is required");
2066
+ let hasName = false;
2067
+ for (const f of def) {
2068
+ const expected = RESERVED_DOMAIN_FIELDS[f.name];
2069
+ if (!expected) {
2070
+ throw new Error(
2071
+ `QRLDomain field "${f.name}" not in reserved set (name, version, chainId, verifyingContract, salt)`
2072
+ );
2073
+ }
2074
+ if (f.type !== expected) {
2075
+ throw new Error(`QRLDomain field "${f.name}" must be type "${expected}", got "${f.type}"`);
2076
+ }
2077
+ if (f.name === "name") hasName = true;
2078
+ }
2079
+ if (!hasName) throw new Error("QRLDomain.name is required");
2080
+ }
2081
+ function validatePayloadReachability(primary, types) {
2082
+ const reachable = /* @__PURE__ */ new Set();
2083
+ for (const root of ["QRLDomain", primary]) {
2084
+ for (const t of collectDependencies(root, types)) reachable.add(t);
2085
+ }
2086
+ for (const k of Object.keys(types)) {
2087
+ if (!reachable.has(k)) throw new Error(`unused referenced type: ${k}`);
2088
+ }
2089
+ }
2090
+ function computeTypedDataDigest(payload) {
2091
+ if (!payload || typeof payload !== "object") {
2092
+ throw new Error("invalid typed data payload");
2093
+ }
2094
+ validateTypeMap(payload.types);
2095
+ validateDomainTypes(payload.types);
2096
+ validatePayloadReachability(payload.primaryType, payload.types);
2097
+ const domainHash = hashStruct("QRLDomain", payload.domain, payload.types);
2098
+ const messageHash = hashStruct(payload.primaryType, payload.message, payload.types);
2099
+ return shake2562(concatBytes(SCHEME_TAG_TYPED, domainHash, messageHash), { dkLen: DIGEST_LEN });
2100
+ }
2101
+
2102
+ // src/signing/verify.ts
2103
+ import * as mldsa from "@theqrl/mldsa87";
2104
+ function bytesOrHex(v) {
2105
+ if (v instanceof Uint8Array) return v;
2106
+ if (typeof v === "string") return hexToBytes(v);
2107
+ throw new Error("expected Uint8Array or 0x-hex string");
2108
+ }
2109
+ function verifyMessage({
2110
+ signature,
2111
+ publicKey,
2112
+ messageBytes
2113
+ }) {
2114
+ try {
2115
+ const sig = bytesOrHex(signature);
2116
+ const pk = bytesOrHex(publicKey);
2117
+ const msg = bytesOrHex(messageBytes);
2118
+ const digest = computeMessageDigest(msg);
2119
+ return mldsa.cryptoSignVerify(sig, digest, pk, SCHEME_TAG_MSG);
2120
+ } catch {
2121
+ return false;
2122
+ }
2123
+ }
2124
+ function verifyTypedData({ signature, publicKey, payload }) {
2125
+ try {
2126
+ const sig = bytesOrHex(signature);
2127
+ const pk = bytesOrHex(publicKey);
2128
+ const digest = computeTypedDataDigest(payload);
2129
+ return mldsa.cryptoSignVerify(sig, digest, pk, SCHEME_TAG_TYPED);
2130
+ } catch {
2131
+ return false;
2132
+ }
2133
+ }
1219
2134
  export {
2135
+ BLOB_LEN,
2136
+ CID_LEN,
1220
2137
  ConnectionManager,
1221
2138
  ConnectionStatus,
1222
2139
  DEFAULT_RELAY_URL,
1223
- ECIESClient,
2140
+ DIGEST_LEN,
2141
+ FP_LEN,
1224
2142
  KeyExchange,
1225
2143
  KeyExchangeMessageType,
1226
2144
  MessageType,
2145
+ PROTOCOL_VERSION,
1227
2146
  QRLConnectProvider as QRLConnect,
1228
2147
  QRLConnectProvider,
2148
+ QRL_CONNECT_PROVIDER_INFO,
1229
2149
  RESTRICTED_METHODS,
2150
+ SCHEME_TAG_MSG,
2151
+ SCHEME_TAG_TYPED,
2152
+ SCHEME_VERSION_MSG,
2153
+ SCHEME_VERSION_TYPED,
1230
2154
  SocketClient,
1231
2155
  UNRESTRICTED_METHODS,
2156
+ bytesToHex,
2157
+ cidFromString,
2158
+ cidRandom,
2159
+ cidToString,
2160
+ computeFingerprint,
2161
+ computeMessageDigest,
2162
+ computeTypedDataDigest,
2163
+ concatBytes,
2164
+ encodeField,
2165
+ encodeType,
2166
+ fingerprintEquals,
1232
2167
  generateConnectionURI,
1233
2168
  getAppStoreUrl,
2169
+ hashStruct,
2170
+ hexToBytes,
1234
2171
  isMobileBrowser,
1235
- parseConnectionURI
2172
+ parseConnectionURI,
2173
+ typeHash,
2174
+ verifyMessage,
2175
+ verifyTypedData
1236
2176
  };