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