@qrlwallet/connect 3.2.0 → 4.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
@@ -31,15 +31,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  BLOB_LEN: () => BLOB_LEN,
34
+ CAP_LEN: () => CAP_LEN,
34
35
  CID_LEN: () => CID_LEN,
35
36
  ConnectionManager: () => ConnectionManager,
36
37
  ConnectionStatus: () => ConnectionStatus,
37
38
  DEFAULT_RELAY_URL: () => DEFAULT_RELAY_URL,
38
39
  DIGEST_LEN: () => DIGEST_LEN,
40
+ EXPLICITLY_UNSUPPORTED_METHODS: () => EXPLICITLY_UNSUPPORTED_METHODS,
39
41
  FP_LEN: () => FP_LEN,
40
42
  KeyExchange: () => KeyExchange,
41
43
  KeyExchangeMessageType: () => KeyExchangeMessageType,
44
+ MAX_CONNECTION_URI_LENGTH: () => MAX_CONNECTION_URI_LENGTH,
45
+ MAX_JSON_RPC_ID_LENGTH: () => MAX_JSON_RPC_ID_LENGTH,
46
+ MAX_JSON_RPC_METHOD_LENGTH: () => MAX_JSON_RPC_METHOD_LENGTH,
47
+ MAX_RELAY_URL_LENGTH: () => MAX_RELAY_URL_LENGTH,
48
+ ML_DSA_87_PUBLIC_KEY_BYTES: () => ML_DSA_87_PUBLIC_KEY_BYTES,
49
+ ML_DSA_87_SIGNATURE_BYTES: () => ML_DSA_87_SIGNATURE_BYTES,
50
+ ML_DSA_DESCRIPTOR_BYTES: () => ML_DSA_DESCRIPTOR_BYTES,
42
51
  MessageType: () => MessageType,
52
+ PAIRING_CAPABILITY_LEN: () => PAIRING_CAPABILITY_LEN,
43
53
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
44
54
  QRLConnect: () => QRLConnectProvider,
45
55
  QRLConnectProvider: () => QRLConnectProvider,
@@ -50,12 +60,14 @@ __export(index_exports, {
50
60
  SCHEME_VERSION_MSG: () => SCHEME_VERSION_MSG,
51
61
  SCHEME_VERSION_TYPED: () => SCHEME_VERSION_TYPED,
52
62
  SocketClient: () => SocketClient,
63
+ TYPED_DATA_LIMITS: () => TYPED_DATA_LIMITS,
53
64
  UNRESTRICTED_METHODS: () => UNRESTRICTED_METHODS,
54
65
  attemptWalletRedirect: () => attemptWalletRedirect,
55
66
  bytesToHex: () => bytesToHex,
56
67
  cidFromString: () => cidFromString,
57
68
  cidRandom: () => cidRandom,
58
69
  cidToString: () => cidToString,
70
+ classifyRpcMethod: () => classifyRpcMethod,
59
71
  computeFingerprint: () => computeFingerprint,
60
72
  computeMessageDigest: () => computeMessageDigest,
61
73
  computeTypedDataDigest: () => computeTypedDataDigest,
@@ -65,13 +77,24 @@ __export(index_exports, {
65
77
  fingerprintEquals: () => fingerprintEquals,
66
78
  generateConnectionURI: () => generateConnectionURI,
67
79
  getAppStoreUrl: () => getAppStoreUrl,
80
+ hasSigningDescriptor: () => hasSigningDescriptor,
68
81
  hashStruct: () => hashStruct,
69
82
  hexToBytes: () => hexToBytes,
70
83
  isMobileBrowser: () => isMobileBrowser,
84
+ isQrlSignedMessageResult: () => isQrlSignedMessageResult,
85
+ isQrlSignedResult: () => isQrlSignedResult,
86
+ isQrlSignedTypedDataResult: () => isQrlSignedTypedDataResult,
87
+ isValidJsonRpcId: () => isValidJsonRpcId,
88
+ isValidJsonRpcMethod: () => isValidJsonRpcMethod,
89
+ normalizeRelayUrl: () => normalizeRelayUrl,
71
90
  parseConnectionURI: () => parseConnectionURI,
72
91
  typeHash: () => typeHash,
73
92
  verifyMessage: () => verifyMessage,
74
- verifyTypedData: () => verifyTypedData
93
+ verifyMessageForSigner: () => verifyMessageForSigner,
94
+ verifyMessageSignature: () => verifyMessageSignature,
95
+ verifyTypedData: () => verifyTypedData,
96
+ verifyTypedDataForSigner: () => verifyTypedDataForSigner,
97
+ verifyTypedDataSignature: () => verifyTypedDataSignature
75
98
  });
76
99
  module.exports = __toCommonJS(index_exports);
77
100
 
@@ -186,14 +209,115 @@ function constantTimeEquals(a, b) {
186
209
  return d === 0;
187
210
  }
188
211
 
212
+ // src/config.ts
213
+ var DEFAULT_RELAY_URL = "https://qrlwallet.com";
214
+ var RELAY_PATH = "/relay";
215
+ var PROTOCOL_VERSION = 3;
216
+ var PAIRING_CAPABILITY_LEN = 32;
217
+ var SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
218
+ var STORAGE_KEY_PREFIX = "@qrlwallet/connect";
219
+ var REQUEST_TIMEOUT_MS = 5 * 60 * 1e3;
220
+ var WALLET_UNRESPONSIVE_MS = 30 * 1e3;
221
+ var MAX_JSON_RPC_ID_LENGTH = 128;
222
+ var MAX_JSON_RPC_METHOD_LENGTH = 128;
223
+ var MAX_RELAY_URL_LENGTH = 2048;
224
+ var RECONNECT_WALLET_PROBE_MS = 12 * 1e3;
225
+ var CURRENT_QRL_ADDRESS_RE = /^Q[0-9a-fA-F]{40}$/;
226
+ function isCurrentQrlAddress(value) {
227
+ return typeof value === "string" && CURRENT_QRL_ADDRESS_RE.test(value);
228
+ }
229
+ function isExplicitLoopbackHostname(value) {
230
+ const hostname = value.toLowerCase();
231
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
232
+ }
233
+ function normalizeRelayUrl(value) {
234
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_RELAY_URL_LENGTH) {
235
+ throw new Error("Invalid relay URL");
236
+ }
237
+ let url;
238
+ try {
239
+ url = new URL(value);
240
+ } catch {
241
+ throw new Error("Invalid relay URL");
242
+ }
243
+ const isLoopback = isExplicitLoopbackHostname(url.hostname);
244
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback) || url.username !== "" || url.password !== "" || url.hash !== "" || url.search !== "" || url.pathname !== "/") {
245
+ throw new Error("Invalid relay URL");
246
+ }
247
+ return `${url.protocol}//${url.host}`;
248
+ }
249
+ var RESTRICTED_METHOD_NAMES = [
250
+ "qrl_requestAccounts",
251
+ "qrl_sendTransaction",
252
+ "qrl_signTransaction",
253
+ "qrl_signMessage",
254
+ "qrl_signTypedData",
255
+ "wallet_switchQrlChain"
256
+ ];
257
+ var UNRESTRICTED_METHOD_NAMES = [
258
+ "qrl_chainId",
259
+ "qrl_blockNumber",
260
+ "qrl_getBalance",
261
+ "qrl_getTransactionCount",
262
+ "qrl_getBlockByNumber",
263
+ "qrl_getTransactionReceipt",
264
+ "qrl_call",
265
+ "qrl_estimateGas",
266
+ "qrl_gasPrice",
267
+ "qrl_getCode",
268
+ "qrl_getLogs",
269
+ // Wallet-local authorization cache, never forwarded to hosted node RPC.
270
+ "qrl_accounts",
271
+ "net_version",
272
+ "net_listening"
273
+ ];
274
+ var EXPLICITLY_UNSUPPORTED_METHOD_NAMES = [
275
+ "qrl_sendRawTransaction",
276
+ "wallet_addQrlChain",
277
+ "personal_sign",
278
+ "qrl_sign",
279
+ "qrl_signTypedData_v3",
280
+ "qrl_signTypedData_v4"
281
+ ];
282
+ var RESTRICTED_METHOD_POLICY = new Set(RESTRICTED_METHOD_NAMES);
283
+ var UNRESTRICTED_METHOD_POLICY = new Set(UNRESTRICTED_METHOD_NAMES);
284
+ var EXPLICITLY_UNSUPPORTED_METHOD_POLICY = new Set(EXPLICITLY_UNSUPPORTED_METHOD_NAMES);
285
+ var RESTRICTED_METHODS = new Set(RESTRICTED_METHOD_NAMES);
286
+ var UNRESTRICTED_METHODS = new Set(UNRESTRICTED_METHOD_NAMES);
287
+ var EXPLICITLY_UNSUPPORTED_METHODS = new Set(
288
+ EXPLICITLY_UNSUPPORTED_METHOD_NAMES
289
+ );
290
+ function isValidJsonRpcId(value) {
291
+ return typeof value === "string" && value.length > 0 && value.length <= MAX_JSON_RPC_ID_LENGTH || typeof value === "number" && Number.isSafeInteger(value);
292
+ }
293
+ function isValidJsonRpcMethod(value) {
294
+ return typeof value === "string" && value.length > 0 && value.length <= MAX_JSON_RPC_METHOD_LENGTH && /^[A-Za-z0-9_.]+$/.test(value);
295
+ }
296
+ function classifyRpcMethod(method) {
297
+ if (EXPLICITLY_UNSUPPORTED_METHOD_POLICY.has(method)) return "unsupported";
298
+ if (RESTRICTED_METHOD_POLICY.has(method)) return "restricted";
299
+ if (UNRESTRICTED_METHOD_POLICY.has(method)) return "unrestricted";
300
+ return "unsupported";
301
+ }
302
+
189
303
  // src/PQCrypto.ts
190
304
  var textEncoder = new TextEncoder();
191
- var LABEL = textEncoder.encode("pq-pair/v1");
305
+ var LABEL = textEncoder.encode("pq-pair/v3");
192
306
  var LABEL_AEAD_SUFFIX = textEncoder.encode(" aead");
193
307
  var DIR_DAPP_TX = new Uint8Array([0, 0, 0, 1]);
194
308
  var DIR_WALLET_TX = new Uint8Array([0, 0, 0, 2]);
309
+ var ML_KEM_768_PK_LEN = 1184;
195
310
  var ML_KEM_768_CT_LEN = 1088;
311
+ var SHARED_SECRET_LEN = 32;
196
312
  var AEAD_KEY_LEN = 32;
313
+ function requireLength(name, value, expected) {
314
+ if (value.length !== expected) {
315
+ throw new Error(`PQCrypto: ${name} must be ${expected} bytes`);
316
+ }
317
+ }
318
+ function generatePairingCapability() {
319
+ return randomBytes(PAIRING_CAPABILITY_LEN);
320
+ }
197
321
  function kemKeygen() {
198
322
  return mlkemKeygen();
199
323
  }
@@ -203,12 +327,30 @@ function kemEncaps(pk) {
203
327
  function kemDecaps(sk, ct) {
204
328
  return mlkemDecaps(sk, ct);
205
329
  }
206
- async function transcriptHash(cid, pk, ct) {
207
- return sha256(concat(LABEL, cid, pk, ct));
330
+ async function transcriptHash(cid, pk, ct, capability) {
331
+ requireLength("cid", cid, 16);
332
+ requireLength("ML-KEM public key", pk, ML_KEM_768_PK_LEN);
333
+ requireLength("ML-KEM ciphertext", ct, ML_KEM_768_CT_LEN);
334
+ requireLength("pairing capability", capability, PAIRING_CAPABILITY_LEN);
335
+ const preimage = concat(LABEL, cid, pk, ct, capability);
336
+ try {
337
+ return await sha256(preimage);
338
+ } finally {
339
+ zeroize(preimage);
340
+ }
208
341
  }
209
- async function deriveAeadKey(ss, htx) {
342
+ async function deriveAeadKey(ss, htx, capability) {
343
+ requireLength("shared secret", ss, SHARED_SECRET_LEN);
344
+ requireLength("transcript hash", htx, 32);
345
+ requireLength("pairing capability", capability, PAIRING_CAPABILITY_LEN);
210
346
  const info = concat(LABEL, LABEL_AEAD_SUFFIX, htx);
211
- return hkdfAesGcmKey(ss, new Uint8Array(32), info);
347
+ const salt = capability.slice();
348
+ try {
349
+ return await hkdfAesGcmKey(ss, salt, info);
350
+ } finally {
351
+ zeroize(salt);
352
+ zeroize(info);
353
+ }
212
354
  }
213
355
  async function importRawAeadKey(raw) {
214
356
  if (raw.length !== AEAD_KEY_LEN) {
@@ -267,6 +409,33 @@ function toBase64(bytes) {
267
409
  }
268
410
  return btoa(bin);
269
411
  }
412
+ function base64EncodedLength(decodedLength) {
413
+ if (!Number.isSafeInteger(decodedLength) || decodedLength < 0) {
414
+ throw new Error("PQCrypto: decoded base64 length must be a non-negative safe integer");
415
+ }
416
+ return Math.ceil(decodedLength / 3) * 4;
417
+ }
418
+ var PADDED_BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
419
+ function isCanonicalBase64OfLength(value, decodedLength) {
420
+ if (typeof value !== "string" || value.length !== base64EncodedLength(decodedLength) || !PADDED_BASE64_RE.test(value)) {
421
+ return false;
422
+ }
423
+ let decoded = null;
424
+ try {
425
+ decoded = fromBase64(value);
426
+ return decoded.length === decodedLength && toBase64(decoded) === value;
427
+ } catch {
428
+ return false;
429
+ } finally {
430
+ if (decoded) zeroize(decoded);
431
+ }
432
+ }
433
+ function fromBase64Exact(value, decodedLength, fieldName = "value") {
434
+ if (!isCanonicalBase64OfLength(value, decodedLength)) {
435
+ throw new Error(`PQCrypto: ${fieldName} must encode exactly ${decodedLength} bytes`);
436
+ }
437
+ return fromBase64(value);
438
+ }
270
439
  function fromBase64(b64) {
271
440
  const bin = atob(b64);
272
441
  const out = new Uint8Array(bin.length);
@@ -274,66 +443,6 @@ function fromBase64(b64) {
274
443
  return out;
275
444
  }
276
445
 
277
- // src/config.ts
278
- var DEFAULT_RELAY_URL = "https://qrlwallet.com";
279
- var RELAY_PATH = "/relay";
280
- var PROTOCOL_VERSION = 2;
281
- var SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
282
- var STORAGE_KEY_PREFIX = "@qrlwallet/connect";
283
- var REQUEST_TIMEOUT_MS = 5 * 60 * 1e3;
284
- var WALLET_UNRESPONSIVE_MS = 30 * 1e3;
285
- var RECONNECT_WALLET_PROBE_MS = 12 * 1e3;
286
- var RESTRICTED_METHODS = /* @__PURE__ */ new Set([
287
- "qrl_requestAccounts",
288
- "qrl_sendTransaction",
289
- "qrl_signTransaction",
290
- "qrl_signMessage",
291
- "qrl_signTypedData",
292
- "wallet_addQrlChain",
293
- "wallet_switchQrlChain"
294
- ]);
295
- var UNRESTRICTED_METHODS = /* @__PURE__ */ new Set([
296
- "qrl_chainId",
297
- "qrl_blockNumber",
298
- "qrl_getBalance",
299
- "qrl_getTransactionCount",
300
- "qrl_getBlockByNumber",
301
- "qrl_getBlockByHash",
302
- "qrl_getTransactionByHash",
303
- "qrl_getTransactionReceipt",
304
- "qrl_call",
305
- "qrl_estimateGas",
306
- "qrl_gasPrice",
307
- "qrl_getCode",
308
- "qrl_getStorageAt",
309
- "qrl_getLogs",
310
- "qrl_getBlockTransactionCountByHash",
311
- "qrl_getBlockTransactionCountByNumber",
312
- "qrl_getTransactionByBlockHashAndIndex",
313
- "qrl_getTransactionByBlockNumberAndIndex",
314
- "qrl_accounts",
315
- "net_version",
316
- "net_listening",
317
- "net_peerCount",
318
- "web3_clientVersion",
319
- "web3_sha3",
320
- "qrl_syncing",
321
- "qrl_coinbase",
322
- "qrl_mining",
323
- "qrl_hashrate",
324
- "qrl_protocolVersion",
325
- "qrl_getUncleCountByBlockHash",
326
- "qrl_getUncleCountByBlockNumber",
327
- "qrl_getUncleByBlockHashAndIndex",
328
- "qrl_getUncleByBlockNumberAndIndex",
329
- "qrl_getFilterChanges",
330
- "qrl_getFilterLogs",
331
- "qrl_newBlockFilter",
332
- "qrl_newFilter",
333
- "qrl_newPendingTransactionFilter",
334
- "qrl_uninstallFilter"
335
- ]);
336
-
337
446
  // src/types.ts
338
447
  var KeyExchangeMessageType = /* @__PURE__ */ ((KeyExchangeMessageType2) => {
339
448
  KeyExchangeMessageType2["SYN"] = "key_handshake_SYN";
@@ -360,6 +469,69 @@ var ConnectionStatus = /* @__PURE__ */ ((ConnectionStatus2) => {
360
469
  ConnectionStatus2["RECONNECTING"] = "reconnecting";
361
470
  return ConnectionStatus2;
362
471
  })(ConnectionStatus || {});
472
+ var SIGNATURE_HEX_BYTES = 4627;
473
+ var PUBLIC_KEY_HEX_BYTES = 2592;
474
+ var DIGEST_HEX_BYTES = 64;
475
+ var MESSAGE_RESULT_KEYS = [
476
+ "signature",
477
+ "publicKey",
478
+ "signer",
479
+ "digest",
480
+ "schemeVersion"
481
+ ];
482
+ var MESSAGE_RESULT_KEYS_WITH_DESCRIPTOR = [
483
+ ...MESSAGE_RESULT_KEYS,
484
+ "descriptor"
485
+ ];
486
+ var TYPED_RESULT_KEYS = [...MESSAGE_RESULT_KEYS, "domain"];
487
+ var TYPED_RESULT_KEYS_WITH_DESCRIPTOR = [...TYPED_RESULT_KEYS, "descriptor"];
488
+ function isSigningResultRecord(value) {
489
+ return typeof value === "object" && value !== null && !Array.isArray(value);
490
+ }
491
+ function hasOwn(record, key) {
492
+ return Object.prototype.hasOwnProperty.call(record, key);
493
+ }
494
+ function hasExactOwnKeys(record, expected) {
495
+ const keys = Reflect.ownKeys(record);
496
+ return keys.length === expected.length && keys.every((key) => typeof key === "string" && expected.includes(key));
497
+ }
498
+ function isFixedHex(value, bytes) {
499
+ return typeof value === "string" && value.length === bytes * 2 + 2 && /^0x[0-9a-fA-F]+$/.test(value);
500
+ }
501
+ function isSigningDescriptor(value) {
502
+ return typeof value === "string" && /^0x01[0-9a-fA-F]{4}$/.test(value);
503
+ }
504
+ function hasValidSigningFields(record) {
505
+ return isFixedHex(record.signature, SIGNATURE_HEX_BYTES) && isFixedHex(record.publicKey, PUBLIC_KEY_HEX_BYTES) && (!hasOwn(record, "descriptor") || isSigningDescriptor(record.descriptor)) && typeof record.signer === "string" && /^Q[0-9a-fA-F]{40}$/.test(record.signer) && isFixedHex(record.digest, DIGEST_HEX_BYTES);
506
+ }
507
+ function isQrlSignedMessageResult(value) {
508
+ try {
509
+ if (!isSigningResultRecord(value)) return false;
510
+ const keys = hasOwn(value, "descriptor") ? MESSAGE_RESULT_KEYS_WITH_DESCRIPTOR : MESSAGE_RESULT_KEYS;
511
+ return hasExactOwnKeys(value, keys) && value.schemeVersion === "QRL-SIGN-MSG-v1" && hasValidSigningFields(value);
512
+ } catch {
513
+ return false;
514
+ }
515
+ }
516
+ function isQrlSignedTypedDataResult(value) {
517
+ try {
518
+ if (!isSigningResultRecord(value)) return false;
519
+ const keys = hasOwn(value, "descriptor") ? TYPED_RESULT_KEYS_WITH_DESCRIPTOR : TYPED_RESULT_KEYS;
520
+ return hasExactOwnKeys(value, keys) && value.schemeVersion === "QRL-SIGN-TYPED-v1" && isSigningResultRecord(value.domain) && hasValidSigningFields(value);
521
+ } catch {
522
+ return false;
523
+ }
524
+ }
525
+ function isQrlSignedResult(value) {
526
+ return isQrlSignedMessageResult(value) || isQrlSignedTypedDataResult(value);
527
+ }
528
+ function hasSigningDescriptor(result) {
529
+ try {
530
+ return isQrlSignedResult(result) && hasOwn(result, "descriptor") && isSigningDescriptor(result.descriptor);
531
+ } catch {
532
+ return false;
533
+ }
534
+ }
363
535
 
364
536
  // src/utils/logger.ts
365
537
  var debugEnabled = false;
@@ -383,15 +555,30 @@ var textEncoder2 = new TextEncoder();
383
555
  var textDecoder = new TextDecoder();
384
556
  var HELLO_WALLET = textEncoder2.encode("hello/wallet/v1");
385
557
  var HELLO_DAPP = textEncoder2.encode("hello/dapp/v1");
558
+ var AES_GCM_TAG_LEN = 16;
559
+ var SYNACK_C0_LEN = HELLO_WALLET.length + AES_GCM_TAG_LEN;
560
+ var ACK_C1_LEN = HELLO_DAPP.length + AES_GCM_TAG_LEN;
561
+ function snapshotSession(session) {
562
+ return {
563
+ ...session,
564
+ cid: session.cid.slice(),
565
+ htx: session.htx.slice(),
566
+ sendDir: session.sendDir.slice(),
567
+ recvDir: session.recvDir.slice()
568
+ };
569
+ }
386
570
  var KeyExchange = class extends import_eventemitter3.default {
387
571
  constructor(isOriginator, restored) {
388
572
  super();
389
573
  this.keypair = null;
574
+ this.capability = null;
390
575
  this.session = null;
391
576
  this.step = "key_handshake_SYN" /* SYN */;
392
577
  this.awaitingSynAck = false;
393
578
  this.awaitingAck = false;
579
+ this.awaitingAckDelivery = false;
394
580
  this.keysExchanged = false;
581
+ this.stateGeneration = 0;
395
582
  // The ACK we produced for the current handshake. A wallet that lost its
396
583
  // transport right after sending SYNACK re-sends the identical SYNACK on
397
584
  // rejoin; we answer with this cached ACK so the handshake converges
@@ -401,7 +588,10 @@ var KeyExchange = class extends import_eventemitter3.default {
401
588
  this.lastAck = null;
402
589
  this.isOriginator = isOriginator;
403
590
  if (restored) {
404
- this.session = restored;
591
+ if (restored.protocolVersion !== PROTOCOL_VERSION) {
592
+ throw new Error("KeyExchange: cannot restore a pre-v3 session");
593
+ }
594
+ this.session = snapshotSession(restored);
405
595
  this.keysExchanged = true;
406
596
  this.step = "key_handshake_ACK" /* ACK */;
407
597
  log("KeyExchange", "Hydrated from persisted session");
@@ -409,7 +599,9 @@ var KeyExchange = class extends import_eventemitter3.default {
409
599
  }
410
600
  /**
411
601
  * Originator (dApp) side: begin a fresh handshake by generating an
412
- * ephemeral ML-KEM-768 keypair. Returns pk to embed in the QR URI.
602
+ * ephemeral ML-KEM-768 keypair and a fresh 32-byte pairing capability.
603
+ * Both returned arrays are copies; the originator retains its own secret
604
+ * capability until the handshake succeeds or is retired.
413
605
  */
414
606
  initiate() {
415
607
  if (!this.isOriginator) {
@@ -417,10 +609,14 @@ var KeyExchange = class extends import_eventemitter3.default {
417
609
  }
418
610
  this.resetInternal(false);
419
611
  this.keypair = kemKeygen();
612
+ this.capability = generatePairingCapability();
420
613
  this.step = "key_handshake_SYN" /* SYN */;
421
614
  this.awaitingSynAck = true;
422
615
  this.emit("step_change", this.step);
423
- return this.keypair.pk;
616
+ return {
617
+ publicKey: this.keypair.pk.slice(),
618
+ capability: this.capability.slice()
619
+ };
424
620
  }
425
621
  /**
426
622
  * Originator: handle a SYNACK received over the relay.
@@ -432,86 +628,147 @@ var KeyExchange = class extends import_eventemitter3.default {
432
628
  warn("KeyExchange", "Responder received SYNACK - ignoring");
433
629
  return null;
434
630
  }
435
- if (!this.awaitingSynAck || !this.keypair) {
631
+ if (!this.awaitingSynAck || !this.keypair || !this.capability) {
436
632
  return null;
437
633
  }
438
634
  this.awaitingSynAck = false;
439
- const ct = fromBase64(msg.ct);
440
- const c0 = fromBase64(msg.c0);
441
- if (ct.length !== ML_KEM_768_CT_LEN) {
442
- throw new Error(`KeyExchange: bad ct length ${ct.length}`);
443
- }
444
- const ss = kemDecaps(this.keypair.sk, ct);
445
- const htx = await transcriptHash(cid, this.keypair.pk, ct);
446
- const key = await deriveAeadKey(ss, htx);
447
- let hello;
635
+ const stableCid = cid.slice();
636
+ const keypair = this.keypair;
637
+ const capability = this.capability;
638
+ const generation = this.stateGeneration;
639
+ let ss = null;
448
640
  try {
449
- hello = await open(key, DIR_WALLET_TX, 0, htx, c0);
450
- } catch {
451
- throw new Error(
452
- "KeyExchange: wallet hello AEAD tag failed (tampered SYNACK, wrong QR, or protocol mismatch)"
453
- );
641
+ if (msg.v !== PROTOCOL_VERSION) {
642
+ throw new Error(`KeyExchange: expected protocol v${PROTOCOL_VERSION}`);
643
+ }
644
+ const ct = fromBase64Exact(msg.ct, ML_KEM_768_CT_LEN, "SYNACK ciphertext");
645
+ const c0 = fromBase64Exact(msg.c0, SYNACK_C0_LEN, "SYNACK wallet hello");
646
+ ss = kemDecaps(keypair.sk, ct);
647
+ const htx = await transcriptHash(stableCid, keypair.pk, ct, capability);
648
+ const key = await deriveAeadKey(ss, htx, capability);
649
+ let hello = null;
650
+ try {
651
+ try {
652
+ hello = await open(key, DIR_WALLET_TX, 0, htx, c0);
653
+ } catch {
654
+ throw new Error(
655
+ "KeyExchange: wallet hello AEAD tag failed (tampered SYNACK, wrong QR, or protocol mismatch)"
656
+ );
657
+ }
658
+ if (!constantTimeEquals(hello, HELLO_WALLET)) {
659
+ throw new Error("KeyExchange: wallet hello mismatch");
660
+ }
661
+ } finally {
662
+ if (hello) zeroize(hello);
663
+ }
664
+ if (this.stateGeneration !== generation || this.keypair !== keypair) {
665
+ throw new Error("KeyExchange: handshake generation changed");
666
+ }
667
+ const c1 = await seal(key, DIR_DAPP_TX, 0, htx, HELLO_DAPP);
668
+ if (this.stateGeneration !== generation || this.keypair !== keypair) {
669
+ throw new Error("KeyExchange: handshake generation changed");
670
+ }
671
+ zeroize(keypair.sk);
672
+ if (this.keypair === keypair) this.keypair = null;
673
+ zeroize(capability);
674
+ if (this.capability === capability) this.capability = null;
675
+ this.session = {
676
+ protocolVersion: PROTOCOL_VERSION,
677
+ cid: stableCid,
678
+ key,
679
+ htx,
680
+ sendDir: DIR_DAPP_TX.slice(),
681
+ recvDir: DIR_WALLET_TX.slice(),
682
+ sendSeq: 1,
683
+ recvSeq: 1
684
+ };
685
+ const ack = {
686
+ type: "key_handshake_ACK" /* ACK */,
687
+ c1: toBase64(c1),
688
+ v: PROTOCOL_VERSION
689
+ };
690
+ this.lastAck = { ...ack };
691
+ this.awaitingAckDelivery = true;
692
+ return ack;
693
+ } catch (error2) {
694
+ if (this.stateGeneration === generation) this.resetInternal(false);
695
+ throw error2;
696
+ } finally {
697
+ if (ss) zeroize(ss);
454
698
  }
455
- if (!constantTimeEquals(hello, HELLO_WALLET)) {
456
- throw new Error("KeyExchange: wallet hello mismatch");
699
+ }
700
+ /**
701
+ * Originator: publish completion only after the relay acknowledged ACK.
702
+ * This is deliberately separate from onSynAck() so transport ambiguity
703
+ * cannot leave a live or persistable one-sided session.
704
+ */
705
+ confirmOriginatorAckDelivered() {
706
+ if (!this.isOriginator) {
707
+ throw new Error("KeyExchange: responder cannot confirm originator ACK delivery");
457
708
  }
458
- zeroize(ss);
459
- zeroize(this.keypair.sk);
460
- this.keypair = null;
461
- const c1 = await seal(key, DIR_DAPP_TX, 0, htx, HELLO_DAPP);
462
- this.session = {
463
- cid,
464
- key,
465
- htx,
466
- sendDir: DIR_DAPP_TX,
467
- recvDir: DIR_WALLET_TX,
468
- sendSeq: 1,
469
- recvSeq: 1
470
- };
709
+ if (this.keysExchanged) return;
710
+ if (!this.awaitingAckDelivery || !this.session || !this.lastAck) {
711
+ throw new Error("KeyExchange: no provisional originator session to confirm");
712
+ }
713
+ this.awaitingAckDelivery = false;
471
714
  this.keysExchanged = true;
472
715
  this.step = "key_handshake_ACK" /* ACK */;
473
716
  this.emit("keys_exchanged");
474
717
  this.emit("step_change", this.step);
475
- const ack = {
476
- type: "key_handshake_ACK" /* ACK */,
477
- c1: toBase64(c1),
478
- v: PROTOCOL_VERSION
479
- };
480
- this.lastAck = ack;
481
- return ack;
482
718
  }
483
719
  /**
484
720
  * Responder (wallet) side: begin handshake from a scanned QR.
485
721
  * Returns the SYNACK wire message to emit on the relay.
486
722
  */
487
- async receiveQR(cid, pk) {
723
+ async receiveQR(cid, pk, capability) {
488
724
  if (this.isOriginator) {
489
725
  throw new Error("KeyExchange: originator cannot consume a QR");
490
726
  }
491
727
  this.resetInternal(false);
492
- const { ct, ss } = kemEncaps(pk);
493
- const htx = await transcriptHash(cid, pk, ct);
494
- const key = await deriveAeadKey(ss, htx);
495
- const c0 = await seal(key, DIR_WALLET_TX, 0, htx, HELLO_WALLET);
496
- zeroize(ss);
497
- this.session = {
498
- cid,
499
- key,
500
- htx,
501
- sendDir: DIR_WALLET_TX,
502
- recvDir: DIR_DAPP_TX,
503
- sendSeq: 1,
504
- recvSeq: 1
505
- };
506
- this.awaitingAck = true;
507
- this.step = "key_handshake_SYNACK" /* SYNACK */;
508
- this.emit("step_change", this.step);
509
- return {
510
- type: "key_handshake_SYNACK" /* SYNACK */,
511
- ct: toBase64(ct),
512
- c0: toBase64(c0),
513
- v: PROTOCOL_VERSION
514
- };
728
+ const generation = this.stateGeneration;
729
+ const stableCid = cid.slice();
730
+ const stablePk = pk.slice();
731
+ if (capability.length !== PAIRING_CAPABILITY_LEN) {
732
+ throw new Error(`KeyExchange: capability must be ${PAIRING_CAPABILITY_LEN} bytes`);
733
+ }
734
+ const stableCapability = capability.slice();
735
+ let ss = null;
736
+ try {
737
+ const encapsulated = kemEncaps(stablePk);
738
+ const { ct } = encapsulated;
739
+ ss = encapsulated.ss;
740
+ const htx = await transcriptHash(stableCid, stablePk, ct, stableCapability);
741
+ const key = await deriveAeadKey(ss, htx, stableCapability);
742
+ const c0 = await seal(key, DIR_WALLET_TX, 0, htx, HELLO_WALLET);
743
+ if (this.stateGeneration !== generation) {
744
+ throw new Error("KeyExchange: handshake generation changed");
745
+ }
746
+ this.session = {
747
+ protocolVersion: PROTOCOL_VERSION,
748
+ cid: stableCid,
749
+ key,
750
+ htx,
751
+ sendDir: DIR_WALLET_TX.slice(),
752
+ recvDir: DIR_DAPP_TX.slice(),
753
+ sendSeq: 1,
754
+ recvSeq: 1
755
+ };
756
+ this.awaitingAck = true;
757
+ this.step = "key_handshake_SYNACK" /* SYNACK */;
758
+ this.emit("step_change", this.step);
759
+ return {
760
+ type: "key_handshake_SYNACK" /* SYNACK */,
761
+ ct: toBase64(ct),
762
+ c0: toBase64(c0),
763
+ v: PROTOCOL_VERSION
764
+ };
765
+ } catch (error2) {
766
+ if (this.stateGeneration === generation) this.resetInternal(false);
767
+ throw error2;
768
+ } finally {
769
+ if (ss) zeroize(ss);
770
+ zeroize(stableCapability);
771
+ }
515
772
  }
516
773
  /**
517
774
  * Responder: verify an incoming ACK and finalize the session.
@@ -529,59 +786,101 @@ var KeyExchange = class extends import_eventemitter3.default {
529
786
  throw new Error("KeyExchange: onAck called without a session");
530
787
  }
531
788
  this.awaitingAck = false;
532
- const c1 = fromBase64(msg.c1);
533
- let hello;
789
+ const session = this.session;
790
+ const generation = this.stateGeneration;
534
791
  try {
535
- hello = await open(this.session.key, DIR_DAPP_TX, 0, this.session.htx, c1);
536
- } catch {
537
- throw new Error("KeyExchange: dApp hello AEAD tag failed");
538
- }
539
- if (!constantTimeEquals(hello, HELLO_DAPP)) {
540
- throw new Error("KeyExchange: dApp hello mismatch");
792
+ if (msg.v !== PROTOCOL_VERSION) {
793
+ throw new Error(`KeyExchange: expected protocol v${PROTOCOL_VERSION}`);
794
+ }
795
+ const c1 = fromBase64Exact(msg.c1, ACK_C1_LEN, "ACK dApp hello");
796
+ let hello = null;
797
+ try {
798
+ try {
799
+ hello = await open(session.key, DIR_DAPP_TX, 0, session.htx, c1);
800
+ } catch {
801
+ throw new Error("KeyExchange: dApp hello AEAD tag failed");
802
+ }
803
+ if (!constantTimeEquals(hello, HELLO_DAPP)) {
804
+ throw new Error("KeyExchange: dApp hello mismatch");
805
+ }
806
+ } finally {
807
+ if (hello) zeroize(hello);
808
+ }
809
+ if (this.stateGeneration !== generation || this.session !== session) {
810
+ throw new Error("KeyExchange: handshake generation changed");
811
+ }
812
+ this.keysExchanged = true;
813
+ this.step = "key_handshake_ACK" /* ACK */;
814
+ this.emit("keys_exchanged");
815
+ } catch (error2) {
816
+ if (this.stateGeneration === generation) this.resetInternal(false);
817
+ throw error2;
541
818
  }
542
- this.keysExchanged = true;
543
- this.step = "key_handshake_ACK" /* ACK */;
544
- this.emit("keys_exchanged");
545
819
  }
546
820
  /** Encrypt a string for the counterparty. Returns base64. */
547
821
  async encryptMessage(data) {
548
- if (!this.session) {
822
+ const session = this.session;
823
+ const generation = this.stateGeneration;
824
+ if (!session) {
549
825
  throw new Error("KeyExchange: cannot encrypt - session not established");
550
826
  }
551
- const seq = this.session.sendSeq++;
827
+ if (session.sendSeq >= Number.MAX_SAFE_INTEGER) {
828
+ throw new Error("KeyExchange: send counter exhausted");
829
+ }
830
+ const seq = session.sendSeq++;
552
831
  const pt = textEncoder2.encode(data);
553
- const ct = await seal(this.session.key, this.session.sendDir, seq, this.session.htx, pt);
554
- return toBase64(ct);
832
+ try {
833
+ const ct = await seal(session.key, session.sendDir, seq, session.htx, pt);
834
+ if (this.stateGeneration !== generation || this.session !== session) {
835
+ throw new Error("KeyExchange: session changed while encrypting");
836
+ }
837
+ return toBase64(ct);
838
+ } finally {
839
+ zeroize(pt);
840
+ }
555
841
  }
556
842
  /** Decrypt a base64 ciphertext from the counterparty. */
557
843
  async decryptMessage(b64) {
558
- if (!this.session) {
844
+ const session = this.session;
845
+ const generation = this.stateGeneration;
846
+ if (!session) {
559
847
  throw new Error("KeyExchange: cannot decrypt - session not established");
560
848
  }
849
+ if (session.recvSeq >= Number.MAX_SAFE_INTEGER) {
850
+ throw new Error("KeyExchange: receive counter exhausted");
851
+ }
561
852
  const ct = fromBase64(b64);
562
- const pt = await open(
563
- this.session.key,
564
- this.session.recvDir,
565
- this.session.recvSeq,
566
- this.session.htx,
567
- ct
568
- );
569
- this.session.recvSeq++;
570
- return textDecoder.decode(pt);
853
+ let pt = null;
854
+ try {
855
+ pt = await open(session.key, session.recvDir, session.recvSeq, session.htx, ct);
856
+ if (this.stateGeneration !== generation || this.session !== session) {
857
+ throw new Error("KeyExchange: session changed while decrypting");
858
+ }
859
+ session.recvSeq++;
860
+ return textDecoder.decode(pt);
861
+ } finally {
862
+ if (pt) zeroize(pt);
863
+ }
571
864
  }
572
865
  /** Reset all state for a fresh handshake. Zeroes any live secret-key buffer. */
573
866
  reset() {
574
867
  this.resetInternal(true);
575
868
  }
576
869
  resetInternal(emit) {
870
+ this.stateGeneration++;
577
871
  if (this.keypair) {
578
872
  zeroize(this.keypair.sk);
579
873
  this.keypair = null;
580
874
  }
875
+ if (this.capability) {
876
+ zeroize(this.capability);
877
+ this.capability = null;
878
+ }
581
879
  this.session = null;
582
880
  this.keysExchanged = false;
583
881
  this.awaitingSynAck = false;
584
882
  this.awaitingAck = false;
883
+ this.awaitingAckDelivery = false;
585
884
  this.lastAck = null;
586
885
  this.step = "key_handshake_SYN" /* SYN */;
587
886
  if (emit) this.emit("step_change", this.step);
@@ -591,36 +890,55 @@ var KeyExchange = class extends import_eventemitter3.default {
591
890
  }
592
891
  /** The ACK produced for the current handshake, for duplicate-SYNACK replies. */
593
892
  getLastAck() {
594
- return this.lastAck;
893
+ return this.lastAck ? { ...this.lastAck } : null;
595
894
  }
596
895
  getSession() {
597
- return this.session;
896
+ return this.session ? snapshotSession(this.session) : null;
598
897
  }
599
898
  getCurrentStep() {
600
899
  return this.step;
601
900
  }
602
901
  /** Export the current session as a JSON-safe persisted record. */
603
902
  async exportPersisted() {
604
- if (!this.session) return null;
605
- return {
606
- cid: toBase64(this.session.cid),
607
- kAeadRaw: toBase64(await exportRawAeadKey(this.session.key)),
608
- htx: toBase64(this.session.htx),
609
- sendDir: toBase64(this.session.sendDir),
610
- recvDir: toBase64(this.session.recvDir),
611
- sendSeq: this.session.sendSeq,
612
- recvSeq: this.session.recvSeq
613
- };
903
+ const session = this.session;
904
+ const generation = this.stateGeneration;
905
+ if (!session || !this.keysExchanged) return null;
906
+ const kAeadRaw = await exportRawAeadKey(session.key);
907
+ try {
908
+ if (this.stateGeneration !== generation || this.session !== session) return null;
909
+ return {
910
+ protocolVersion: PROTOCOL_VERSION,
911
+ cid: toBase64(session.cid),
912
+ kAeadRaw: toBase64(kAeadRaw),
913
+ htx: toBase64(session.htx),
914
+ sendDir: toBase64(session.sendDir),
915
+ recvDir: toBase64(session.recvDir),
916
+ sendSeq: session.sendSeq,
917
+ recvSeq: session.recvSeq
918
+ };
919
+ } finally {
920
+ zeroize(kAeadRaw);
921
+ }
614
922
  }
615
923
  /** Rehydrate a Session from its persisted form. */
616
924
  static async sessionFromPersisted(p) {
617
- const key = await importRawAeadKey(fromBase64(p.kAeadRaw));
925
+ if (p.protocolVersion !== PROTOCOL_VERSION) {
926
+ throw new Error("KeyExchange: cannot hydrate a pre-v3 session");
927
+ }
928
+ const kAeadRaw = fromBase64Exact(p.kAeadRaw, 32, "persisted AEAD key");
929
+ let key;
930
+ try {
931
+ key = await importRawAeadKey(kAeadRaw);
932
+ } finally {
933
+ zeroize(kAeadRaw);
934
+ }
618
935
  return {
619
- cid: fromBase64(p.cid),
936
+ protocolVersion: PROTOCOL_VERSION,
937
+ cid: fromBase64Exact(p.cid, 16, "persisted channel id"),
620
938
  key,
621
- htx: fromBase64(p.htx),
622
- sendDir: fromBase64(p.sendDir),
623
- recvDir: fromBase64(p.recvDir),
939
+ htx: fromBase64Exact(p.htx, 32, "persisted transcript hash"),
940
+ sendDir: fromBase64Exact(p.sendDir, 4, "persisted send direction"),
941
+ recvDir: fromBase64Exact(p.recvDir, 4, "persisted receive direction"),
624
942
  sendSeq: p.sendSeq,
625
943
  recvSeq: p.recvSeq
626
944
  };
@@ -631,6 +949,20 @@ var KeyExchange = class extends import_eventemitter3.default {
631
949
  var import_socket = require("socket.io-client");
632
950
  var import_eventemitter32 = __toESM(require("eventemitter3"));
633
951
  var PENDING_JOIN_TIMEOUT_MS = 2e4;
952
+ var SEND_FLUSH_TIMEOUT_MS = 600;
953
+ var MESSAGE_ACK_TIMEOUT_MS = 15e3;
954
+ var MAX_JOIN_BUFFERED_MESSAGES = 50;
955
+ var MAX_JOIN_PARTICIPANTS = 1;
956
+ var MAX_RELAY_ERROR_CHARS = 512;
957
+ function isRecord(value) {
958
+ return typeof value === "object" && value !== null && !Array.isArray(value);
959
+ }
960
+ function parseParticipants(value, expectedPeerType) {
961
+ if (value === void 0) return [];
962
+ if (!Array.isArray(value) || value.length > MAX_JOIN_PARTICIPANTS) return null;
963
+ if (!value.every((participant) => participant === expectedPeerType)) return null;
964
+ return Array(value.length).fill(expectedPeerType);
965
+ }
634
966
  var SocketClient = class extends import_eventemitter32.default {
635
967
  constructor(relayUrl, clientType) {
636
968
  super();
@@ -642,9 +974,14 @@ var SocketClient = class extends import_eventemitter32.default {
642
974
  this.publicKeyBase64 = null;
643
975
  this.seq = 0;
644
976
  this.pendingJoin = null;
645
- this.relayUrl = relayUrl;
977
+ this.pendingSends = /* @__PURE__ */ new Set();
978
+ this.pendingJoinAttempts = /* @__PURE__ */ new Set();
979
+ this.relayUrl = normalizeRelayUrl(relayUrl);
646
980
  this.clientType = clientType;
647
981
  }
982
+ get expectedPeerType() {
983
+ return this.clientType === "dapp" ? "wallet" : "dapp";
984
+ }
648
985
  /**
649
986
  * dApp-only: stash the PK so it gets uploaded on every join_channel
650
987
  * attempt (including auto-rejoins after reconnect).
@@ -653,6 +990,9 @@ var SocketClient = class extends import_eventemitter32.default {
653
990
  if (this.clientType !== "dapp") {
654
991
  throw new Error("SocketClient.setPublicKey is only valid for dApp clients");
655
992
  }
993
+ if (!isCanonicalBase64OfLength(publicKeyBase64, ML_KEM_768_PK_LEN)) {
994
+ throw new Error(`SocketClient public key must be ${ML_KEM_768_PK_LEN} bytes`);
995
+ }
656
996
  this.publicKeyBase64 = publicKeyBase64;
657
997
  }
658
998
  /**
@@ -678,9 +1018,10 @@ var SocketClient = class extends import_eventemitter32.default {
678
1018
  this.emit("connected");
679
1019
  if (this.channelId) {
680
1020
  const reconnectChannelId = this.channelId;
1021
+ const isExplicitPendingJoin = this.pendingJoin?.channelId === reconnectChannelId;
681
1022
  this.joinChannelNow(reconnectChannelId).then((result) => {
682
1023
  this.settlePendingJoin(reconnectChannelId, { result });
683
- this.emit("reconnected", result);
1024
+ if (!isExplicitPendingJoin) this.emit("reconnected", result);
684
1025
  }).catch((err) => {
685
1026
  const reconnectErr = err instanceof Error ? err : new Error(String(err));
686
1027
  this.settlePendingJoin(reconnectChannelId, { error: reconnectErr });
@@ -691,20 +1032,28 @@ var SocketClient = class extends import_eventemitter32.default {
691
1032
  });
692
1033
  this.socket.on("disconnect", (reason) => {
693
1034
  log("Socket", `Disconnected: ${reason}`);
1035
+ this.rejectPendingJoinAttempts(
1036
+ new Error(`Socket disconnected before join acknowledgement: ${reason}`)
1037
+ );
1038
+ this.rejectPendingSends(
1039
+ new Error(`Socket disconnected before relay acknowledgement: ${reason}`)
1040
+ );
694
1041
  this.emit("disconnected", reason);
695
1042
  });
696
1043
  this.socket.on("message", (data) => {
697
- log("Socket", `Message received in channel ${data.id}`);
1044
+ log("Socket", "Relay message received");
698
1045
  this.emit("message", data);
699
1046
  });
700
1047
  this.socket.on("participants_changed", (data) => {
701
- if (typeof data !== "object" || data === null) return;
702
- const rec = { ...data };
703
- const event = typeof rec.event === "string" ? rec.event : "";
1048
+ if (!isRecord(data)) return;
1049
+ const event = data.event;
1050
+ if (event !== "join" && event !== "leave" && event !== "disconnect" && event !== "close" || data.clientType !== this.expectedPeerType) {
1051
+ return;
1052
+ }
704
1053
  log("Socket", `Participants changed: ${event}`);
705
1054
  this.emit("participants_changed", {
706
1055
  event,
707
- ...typeof rec.clientType === "string" ? { clientType: rec.clientType } : {}
1056
+ clientType: this.expectedPeerType
708
1057
  });
709
1058
  });
710
1059
  this.socket.on("connect_error", (err) => {
@@ -755,6 +1104,15 @@ var SocketClient = class extends import_eventemitter32.default {
755
1104
  rejectPending = reject;
756
1105
  });
757
1106
  const watchdog = setTimeout(() => {
1107
+ if (this.channelId === channelId) {
1108
+ this.channelId = null;
1109
+ const socket = this.socket;
1110
+ if (socket) {
1111
+ socket.removeAllListeners();
1112
+ socket.disconnect();
1113
+ if (this.socket === socket) this.socket = null;
1114
+ }
1115
+ }
758
1116
  this.settlePendingJoin(channelId, {
759
1117
  error: new Error(`joinChannel timed out after ${PENDING_JOIN_TIMEOUT_MS}ms`)
760
1118
  });
@@ -770,57 +1128,194 @@ var SocketClient = class extends import_eventemitter32.default {
770
1128
  }
771
1129
  joinChannelNow(channelId) {
772
1130
  return new Promise((resolve, reject) => {
773
- if (!this.socket?.connected) {
1131
+ const socket = this.socket;
1132
+ if (!socket?.connected) {
774
1133
  reject(new Error("Socket not connected"));
775
1134
  return;
776
1135
  }
1136
+ let settled = false;
1137
+ const abandonJoin = () => {
1138
+ if (this.channelId !== channelId) return;
1139
+ if (socket.connected) socket.emit("leave_channel", { channelId });
1140
+ this.channelId = null;
1141
+ };
1142
+ const finish = (outcome) => {
1143
+ if (settled) return;
1144
+ settled = true;
1145
+ clearTimeout(pending.timer);
1146
+ this.pendingJoinAttempts.delete(pending);
1147
+ if ("result" in outcome) resolve(outcome.result);
1148
+ else reject(outcome.error);
1149
+ };
1150
+ const pending = {
1151
+ timer: setTimeout(() => {
1152
+ abandonJoin();
1153
+ finish({
1154
+ error: new Error(
1155
+ `joinChannel acknowledgement timed out after ${PENDING_JOIN_TIMEOUT_MS}ms`
1156
+ )
1157
+ });
1158
+ }, PENDING_JOIN_TIMEOUT_MS),
1159
+ reject: (error2) => {
1160
+ finish({ error: error2 });
1161
+ }
1162
+ };
1163
+ this.pendingJoinAttempts.add(pending);
777
1164
  const payload = { channelId, clientType: this.clientType };
778
1165
  if (this.clientType === "dapp" && this.publicKeyBase64) {
779
1166
  payload.publicKey = this.publicKeyBase64;
780
1167
  }
781
- this.socket.emit(
782
- "join_channel",
783
- payload,
784
- (response) => {
785
- if (response.success) {
786
- log("Socket", `Joined channel ${channelId}`);
787
- resolve({
788
- bufferedMessages: response.bufferedMessages ?? [],
789
- channelPublicKey: response.channelPublicKey ?? null,
790
- participants: response.participants ?? [],
791
- terminated: response.terminated === true
792
- });
793
- } else {
794
- error("Socket", `Failed to join channel: ${response.error}`);
795
- reject(new Error(response.error ?? "Failed to join channel"));
1168
+ socket.emit("join_channel", payload, (response) => {
1169
+ if (!isRecord(response)) {
1170
+ abandonJoin();
1171
+ finish({ error: new Error("Relay returned a malformed join acknowledgement") });
1172
+ return;
1173
+ }
1174
+ if (response.success === true) {
1175
+ const rawMessages = response.bufferedMessages;
1176
+ if (rawMessages !== void 0 && !Array.isArray(rawMessages) || Array.isArray(rawMessages) && rawMessages.length > MAX_JOIN_BUFFERED_MESSAGES) {
1177
+ abandonJoin();
1178
+ finish({ error: new Error("Relay returned an invalid buffered message list") });
1179
+ return;
1180
+ }
1181
+ const rawParticipants = response.participants;
1182
+ const participants = parseParticipants(rawParticipants, this.expectedPeerType);
1183
+ if (!participants) {
1184
+ abandonJoin();
1185
+ finish({ error: new Error("Relay returned an invalid participant list") });
1186
+ return;
1187
+ }
1188
+ const rawPublicKey = response.channelPublicKey;
1189
+ if (rawPublicKey !== void 0 && rawPublicKey !== null && !isCanonicalBase64OfLength(rawPublicKey, ML_KEM_768_PK_LEN)) {
1190
+ abandonJoin();
1191
+ finish({ error: new Error("Relay returned an invalid channel public key") });
1192
+ return;
1193
+ }
1194
+ if (response.terminated !== void 0 && typeof response.terminated !== "boolean") {
1195
+ abandonJoin();
1196
+ finish({ error: new Error("Relay returned an invalid termination status") });
1197
+ return;
796
1198
  }
1199
+ log("Socket", `Joined channel ${channelId}`);
1200
+ finish({
1201
+ result: {
1202
+ bufferedMessages: Array.isArray(rawMessages) ? rawMessages.slice() : [],
1203
+ channelPublicKey: typeof rawPublicKey === "string" ? rawPublicKey : null,
1204
+ participants,
1205
+ terminated: response.terminated === true
1206
+ }
1207
+ });
1208
+ } else {
1209
+ const relayError = typeof response.error === "string" ? response.error.slice(0, MAX_RELAY_ERROR_CHARS) : "Failed to join channel";
1210
+ error("Socket", `Failed to join channel: ${relayError}`);
1211
+ abandonJoin();
1212
+ finish({ error: new Error(relayError) });
797
1213
  }
798
- );
1214
+ });
799
1215
  });
800
1216
  }
1217
+ rejectPendingJoinAttempts(error2) {
1218
+ for (const pending of [...this.pendingJoinAttempts]) pending.reject(error2);
1219
+ }
801
1220
  /**
802
1221
  * Send a message through the relay.
803
1222
  */
804
1223
  sendMessage(data) {
805
1224
  return new Promise((resolve, reject) => {
806
- if (!this.socket?.connected) {
1225
+ const socket = this.socket;
1226
+ if (!socket?.connected) {
807
1227
  reject(new Error("Socket not connected"));
808
1228
  return;
809
1229
  }
810
1230
  const dataWithSeq = { ...data, seq: this.seq++ };
811
- this.socket.emit(
812
- "message",
813
- dataWithSeq,
814
- (response) => {
815
- if (response?.success) {
816
- resolve({ success: true, buffered: response.buffered });
817
- } else {
818
- reject(new Error(response?.error ?? "Failed to send message"));
1231
+ let settled = false;
1232
+ const finish = (outcome) => {
1233
+ if (settled) return;
1234
+ settled = true;
1235
+ clearTimeout(pending.timer);
1236
+ this.pendingSends.delete(pending);
1237
+ if ("result" in outcome) resolve(outcome.result);
1238
+ else reject(outcome.error);
1239
+ };
1240
+ const pending = {
1241
+ timer: setTimeout(() => {
1242
+ finish({
1243
+ error: new Error(
1244
+ `Relay message acknowledgement timed out after ${MESSAGE_ACK_TIMEOUT_MS}ms`
1245
+ )
1246
+ });
1247
+ }, MESSAGE_ACK_TIMEOUT_MS),
1248
+ reject: (error2) => {
1249
+ finish({ error: error2 });
1250
+ }
1251
+ };
1252
+ this.pendingSends.add(pending);
1253
+ socket.emit("message", dataWithSeq, (response) => {
1254
+ if (!isRecord(response)) {
1255
+ finish({ error: new Error("Relay returned a malformed message acknowledgement") });
1256
+ return;
1257
+ }
1258
+ if (response.success === true) {
1259
+ if (typeof response.buffered !== "boolean") {
1260
+ finish({ error: new Error("Relay returned a malformed message acknowledgement") });
1261
+ return;
819
1262
  }
1263
+ finish({ result: { success: true, buffered: response.buffered } });
1264
+ } else {
1265
+ const relayError = typeof response.error === "string" ? response.error.slice(0, MAX_RELAY_ERROR_CHARS) : "Failed to send message";
1266
+ finish({ error: new Error(relayError) });
820
1267
  }
821
- );
1268
+ });
1269
+ });
1270
+ }
1271
+ rejectPendingSends(error2) {
1272
+ for (const pending of [...this.pendingSends]) pending.reject(error2);
1273
+ }
1274
+ /**
1275
+ * Emit an event and resolve once the relay acks it, or after a bounded
1276
+ * flush window. Lets a caller await transmission before tearing the
1277
+ * socket down.
1278
+ */
1279
+ flushEmit(event, payload) {
1280
+ return new Promise((resolve) => {
1281
+ if (!this.socket?.connected) {
1282
+ resolve(false);
1283
+ return;
1284
+ }
1285
+ let settled = false;
1286
+ const done = (confirmed) => {
1287
+ if (settled) return;
1288
+ settled = true;
1289
+ resolve(confirmed);
1290
+ };
1291
+ const timer = setTimeout(() => {
1292
+ done(false);
1293
+ }, SEND_FLUSH_TIMEOUT_MS);
1294
+ this.socket.emit(event, payload, (response) => {
1295
+ clearTimeout(timer);
1296
+ done(isRecord(response) && response.success === true && response.terminated === true);
1297
+ });
822
1298
  });
823
1299
  }
1300
+ /**
1301
+ * Explicitly terminate the channel on the relay (durable tombstone), as
1302
+ * opposed to the transient leaveChannel(). Used when the session is dead
1303
+ * in a way an encrypted TERMINATE cannot communicate (AEAD desync: the
1304
+ * peer could not open it). Resolves once the close is flushed or the
1305
+ * bounded window elapses, so the caller can safely disconnect afterwards.
1306
+ */
1307
+ closeChannel() {
1308
+ const channelId = this.channelId;
1309
+ this.channelId = null;
1310
+ if (this.pendingJoin) {
1311
+ clearTimeout(this.pendingJoin.watchdog);
1312
+ this.pendingJoin.reject(new Error("Channel closed before join completed"));
1313
+ this.pendingJoin = null;
1314
+ }
1315
+ this.rejectPendingJoinAttempts(new Error("Channel closed before join completed"));
1316
+ if (!this.socket?.connected || !channelId) return Promise.resolve(false);
1317
+ return this.flushEmit("close_channel", { channelId });
1318
+ }
824
1319
  /**
825
1320
  * Leave the current channel.
826
1321
  */
@@ -830,6 +1325,7 @@ var SocketClient = class extends import_eventemitter32.default {
830
1325
  this.pendingJoin.reject(new Error("Channel left before join completed"));
831
1326
  this.pendingJoin = null;
832
1327
  }
1328
+ this.rejectPendingJoinAttempts(new Error("Channel left before join completed"));
833
1329
  if (this.socket?.connected && this.channelId) {
834
1330
  this.socket.emit("leave_channel", { channelId: this.channelId });
835
1331
  }
@@ -844,8 +1340,10 @@ var SocketClient = class extends import_eventemitter32.default {
844
1340
  this.pendingJoin.reject(new Error("Socket disconnected before join completed"));
845
1341
  this.pendingJoin = null;
846
1342
  }
1343
+ this.rejectPendingJoinAttempts(new Error("Socket disconnected before join completed"));
847
1344
  this.channelId = null;
848
1345
  this.seq = 0;
1346
+ this.rejectPendingSends(new Error("Socket disconnected before relay acknowledgement"));
849
1347
  if (this.socket) {
850
1348
  this.socket.removeAllListeners();
851
1349
  this.socket.disconnect();
@@ -934,38 +1432,67 @@ function base45Decode(s) {
934
1432
  }
935
1433
 
936
1434
  // src/utils/qrUri.ts
937
- var MAGIC = new Uint8Array([80, 81, 80, 50]);
938
- var FP_LABEL = new TextEncoder().encode("pq-fp/v2");
1435
+ var MAGIC = new Uint8Array([80, 81, 80, 51]);
1436
+ var FP_LABEL = new TextEncoder().encode("pq-fp/v3");
939
1437
  var CID_LEN = 16;
940
1438
  var FP_LEN = 32;
941
- var BLOB_LEN = 4 + CID_LEN + FP_LEN;
942
- async function computeFingerprint(cid, pk) {
1439
+ var CAP_LEN = PAIRING_CAPABILITY_LEN;
1440
+ var BLOB_LEN = 4 + CID_LEN + FP_LEN + CAP_LEN;
1441
+ var MAX_CONNECTION_URI_LENGTH = 4096;
1442
+ async function computeFingerprint(cid, pk, capability) {
943
1443
  if (cid.length !== CID_LEN) {
944
1444
  throw new Error(`qrUri: cid must be ${CID_LEN} bytes`);
945
1445
  }
946
- const buf = new Uint8Array(FP_LABEL.length + cid.length + pk.length);
1446
+ if (pk.length !== ML_KEM_768_PK_LEN) {
1447
+ throw new Error(`qrUri: ML-KEM public key must be ${ML_KEM_768_PK_LEN} bytes`);
1448
+ }
1449
+ if (capability.length !== CAP_LEN) {
1450
+ throw new Error(`qrUri: capability must be ${CAP_LEN} bytes`);
1451
+ }
1452
+ const buf = new Uint8Array(FP_LABEL.length + cid.length + pk.length + capability.length);
947
1453
  buf.set(FP_LABEL, 0);
948
1454
  buf.set(cid, FP_LABEL.length);
949
1455
  buf.set(pk, FP_LABEL.length + cid.length);
950
- return sha256(buf);
1456
+ buf.set(capability, FP_LABEL.length + cid.length + pk.length);
1457
+ try {
1458
+ return await sha256(buf);
1459
+ } finally {
1460
+ buf.fill(0);
1461
+ }
951
1462
  }
952
1463
  function fingerprintEquals(a, b) {
953
1464
  return constantTimeEquals(a, b);
954
1465
  }
955
- async function generateConnectionURI(cid, pk, relayUrl) {
1466
+ async function generateConnectionURI(cid, pk, capability, relayUrl) {
956
1467
  if (cid.length !== CID_LEN) {
957
1468
  throw new Error(`qrUri: cid must be ${CID_LEN} bytes (got ${cid.length})`);
958
1469
  }
959
- const fp = await computeFingerprint(cid, pk);
1470
+ if (pk.length !== ML_KEM_768_PK_LEN) {
1471
+ throw new Error(
1472
+ `qrUri: ML-KEM public key must be ${ML_KEM_768_PK_LEN} bytes (got ${pk.length})`
1473
+ );
1474
+ }
1475
+ if (capability.length !== CAP_LEN) {
1476
+ throw new Error(`qrUri: capability must be ${CAP_LEN} bytes (got ${capability.length})`);
1477
+ }
1478
+ const stableCapability = capability.slice();
960
1479
  const blob = new Uint8Array(BLOB_LEN);
961
- blob.set(MAGIC, 0);
962
- blob.set(cid, 4);
963
- blob.set(fp, 4 + CID_LEN);
964
- const params = new URLSearchParams({ q: base45Encode(blob) });
965
- if (relayUrl) params.set("r", relayUrl);
966
- return `qrlconnect://?${params.toString()}`;
1480
+ try {
1481
+ const fp = await computeFingerprint(cid, pk, stableCapability);
1482
+ blob.set(MAGIC, 0);
1483
+ blob.set(cid, 4);
1484
+ blob.set(fp, 4 + CID_LEN);
1485
+ blob.set(stableCapability, 4 + CID_LEN + FP_LEN);
1486
+ const params = new URLSearchParams({ q: base45Encode(blob) });
1487
+ if (relayUrl !== void 0) params.set("r", normalizeRelayUrl(relayUrl));
1488
+ return `qrlconnect://?${params.toString()}`;
1489
+ } finally {
1490
+ stableCapability.fill(0);
1491
+ blob.fill(0);
1492
+ }
967
1493
  }
968
1494
  var PQP1_MAGIC = new Uint8Array([80, 81, 80, 49]);
1495
+ var PQP2_MAGIC = new Uint8Array([80, 81, 80, 50]);
969
1496
  function startsWith(buf, prefix) {
970
1497
  if (buf.length < prefix.length) return false;
971
1498
  for (let i = 0; i < prefix.length; i++) {
@@ -977,21 +1504,38 @@ async function parseConnectionURI(uri) {
977
1504
  if (typeof uri !== "string" || uri.length === 0) {
978
1505
  throw new Error("qrUri: empty URI");
979
1506
  }
980
- if (!/^qrlconnect:/i.test(uri)) {
1507
+ if (uri.length > MAX_CONNECTION_URI_LENGTH) {
1508
+ throw new Error(`qrUri: URI exceeds ${MAX_CONNECTION_URI_LENGTH} characters`);
1509
+ }
1510
+ if (!uri.startsWith("qrlconnect:")) {
981
1511
  throw new Error("qrUri: not a qrlconnect URI");
982
1512
  }
1513
+ if (!uri.startsWith("qrlconnect://?") || uri.includes("#")) {
1514
+ throw new Error("qrUri: URI must use canonical qrlconnect://? query form");
1515
+ }
983
1516
  let params;
984
1517
  try {
985
- const swapped = new URL(uri.replace(/^qrlconnect:\/?\/?/i, "https://qrlconnect/"));
986
- params = swapped.searchParams;
1518
+ const parsed = new URL(uri);
1519
+ if (parsed.protocol !== "qrlconnect:" || parsed.host !== "" || parsed.pathname !== "" || parsed.hash !== "") {
1520
+ throw new Error("non-canonical URI components");
1521
+ }
1522
+ params = parsed.searchParams;
987
1523
  } catch {
988
1524
  throw new Error("qrUri: malformed URI");
989
1525
  }
990
1526
  if (params.has("channelId") || params.has("pubKey")) {
991
1527
  throw new Error(
992
- "qrUri: legacy v1 URI detected - this wallet and this dApp must both run protocol v2"
1528
+ "qrUri: legacy v1 URI detected - this wallet and this dApp must both run protocol v3"
993
1529
  );
994
1530
  }
1531
+ if (params.getAll("q").length > 1 || params.getAll("r").length > 1) {
1532
+ throw new Error("qrUri: duplicate q or r parameter");
1533
+ }
1534
+ for (const key of params.keys()) {
1535
+ if (key !== "q" && key !== "r") {
1536
+ throw new Error(`qrUri: unknown parameter ${key}`);
1537
+ }
1538
+ }
995
1539
  const q = params.get("q");
996
1540
  if (!q) {
997
1541
  throw new Error("qrUri: missing q parameter");
@@ -1003,19 +1547,40 @@ async function parseConnectionURI(uri) {
1003
1547
  const msg = err instanceof Error ? err.message : String(err);
1004
1548
  throw new Error(`qrUri: base45 decode failed: ${msg}`);
1005
1549
  }
1006
- if (blob.length !== BLOB_LEN) {
1007
- if (blob.length === 1208 && startsWith(blob, PQP1_MAGIC)) {
1008
- throw new Error("qrUri: legacy PQP1 URI detected - regenerate the QR with a v2.0+ dApp SDK");
1009
- }
1010
- throw new Error(`qrUri: expected ${BLOB_LEN}-byte blob, got ${blob.length}`);
1011
- }
1012
- if (!startsWith(blob, MAGIC)) {
1013
- throw new Error("qrUri: bad PQP2 magic");
1550
+ try {
1551
+ if (blob.length !== BLOB_LEN) {
1552
+ if (blob.length === 1208 && startsWith(blob, PQP1_MAGIC)) {
1553
+ throw new Error(
1554
+ "qrUri: legacy PQP1 URI detected - regenerate the QR with a v4.0+ dApp SDK"
1555
+ );
1556
+ }
1557
+ if (blob.length === 52 && startsWith(blob, PQP2_MAGIC)) {
1558
+ throw new Error(
1559
+ "qrUri: legacy PQP2 URI detected - regenerate the QR with a v4.0+ dApp SDK"
1560
+ );
1561
+ }
1562
+ throw new Error(`qrUri: expected ${BLOB_LEN}-byte blob, got ${blob.length}`);
1563
+ }
1564
+ if (!startsWith(blob, MAGIC)) {
1565
+ throw new Error("qrUri: bad PQP3 magic");
1566
+ }
1567
+ const cid = blob.slice(4, 4 + CID_LEN);
1568
+ const fp = blob.slice(4 + CID_LEN, 4 + CID_LEN + FP_LEN);
1569
+ const capability = blob.slice(4 + CID_LEN + FP_LEN, BLOB_LEN);
1570
+ const r = params.get("r");
1571
+ let relayUrl;
1572
+ if (r !== null) {
1573
+ if (r === "") throw new Error("qrUri: empty relay URL");
1574
+ try {
1575
+ relayUrl = normalizeRelayUrl(r);
1576
+ } catch {
1577
+ throw new Error("qrUri: invalid relay URL");
1578
+ }
1579
+ }
1580
+ return { cid, fp, capability, relayUrl };
1581
+ } finally {
1582
+ blob.fill(0);
1014
1583
  }
1015
- const cid = blob.slice(4, 4 + CID_LEN);
1016
- const fp = blob.slice(4 + CID_LEN, 4 + CID_LEN + FP_LEN);
1017
- const r = params.get("r");
1018
- return { cid, fp, relayUrl: r === null || r === "" ? void 0 : r };
1019
1584
  }
1020
1585
  function cidToString(cid) {
1021
1586
  if (cid.length !== CID_LEN) {
@@ -1042,31 +1607,196 @@ function cidRandom() {
1042
1607
  return randomBytes(CID_LEN);
1043
1608
  }
1044
1609
 
1610
+ // src/SessionOwnership.ts
1611
+ function getBrowserStorage() {
1612
+ try {
1613
+ return typeof localStorage === "undefined" ? null : localStorage;
1614
+ } catch {
1615
+ return null;
1616
+ }
1617
+ }
1618
+ function getBrowserLockManager() {
1619
+ try {
1620
+ if (typeof navigator === "undefined") return null;
1621
+ return navigator.locks ?? null;
1622
+ } catch {
1623
+ return null;
1624
+ }
1625
+ }
1626
+ var SessionOwnership = class {
1627
+ constructor(storageKey, lockManager) {
1628
+ this.owned = false;
1629
+ this.acquireInFlight = null;
1630
+ this.releaseLock = null;
1631
+ this.lockRequest = null;
1632
+ this.releaseInFlight = null;
1633
+ // Monotonic attempt identity. release() advances it before waiting for the
1634
+ // Web Locks request, so a callback dispatched after teardown can observe
1635
+ // cancellation and decline a late grant.
1636
+ this.ownershipGeneration = 0;
1637
+ this.lockName = `${storageKey}:aead-owner`;
1638
+ this.lockManager = lockManager;
1639
+ }
1640
+ isOwned() {
1641
+ return this.owned;
1642
+ }
1643
+ async acquire() {
1644
+ const generation = this.ownershipGeneration;
1645
+ if (this.releaseInFlight) await this.releaseInFlight;
1646
+ if (generation !== this.ownershipGeneration) return false;
1647
+ if (this.owned) return true;
1648
+ if (this.acquireInFlight) return this.acquireInFlight;
1649
+ const task = this.startAcquire(generation);
1650
+ this.acquireInFlight = task;
1651
+ try {
1652
+ return await task;
1653
+ } finally {
1654
+ if (this.acquireInFlight === task) this.acquireInFlight = null;
1655
+ }
1656
+ }
1657
+ startAcquire(generation) {
1658
+ return new Promise((resolve) => {
1659
+ let grantSettled = false;
1660
+ const settleGrant = (granted) => {
1661
+ if (grantSettled) return;
1662
+ grantSettled = true;
1663
+ resolve(granted);
1664
+ };
1665
+ let request;
1666
+ try {
1667
+ request = this.lockManager.request(
1668
+ this.lockName,
1669
+ { mode: "exclusive", ifAvailable: true },
1670
+ async (lock) => {
1671
+ if (!lock || generation !== this.ownershipGeneration) {
1672
+ settleGrant(false);
1673
+ return;
1674
+ }
1675
+ this.owned = true;
1676
+ settleGrant(true);
1677
+ await new Promise((release) => {
1678
+ this.releaseLock = release;
1679
+ });
1680
+ this.releaseLock = null;
1681
+ this.owned = false;
1682
+ }
1683
+ );
1684
+ } catch (err) {
1685
+ warn("SessionOwnership", "Web Lock request failed:", err);
1686
+ settleGrant(false);
1687
+ return;
1688
+ }
1689
+ this.lockRequest = request.then(() => void 0).catch((err) => {
1690
+ warn("SessionOwnership", "Web Lock request failed:", err);
1691
+ settleGrant(false);
1692
+ });
1693
+ });
1694
+ }
1695
+ release() {
1696
+ this.ownershipGeneration++;
1697
+ if (this.releaseInFlight) return this.releaseInFlight;
1698
+ const task = this.finishRelease();
1699
+ const tracked = task.finally(() => {
1700
+ if (this.releaseInFlight === tracked) this.releaseInFlight = null;
1701
+ });
1702
+ this.releaseInFlight = tracked;
1703
+ return tracked;
1704
+ }
1705
+ async finishRelease() {
1706
+ const release = this.releaseLock;
1707
+ this.releaseLock = null;
1708
+ if (release) release();
1709
+ const request = this.lockRequest;
1710
+ if (request) await request;
1711
+ this.owned = false;
1712
+ this.acquireInFlight = null;
1713
+ if (this.lockRequest === request) this.lockRequest = null;
1714
+ }
1715
+ };
1716
+
1045
1717
  // src/ConnectionManager.ts
1046
1718
  var DAPP_PARTICIPANT_CONFLICT_ERROR_MSG = "dapp participant is already connected";
1047
- function isRecord(v) {
1719
+ var MAX_RELAY_CIPHERTEXT_CHARS = 256 * 1024;
1720
+ var MAX_JSON_RPC_ERROR_MESSAGE_CHARS = 1024;
1721
+ var MAX_DAPP_NAME_CHARS = 128;
1722
+ var MAX_DAPP_URL_CHARS = 2048;
1723
+ var MAX_DAPP_ICON_CHARS = 4096;
1724
+ var MAX_CHAIN_ID_CHARS = 66;
1725
+ var MAX_STORED_CLOCK_SKEW_MS = 5 * 60 * 1e3;
1726
+ var PERSISTED_CID_LEN = 16;
1727
+ var PERSISTED_AEAD_KEY_LEN = 32;
1728
+ var PERSISTED_HTX_LEN = 32;
1729
+ var PERSISTED_DIRECTION_LEN = 4;
1730
+ var MAX_CONNECTED_ACCOUNTS = 1;
1731
+ var DAPP_TX_B64 = toBase64(DIR_DAPP_TX);
1732
+ var WALLET_TX_B64 = toBase64(DIR_WALLET_TX);
1733
+ function isRecord2(v) {
1048
1734
  return typeof v === "object" && v !== null && !Array.isArray(v);
1049
1735
  }
1050
- function isStringArray(v) {
1051
- return Array.isArray(v) && v.every((x) => typeof x === "string");
1736
+ function hasUnsafeDAppNameCharacter(value) {
1737
+ for (let index = 0; index < value.length; index++) {
1738
+ const code = value.charCodeAt(index);
1739
+ if (code <= 31 || code >= 127 && code <= 159 || code === 1564 || code === 6158 || code >= 8203 && code <= 8207 || code >= 8232 && code <= 8238 || code === 8288 || code >= 8294 && code <= 8297 || code === 65279) {
1740
+ return true;
1741
+ }
1742
+ }
1743
+ return false;
1744
+ }
1745
+ function canonicalChainId(value) {
1746
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_CHAIN_ID_CHARS || !/^0x[0-9a-fA-F]+$/.test(value)) {
1747
+ return null;
1748
+ }
1749
+ try {
1750
+ return `0x${BigInt(value).toString(16)}`;
1751
+ } catch {
1752
+ return null;
1753
+ }
1754
+ }
1755
+ function canonicalDAppHttpUrl(value) {
1756
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_DAPP_URL_CHARS || value.trim() !== value) {
1757
+ return null;
1758
+ }
1759
+ try {
1760
+ const url = new URL(value);
1761
+ const hostname = url.hostname.toLowerCase();
1762
+ const isLoopback = isExplicitLoopbackHostname(hostname);
1763
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback) || url.username !== "" || url.password !== "" || hostname === "") {
1764
+ return null;
1765
+ }
1766
+ return url.toString();
1767
+ } catch {
1768
+ return null;
1769
+ }
1770
+ }
1771
+ function isBoundedTimestamp(value, now) {
1772
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= now + MAX_STORED_CLOCK_SKEW_MS;
1773
+ }
1774
+ function isCurrentQrlAddressArray(v) {
1775
+ return Array.isArray(v) && v.length <= MAX_CONNECTED_ACCOUNTS && v.every(isCurrentQrlAddress) && new Set(v).size === v.length;
1052
1776
  }
1053
1777
  function isRelayMessage(v) {
1054
- return isRecord(v) && typeof v.id === "string" && (v.clientType === "dapp" || v.clientType === "wallet") && "message" in v && (typeof v.message === "string" || typeof v.message === "object" && v.message !== null);
1778
+ return isRecord2(v) && typeof v.id === "string" && v.id.length > 0 && v.id.length <= 128 && (v.clientType === "dapp" || v.clientType === "wallet") && "message" in v && (typeof v.message === "string" && v.message.length <= MAX_RELAY_CIPHERTEXT_CHARS || isRecord2(v.message));
1055
1779
  }
1056
1780
  var MESSAGE_TYPE_BY_VALUE = Object.fromEntries(
1057
1781
  Object.values(MessageType).map((m) => [m, m])
1058
1782
  );
1059
1783
  function parseJsonRpcResponse(msg) {
1060
- if (typeof msg.id !== "string" && typeof msg.id !== "number") return null;
1784
+ if (msg.jsonrpc !== "2.0" || !isValidJsonRpcId(msg.id)) return null;
1785
+ const hasResult = Object.prototype.hasOwnProperty.call(msg, "result");
1786
+ const hasError = Object.prototype.hasOwnProperty.call(msg, "error");
1787
+ if (hasResult === hasError) return null;
1061
1788
  const out = {
1062
- jsonrpc: typeof msg.jsonrpc === "string" ? msg.jsonrpc : "2.0",
1789
+ jsonrpc: "2.0",
1063
1790
  id: msg.id
1064
1791
  };
1065
- if ("result" in msg) out.result = msg.result;
1066
- if (msg.error !== void 0) {
1067
- if (!isRecord(msg.error) || typeof msg.error.message !== "string") return null;
1792
+ if (hasResult) out.result = msg.result;
1793
+ if (hasError) {
1794
+ const code = isRecord2(msg.error) ? msg.error.code : void 0;
1795
+ if (!isRecord2(msg.error) || typeof code !== "number" || !Number.isSafeInteger(code) || typeof msg.error.message !== "string" || msg.error.message.length === 0 || msg.error.message.length > MAX_JSON_RPC_ERROR_MESSAGE_CHARS) {
1796
+ return null;
1797
+ }
1068
1798
  out.error = {
1069
- code: typeof msg.error.code === "number" ? msg.error.code : -32e3,
1799
+ code,
1070
1800
  message: msg.error.message,
1071
1801
  data: msg.error.data
1072
1802
  };
@@ -1074,18 +1804,38 @@ function parseJsonRpcResponse(msg) {
1074
1804
  return out;
1075
1805
  }
1076
1806
  function parsePersistedKex(v) {
1077
- if (!isRecord(v)) return null;
1078
- const { cid, kAeadRaw, htx, sendDir, recvDir, sendSeq, recvSeq } = v;
1079
- if (typeof cid !== "string" || typeof kAeadRaw !== "string" || typeof htx !== "string" || typeof sendDir !== "string" || typeof recvDir !== "string" || typeof sendSeq !== "number" || !Number.isInteger(sendSeq) || sendSeq < 0 || typeof recvSeq !== "number" || !Number.isInteger(recvSeq) || recvSeq < 0) {
1807
+ if (!isRecord2(v)) return null;
1808
+ const { protocolVersion, cid, kAeadRaw, htx, sendDir, recvDir, sendSeq, recvSeq } = v;
1809
+ if (protocolVersion !== PROTOCOL_VERSION || typeof cid !== "string" || typeof kAeadRaw !== "string" || typeof htx !== "string" || typeof sendDir !== "string" || typeof recvDir !== "string" || typeof sendSeq !== "number" || !Number.isSafeInteger(sendSeq) || sendSeq >= Number.MAX_SAFE_INTEGER || sendSeq < 0 || typeof recvSeq !== "number" || !Number.isSafeInteger(recvSeq) || recvSeq >= Number.MAX_SAFE_INTEGER || recvSeq < 0 || !isCanonicalBase64OfLength(cid, PERSISTED_CID_LEN) || !isCanonicalBase64OfLength(kAeadRaw, PERSISTED_AEAD_KEY_LEN) || !isCanonicalBase64OfLength(htx, PERSISTED_HTX_LEN) || !isCanonicalBase64OfLength(sendDir, PERSISTED_DIRECTION_LEN) || !isCanonicalBase64OfLength(recvDir, PERSISTED_DIRECTION_LEN) || sendDir !== DAPP_TX_B64 || recvDir !== WALLET_TX_B64) {
1080
1810
  return null;
1081
1811
  }
1082
- return { cid, kAeadRaw, htx, sendDir, recvDir, sendSeq, recvSeq };
1812
+ return {
1813
+ protocolVersion: PROTOCOL_VERSION,
1814
+ cid,
1815
+ kAeadRaw,
1816
+ htx,
1817
+ sendDir,
1818
+ recvDir,
1819
+ sendSeq,
1820
+ recvSeq
1821
+ };
1083
1822
  }
1084
1823
  function parseDAppMetadata(v) {
1085
- if (!isRecord(v) || typeof v.name !== "string" || typeof v.url !== "string") return null;
1086
- const meta = { name: v.name, url: v.url };
1087
- if (typeof v.icon === "string") meta.icon = v.icon;
1088
- if (typeof v.redirectUrl === "string") meta.redirectUrl = v.redirectUrl;
1824
+ if (!isRecord2(v) || typeof v.name !== "string" || v.name.length === 0 || v.name.length > MAX_DAPP_NAME_CHARS || v.name.trim() !== v.name || hasUnsafeDAppNameCharacter(v.name) || typeof v.url !== "string") {
1825
+ return null;
1826
+ }
1827
+ const url = canonicalDAppHttpUrl(v.url);
1828
+ if (!url) return null;
1829
+ const meta = { name: v.name, url };
1830
+ if (v.icon !== void 0) {
1831
+ if (typeof v.icon !== "string" || v.icon.length > MAX_DAPP_ICON_CHARS) return null;
1832
+ meta.icon = v.icon;
1833
+ }
1834
+ if (v.redirectUrl !== void 0) {
1835
+ const redirectUrl = canonicalDAppHttpUrl(v.redirectUrl);
1836
+ if (!redirectUrl) return null;
1837
+ meta.redirectUrl = redirectUrl;
1838
+ }
1089
1839
  return meta;
1090
1840
  }
1091
1841
  function parseStoredSession(raw) {
@@ -1095,19 +1845,32 @@ function parseStoredSession(raw) {
1095
1845
  } catch {
1096
1846
  return null;
1097
1847
  }
1098
- if (!isRecord(v) || v.version !== 3) return null;
1848
+ if (!isRecord2(v) || v.version !== 5) return null;
1099
1849
  if (typeof v.channelId !== "string" || typeof v.chainId !== "string") return null;
1100
- if (typeof v.createdAt !== "number" || typeof v.lastActivity !== "number") return null;
1850
+ const parsedChainId = canonicalChainId(v.chainId);
1851
+ if (!parsedChainId || parsedChainId !== v.chainId) return null;
1852
+ let encodedChannelId;
1853
+ try {
1854
+ encodedChannelId = toBase64(cidFromString(v.channelId));
1855
+ } catch {
1856
+ return null;
1857
+ }
1858
+ const now = Date.now();
1859
+ if (!isBoundedTimestamp(v.createdAt, now) || !isBoundedTimestamp(v.lastActivity, now) || v.lastActivity < v.createdAt) {
1860
+ return null;
1861
+ }
1101
1862
  const keyExchange = parsePersistedKex(v.keyExchange);
1102
1863
  const dappMetadata = parseDAppMetadata(v.dappMetadata);
1103
- if (!keyExchange || !dappMetadata || !isStringArray(v.connectedAccounts)) return null;
1864
+ if (keyExchange?.cid !== encodedChannelId || !dappMetadata || !isCurrentQrlAddressArray(v.connectedAccounts)) {
1865
+ return null;
1866
+ }
1104
1867
  return {
1105
- version: 3,
1868
+ version: 5,
1106
1869
  channelId: v.channelId,
1107
1870
  keyExchange,
1108
1871
  dappMetadata,
1109
1872
  connectedAccounts: v.connectedAccounts,
1110
- chainId: v.chainId,
1873
+ chainId: parsedChainId,
1111
1874
  createdAt: v.createdAt,
1112
1875
  lastActivity: v.lastActivity
1113
1876
  };
@@ -1121,15 +1884,36 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1121
1884
  this.connectedAccounts = [];
1122
1885
  this.unresponsiveTimer = null;
1123
1886
  this.reconnectProbeTimer = null;
1887
+ this.restoreInFlight = null;
1124
1888
  this.failedReconnects = 0;
1125
1889
  this.walletPresent = false;
1126
1890
  this.pendingRestore = null;
1127
1891
  this.messageQueue = Promise.resolve();
1128
1892
  this.outboundQueue = Promise.resolve();
1129
- this.dappMetadata = options.dappMetadata;
1130
- this.relayUrl = options.relayUrl ?? DEFAULT_RELAY_URL;
1131
- this.chainId = options.chainId ?? "0x0";
1893
+ this.persistenceQueue = Promise.resolve();
1894
+ this.persistenceBroken = false;
1895
+ this.sessionCreatedAt = Date.now();
1896
+ this.sessionGeneration = 0;
1897
+ this.consecutiveDecryptFailures = 0;
1898
+ const dappMetadata = parseDAppMetadata(options.dappMetadata);
1899
+ if (!dappMetadata) throw new Error("Invalid or unbounded dApp metadata");
1900
+ const initialChainId = canonicalChainId(options.chainId ?? "0x0");
1901
+ if (!initialChainId) throw new Error("Invalid chainId");
1902
+ this.dappMetadata = dappMetadata;
1903
+ this.relayUrl = normalizeRelayUrl(options.relayUrl ?? DEFAULT_RELAY_URL);
1904
+ this.chainId = initialChainId;
1132
1905
  this.storageKey = options.storageKey ?? `${STORAGE_KEY_PREFIX}:session`;
1906
+ this.storage = getBrowserStorage();
1907
+ const lockManager = getBrowserLockManager();
1908
+ this.persistenceEnabled = this.storage !== null && lockManager !== null;
1909
+ this.sessionOwnership = this.persistenceEnabled && lockManager ? new SessionOwnership(this.storageKey, lockManager) : null;
1910
+ if (this.storage && !this.persistenceEnabled) {
1911
+ warn(
1912
+ "ConnectionManager",
1913
+ "Web Locks unavailable; disabling persisted sessions for AEAD safety"
1914
+ );
1915
+ this.removeStoredSession();
1916
+ }
1133
1917
  const stored = this.readStoredSession();
1134
1918
  if (stored) {
1135
1919
  this.channelId = stored.channelId;
@@ -1137,6 +1921,7 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1137
1921
  this.chainId = stored.chainId;
1138
1922
  this.dappMetadata = stored.dappMetadata;
1139
1923
  this.pendingRestore = stored;
1924
+ this.sessionCreatedAt = stored.createdAt;
1140
1925
  log("ConnectionManager", `Found persisted session for channel ${this.channelId}`);
1141
1926
  } else {
1142
1927
  this.channelId = randomUuid();
@@ -1146,50 +1931,35 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1146
1931
  }
1147
1932
  // ── Setup ──────────────────────────────────────────────────
1148
1933
  setupSocketListeners() {
1149
- this.socketClient.on("connected", () => {
1150
- if (this.status === "reconnecting" /* RECONNECTING */) {
1151
- this.failedReconnects = 0;
1152
- }
1153
- });
1154
- this.socketClient.on("reconnected", (result) => {
1155
- if (result?.terminated) {
1156
- log("ConnectionManager", "Channel terminated, observed on auto-reconnect");
1157
- this.handleSessionTerminated();
1158
- return;
1159
- }
1160
- if (result) {
1161
- this.walletPresent = result.participants.includes("wallet");
1162
- }
1163
- if (this.keyExchange?.areKeysExchanged()) {
1164
- if (this.walletPresent) {
1165
- this.clearReconnectProbe();
1166
- this.setStatus("connected" /* CONNECTED */);
1167
- this.failedReconnects = 0;
1168
- } else {
1169
- this.setStatus("waiting" /* WAITING */);
1170
- this.armReconnectProbe();
1171
- }
1172
- } else {
1173
- this.setStatus("waiting" /* WAITING */);
1174
- }
1934
+ const socketClient = this.socketClient;
1935
+ const isCurrentSocket = () => this.socketClient === socketClient;
1936
+ socketClient.on("reconnected", (result) => {
1937
+ void this.handleAutoReconnected(socketClient, result);
1175
1938
  });
1176
- this.socketClient.on("disconnected", () => {
1939
+ socketClient.on("disconnected", () => {
1940
+ if (!isCurrentSocket()) return;
1177
1941
  this.walletPresent = false;
1178
- if (this.status === "connected" /* CONNECTED */) {
1942
+ if (this.keyExchange?.areKeysExchanged() && (this.status === "connected" /* CONNECTED */ || this.status === "reconnecting" /* RECONNECTING */ || this.status === "waiting" /* WAITING */)) {
1179
1943
  this.setStatus("reconnecting" /* RECONNECTING */);
1180
1944
  this.failedReconnects++;
1181
- if (this.failedReconnects >= _ConnectionManager.MAX_RECONNECT_FAILURES) {
1945
+ if (this.failedReconnects === _ConnectionManager.MAX_RECONNECT_FAILURES) {
1182
1946
  this.emit("connection_lost");
1183
1947
  }
1184
1948
  }
1185
1949
  });
1186
- this.socketClient.on("message", (data) => {
1187
- this.enqueueRelayMessage(data);
1950
+ socketClient.on("message", (data) => {
1951
+ if (!isCurrentSocket()) return;
1952
+ const context = this.captureSessionWorkContext();
1953
+ if (context?.socketClient !== socketClient) return;
1954
+ this.enqueueRelayMessage(data, context);
1188
1955
  });
1189
- this.socketClient.on("error", (err) => {
1956
+ socketClient.on("error", (err) => {
1957
+ if (!isCurrentSocket()) return;
1190
1958
  this.emit("error", err);
1191
1959
  });
1192
- this.socketClient.on("participants_changed", (data) => {
1960
+ socketClient.on("participants_changed", (data) => {
1961
+ if (!isCurrentSocket()) return;
1962
+ if (data.clientType !== "wallet") return;
1193
1963
  if (data.event === "join" && data.clientType === "wallet") {
1194
1964
  this.walletPresent = true;
1195
1965
  log("ConnectionManager", "Wallet joined channel");
@@ -1202,97 +1972,248 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1202
1972
  }
1203
1973
  if (data.event === "close") {
1204
1974
  log("ConnectionManager", "Wallet closed the channel (explicit terminate)");
1205
- this.handleSessionTerminated();
1975
+ this.handleSessionTerminated(true);
1206
1976
  return;
1207
1977
  }
1208
1978
  if (data.event === "disconnect" || data.event === "leave") {
1209
- if (data.clientType === "wallet" || !data.clientType) {
1210
- this.walletPresent = false;
1211
- if (this.keyExchange?.areKeysExchanged() && (this.status === "connected" /* CONNECTED */ || this.status === "reconnecting" /* RECONNECTING */)) {
1212
- this.setStatus("waiting" /* WAITING */);
1213
- }
1214
- log("ConnectionManager", "Wallet left channel");
1979
+ this.walletPresent = false;
1980
+ if (this.keyExchange?.areKeysExchanged() && (this.status === "connected" /* CONNECTED */ || this.status === "reconnecting" /* RECONNECTING */)) {
1981
+ this.setStatus("waiting" /* WAITING */);
1215
1982
  }
1983
+ log("ConnectionManager", "Wallet left channel");
1216
1984
  }
1217
1985
  });
1218
1986
  }
1987
+ async handleAutoReconnected(socketClient, result) {
1988
+ if (this.socketClient !== socketClient) return;
1989
+ const context = this.captureSessionWorkContext();
1990
+ if (context?.socketClient !== socketClient) return;
1991
+ if (result.terminated) {
1992
+ log("ConnectionManager", "Channel terminated, observed on auto-reconnect");
1993
+ this.handleSessionTerminated(true);
1994
+ return;
1995
+ }
1996
+ this.walletPresent = result.participants.includes("wallet");
1997
+ for (const message of result.bufferedMessages) {
1998
+ this.enqueueRelayMessage(message, context, false);
1999
+ }
2000
+ await this.messageQueue;
2001
+ if (!this.isSessionWorkCurrent(context)) return;
2002
+ if (context.keyExchange.areKeysExchanged() && this.walletPresent) {
2003
+ this.clearReconnectProbe();
2004
+ this.failedReconnects = 0;
2005
+ this.setStatus("connected" /* CONNECTED */);
2006
+ } else {
2007
+ this.setStatus("waiting" /* WAITING */);
2008
+ if (context.keyExchange.areKeysExchanged()) this.armReconnectProbe();
2009
+ }
2010
+ }
1219
2011
  setupKeyExchangeListeners() {
1220
- if (!this.keyExchange) return;
1221
- this.keyExchange.on("keys_exchanged", () => {
2012
+ const keyExchange = this.keyExchange;
2013
+ const generation = this.sessionGeneration;
2014
+ if (!keyExchange) return;
2015
+ keyExchange.on("keys_exchanged", () => {
2016
+ if (this.keyExchange !== keyExchange || this.sessionGeneration !== generation) return;
1222
2017
  log("ConnectionManager", "Key exchange complete");
1223
2018
  this.clearReconnectProbe();
1224
- this.setStatus("connected" /* CONNECTED */);
1225
2019
  this.walletPresent = true;
1226
2020
  this.failedReconnects = 0;
1227
- void this.persistSession();
1228
- void this.sendEncrypted({
2021
+ const originatorInfoSend = this.sendEncrypted({
1229
2022
  type: "originator_info" /* ORIGINATOR_INFO */,
1230
2023
  originatorInfo: {
1231
2024
  ...this.dappMetadata,
1232
2025
  chainId: this.chainId
1233
2026
  }
1234
2027
  });
2028
+ this.setStatus("connected" /* CONNECTED */);
2029
+ void originatorInfoSend.catch((err) => {
2030
+ error("ConnectionManager", "Failed to send originator info:", err);
2031
+ });
1235
2032
  });
1236
2033
  }
1237
2034
  // ── Public API ─────────────────────────────────────────────
1238
2035
  /**
1239
- * Generate a new v2 connection URI. Rotates channel id and keypair.
2036
+ * Generate a new v3 connection URI. Rotates channel id, keypair, and the
2037
+ * QR-only pairing capability.
1240
2038
  * Returns a `qrlconnect://?q=…` URI safe for QR-rendering or deep-link.
1241
2039
  */
1242
2040
  async getConnectionURI(retryOnConflict = true) {
2041
+ const acquisitionGeneration = this.sessionGeneration;
2042
+ if (!await this.acquireSessionOwnership()) {
2043
+ throw new Error("This QRL Connect session is active in another browser tab");
2044
+ }
2045
+ if (this.sessionGeneration !== acquisitionGeneration) {
2046
+ throw new Error("QRL Connect session changed while browser-tab ownership was pending");
2047
+ }
2048
+ if (retryOnConflict && (this.socketClient.getChannelId() !== null || this.pendingRestore !== null || this.keyExchange !== null)) {
2049
+ await this.resetForNewChannel();
2050
+ }
2051
+ this.invalidateSessionWork();
2052
+ this.socketClient.leaveChannel();
2053
+ this.socketClient.disconnect();
2054
+ this.socketClient = new SocketClient(this.relayUrl, "dapp");
2055
+ this.setupSocketListeners();
1243
2056
  this.setStatus("connecting" /* CONNECTING */);
1244
2057
  this.clearReconnectProbe();
1245
2058
  this.pendingRestore = null;
1246
2059
  this.walletPresent = false;
1247
- if (this.keyExchange) {
1248
- this.keyExchange.reset();
1249
- } else {
1250
- this.keyExchange = new KeyExchange(true);
1251
- this.setupKeyExchangeListeners();
1252
- }
1253
- const pk = this.keyExchange.initiate();
1254
- this.channelId = randomUuid();
1255
- this.socketClient.setPublicKey(toBase64(pk));
1256
- this.socketClient.connect();
2060
+ this.consecutiveDecryptFailures = 0;
2061
+ const accountsWereAuthorized = this.connectedAccounts.length > 0;
2062
+ this.connectedAccounts = [];
2063
+ if (accountsWereAuthorized) this.emit("accounts_changed", []);
1257
2064
  try {
1258
- await this.socketClient.joinChannel(this.channelId);
2065
+ this.removeStoredSessionOrThrow();
1259
2066
  } catch (err) {
1260
- if (retryOnConflict && this.isDappParticipantConflictError(err)) {
1261
- warn(
1262
- "ConnectionManager",
1263
- "Channel already has an active dApp participant. Rotating to a fresh channel."
2067
+ this.setStatus("disconnected" /* DISCONNECTED */);
2068
+ throw err;
2069
+ }
2070
+ this.persistenceBroken = false;
2071
+ this.sessionCreatedAt = Date.now();
2072
+ this.keyExchange = new KeyExchange(true);
2073
+ this.setupKeyExchangeListeners();
2074
+ const initiated = this.keyExchange.initiate();
2075
+ const pk = initiated.publicKey;
2076
+ const capability = initiated.capability;
2077
+ let context = null;
2078
+ try {
2079
+ this.channelId = randomUuid();
2080
+ context = this.captureSessionWorkContext();
2081
+ if (!context) throw new Error("Failed to initialize QRL Connect session");
2082
+ context.socketClient.setPublicKey(toBase64(pk));
2083
+ context.socketClient.connect();
2084
+ try {
2085
+ await context.socketClient.joinChannel(context.channelId);
2086
+ this.assertSessionWorkCurrent(context);
2087
+ } catch (err) {
2088
+ if (!this.isSessionWorkCurrent(context)) throw err;
2089
+ if (retryOnConflict && this.isDappParticipantConflictError(err)) {
2090
+ warn(
2091
+ "ConnectionManager",
2092
+ "Channel already has an active dApp participant. Rotating to a fresh channel."
2093
+ );
2094
+ this.channelId = randomUuid();
2095
+ return await this.getConnectionURI(false);
2096
+ }
2097
+ this.invalidateSessionWork();
2098
+ this.keyExchange = null;
2099
+ context.socketClient.leaveChannel();
2100
+ context.socketClient.disconnect();
2101
+ this.setStatus("disconnected" /* DISCONNECTED */);
2102
+ await this.releaseSessionOwnership();
2103
+ throw err;
2104
+ }
2105
+ this.setStatus("waiting" /* WAITING */);
2106
+ let uri;
2107
+ try {
2108
+ uri = await generateConnectionURI(
2109
+ cidFromString(context.channelId),
2110
+ pk,
2111
+ capability,
2112
+ this.relayUrl === DEFAULT_RELAY_URL ? void 0 : this.relayUrl
1264
2113
  );
1265
- this.channelId = randomUuid();
1266
- return this.getConnectionURI(false);
2114
+ this.assertSessionWorkCurrent(context);
2115
+ } catch (err) {
2116
+ if (this.isSessionWorkCurrent(context)) {
2117
+ this.invalidateSessionWork();
2118
+ this.keyExchange = null;
2119
+ context.socketClient.leaveChannel();
2120
+ context.socketClient.disconnect();
2121
+ this.setStatus("disconnected" /* DISCONNECTED */);
2122
+ await this.releaseSessionOwnership();
2123
+ }
2124
+ throw err;
2125
+ }
2126
+ log("ConnectionManager", `Generated v3 connection URI for channel ${context.channelId}`);
2127
+ return uri;
2128
+ } catch (err) {
2129
+ if (context ? this.isSessionWorkCurrent(context) : this.keyExchange !== null) {
2130
+ this.invalidateSessionWork();
2131
+ this.keyExchange = null;
2132
+ this.socketClient.leaveChannel();
2133
+ this.socketClient.disconnect();
2134
+ this.setStatus("disconnected" /* DISCONNECTED */);
2135
+ await this.releaseSessionOwnership();
1267
2136
  }
1268
- this.setStatus("disconnected" /* DISCONNECTED */);
1269
2137
  throw err;
2138
+ } finally {
2139
+ capability.fill(0);
1270
2140
  }
1271
- this.setStatus("waiting" /* WAITING */);
1272
- const uri = await generateConnectionURI(
1273
- cidFromString(this.channelId),
1274
- pk,
1275
- this.relayUrl === DEFAULT_RELAY_URL ? void 0 : this.relayUrl
1276
- );
1277
- log("ConnectionManager", `Generated v2 connection URI for channel ${this.channelId}`);
1278
- return uri;
1279
2141
  }
1280
2142
  /**
1281
2143
  * Reconnect to an existing session.
1282
2144
  * Returns false if there is nothing to restore.
1283
2145
  */
1284
2146
  async reconnect() {
2147
+ if (this.restoreInFlight) return this.restoreInFlight;
2148
+ const task = this.reconnectNow().finally(() => {
2149
+ this.restoreInFlight = null;
2150
+ });
2151
+ this.restoreInFlight = task;
2152
+ return task;
2153
+ }
2154
+ async reconnectNow() {
2155
+ if (this.keyExchange?.areKeysExchanged()) return true;
1285
2156
  if (!this.pendingRestore) return false;
2157
+ const acquisitionGeneration = this.sessionGeneration;
2158
+ if (!await this.acquireSessionOwnership()) {
2159
+ this.setStatus("disconnected" /* DISCONNECTED */);
2160
+ return false;
2161
+ }
2162
+ if (this.sessionGeneration !== acquisitionGeneration) return false;
2163
+ const latest = this.readStoredSession();
2164
+ if (!latest) {
2165
+ this.pendingRestore = null;
2166
+ this.connectedAccounts = [];
2167
+ const storageInvalidated = this.clearSession();
2168
+ if (storageInvalidated) {
2169
+ await this.releaseSessionOwnership();
2170
+ } else {
2171
+ this.emit(
2172
+ "error",
2173
+ new Error("Unable to invalidate stored session; retaining browser-tab ownership")
2174
+ );
2175
+ }
2176
+ this.setStatus("disconnected" /* DISCONNECTED */);
2177
+ return false;
2178
+ }
2179
+ this.pendingRestore = latest;
2180
+ this.channelId = latest.channelId;
2181
+ this.connectedAccounts = latest.connectedAccounts;
2182
+ this.chainId = latest.chainId;
2183
+ this.dappMetadata = latest.dappMetadata;
2184
+ this.sessionCreatedAt = latest.createdAt;
2185
+ this.invalidateSessionWork();
2186
+ const restoreGeneration = this.sessionGeneration;
2187
+ const restoreChannelId = this.channelId;
2188
+ const restoreSocketClient = this.socketClient;
1286
2189
  this.setStatus("reconnecting" /* RECONNECTING */);
1287
2190
  this.walletPresent = false;
2191
+ this.consecutiveDecryptFailures = 0;
1288
2192
  try {
1289
2193
  const session = await KeyExchange.sessionFromPersisted(this.pendingRestore.keyExchange);
2194
+ if (this.sessionGeneration !== restoreGeneration || this.channelId !== restoreChannelId || this.socketClient !== restoreSocketClient) {
2195
+ return false;
2196
+ }
1290
2197
  this.keyExchange = new KeyExchange(true, session);
2198
+ this.pendingRestore = null;
1291
2199
  this.setupKeyExchangeListeners();
1292
2200
  } catch (err) {
2201
+ if (this.sessionGeneration !== restoreGeneration || this.channelId !== restoreChannelId || this.socketClient !== restoreSocketClient) {
2202
+ return false;
2203
+ }
1293
2204
  error("ConnectionManager", "Failed to hydrate persisted session:", err);
1294
- this.clearSession();
2205
+ const storageInvalidated = this.clearSession();
1295
2206
  this.pendingRestore = null;
2207
+ this.connectedAccounts = [];
2208
+ this.keyExchange = null;
2209
+ if (storageInvalidated) {
2210
+ await this.releaseSessionOwnership();
2211
+ } else {
2212
+ this.emit(
2213
+ "error",
2214
+ new Error("Unable to invalidate stored session; retaining browser-tab ownership")
2215
+ );
2216
+ }
1296
2217
  this.setStatus("disconnected" /* DISCONNECTED */);
1297
2218
  return false;
1298
2219
  }
@@ -1306,23 +2227,27 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1306
2227
  * that the caller has reset walletPresent + set RECONNECTING status.
1307
2228
  */
1308
2229
  async joinAndSettle() {
1309
- this.socketClient.connect();
2230
+ const context = this.captureSessionWorkContext();
2231
+ if (!context) return false;
2232
+ context.socketClient.connect();
1310
2233
  try {
1311
- const { bufferedMessages, participants, terminated } = await this.socketClient.joinChannel(
1312
- this.channelId
2234
+ const { bufferedMessages, participants, terminated } = await context.socketClient.joinChannel(
2235
+ context.channelId
1313
2236
  );
2237
+ if (!this.isSessionWorkCurrent(context)) return false;
1314
2238
  if (terminated) {
1315
2239
  log("ConnectionManager", "Stored session was terminated by the wallet; dropping it");
1316
- this.handleSessionTerminated();
2240
+ this.handleSessionTerminated(true);
1317
2241
  return false;
1318
2242
  }
1319
2243
  if (participants.includes("wallet")) {
1320
2244
  this.walletPresent = true;
1321
2245
  }
1322
2246
  for (const msg of bufferedMessages) {
1323
- this.enqueueRelayMessage(msg);
2247
+ this.enqueueRelayMessage(msg, context, false);
1324
2248
  }
1325
2249
  await this.messageQueue;
2250
+ if (!this.isSessionWorkCurrent(context)) return false;
1326
2251
  if (this.keyExchange?.areKeysExchanged() && this.walletPresent) {
1327
2252
  this.clearReconnectProbe();
1328
2253
  this.setStatus("connected" /* CONNECTED */);
@@ -1332,6 +2257,7 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1332
2257
  }
1333
2258
  return true;
1334
2259
  } catch (err) {
2260
+ if (!this.isSessionWorkCurrent(context)) return false;
1335
2261
  error("ConnectionManager", "Reconnect failed:", err);
1336
2262
  this.setStatus("disconnected" /* DISCONNECTED */);
1337
2263
  return false;
@@ -1360,20 +2286,71 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1360
2286
  }
1361
2287
  }
1362
2288
  }
2289
+ /**
2290
+ * Make sure the socket is open and joined to the paired session's channel
2291
+ * so an outbound request can be routed - or relay-buffered if the wallet is
2292
+ * currently absent (its socket dies within seconds of the app
2293
+ * backgrounding; the relay holds channel traffic for it). Revives the
2294
+ * socket after a probe-timeout teardown and hydrates a cold stored
2295
+ * session. Returns false when there is no session to revive (never paired,
2296
+ * explicit disconnect, or the channel was tombstoned).
2297
+ */
2298
+ async ensureChannelJoined() {
2299
+ if (this.keyExchange?.areKeysExchanged()) {
2300
+ if (this.socketClient.isConnected() && this.socketClient.getChannelId()) {
2301
+ return true;
2302
+ }
2303
+ this.setStatus("reconnecting" /* RECONNECTING */);
2304
+ this.walletPresent = false;
2305
+ return this.joinAndSettle();
2306
+ }
2307
+ if (this.pendingRestore) {
2308
+ return this.reconnect();
2309
+ }
2310
+ return false;
2311
+ }
2312
+ /**
2313
+ * True while a pairing exists (handshake complete, session keys in
2314
+ * memory), even when the wallet's socket is momentarily out of the
2315
+ * channel. Survives the reconnect-probe teardown; false after an explicit
2316
+ * disconnect or a relay tombstone.
2317
+ */
2318
+ isPaired() {
2319
+ return this.keyExchange?.areKeysExchanged() ?? false;
2320
+ }
2321
+ /** True while the relay roster last showed the wallet in the channel. */
2322
+ isWalletPresent() {
2323
+ return this.walletPresent;
2324
+ }
1363
2325
  /**
1364
2326
  * A session was terminated for good (wallet sent a relay 'close', or the
1365
2327
  * join ack reported a tombstone). Clear local state and surface
1366
2328
  * DISCONNECTED so the consumer drops to a fresh-pairing UI.
1367
2329
  */
1368
- handleSessionTerminated() {
2330
+ handleSessionTerminated(relayRetired) {
2331
+ this.invalidateSessionWork();
1369
2332
  this.clearReconnectProbe();
1370
2333
  this.clearUnresponsiveTimer();
1371
2334
  this.walletPresent = false;
2335
+ this.consecutiveDecryptFailures = 0;
1372
2336
  this.pendingRestore = null;
2337
+ const accountsWereAuthorized = this.connectedAccounts.length > 0;
2338
+ this.connectedAccounts = [];
2339
+ if (accountsWereAuthorized) this.emit("accounts_changed", []);
1373
2340
  this.keyExchange = null;
1374
2341
  this.socketClient.leaveChannel();
1375
2342
  this.socketClient.disconnect();
1376
- this.clearSession();
2343
+ const storageInvalidated = this.clearSession();
2344
+ if (storageInvalidated) {
2345
+ void this.releaseSessionOwnership();
2346
+ } else {
2347
+ this.emit(
2348
+ "error",
2349
+ new Error("Unable to invalidate stored session; retaining browser-tab ownership")
2350
+ );
2351
+ }
2352
+ if (!relayRetired) warn("ConnectionManager", "Relay channel termination was not confirmed");
2353
+ this.emit("session_terminated");
1377
2354
  this.setStatus("disconnected" /* DISCONNECTED */);
1378
2355
  }
1379
2356
  armReconnectProbe() {
@@ -1397,28 +2374,70 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1397
2374
  }
1398
2375
  }
1399
2376
  /**
1400
- * Send a JSON-RPC request to the wallet (async, fire-and-forget).
2377
+ * Send a JSON-RPC request to the wallet. Returns the outbound send promise
2378
+ * (settled on the relay's ack) so callers that must sequence on delivery -
2379
+ * e.g. the provider's wallet-wake redirect, which cannot navigate away
2380
+ * before the ciphertext reaches the relay - can await it. Callers may also
2381
+ * ignore it: failures are logged here either way.
1401
2382
  */
1402
2383
  sendJsonRpc(request) {
1403
2384
  if (!this.keyExchange?.areKeysExchanged()) {
1404
2385
  throw new Error("Not connected: key exchange not complete");
1405
2386
  }
1406
- this.sendEncrypted({
2387
+ if (!isValidJsonRpcId(request.id)) {
2388
+ throw new Error("Invalid JSON-RPC request id");
2389
+ }
2390
+ if (!isValidJsonRpcMethod(request.method) || classifyRpcMethod(request.method) === "unsupported") {
2391
+ throw new Error("Unsupported JSON-RPC method");
2392
+ }
2393
+ if (request.params !== void 0 && !Array.isArray(request.params)) {
2394
+ throw new Error("JSON-RPC params must be an array");
2395
+ }
2396
+ const sent = this.sendEncrypted({
1407
2397
  type: "jsonrpc" /* JSONRPC */,
1408
2398
  jsonrpc: "2.0",
1409
2399
  id: request.id,
1410
2400
  method: request.method,
1411
2401
  params: request.params
1412
- }).catch((err) => {
2402
+ });
2403
+ sent.catch((err) => {
1413
2404
  error("ConnectionManager", "Failed to send JSON-RPC:", err);
1414
2405
  });
1415
2406
  this.startUnresponsiveTimer();
2407
+ return sent;
1416
2408
  }
1417
2409
  getStatus() {
1418
2410
  return this.status;
1419
2411
  }
1420
2412
  getAccounts() {
1421
- return this.connectedAccounts;
2413
+ return [...this.connectedAccounts];
2414
+ }
2415
+ /**
2416
+ * Commit accounts returned by an approved qrl_requestAccounts response.
2417
+ * WALLET_INFO alone cannot grant this authorization.
2418
+ */
2419
+ async authorizeAccounts(value) {
2420
+ if (!isCurrentQrlAddressArray(value) || value.length === 0) {
2421
+ throw new Error("qrl_requestAccounts returned an invalid account list");
2422
+ }
2423
+ const context = this.captureSessionWorkContext();
2424
+ if (!context?.keyExchange.areKeysExchanged()) {
2425
+ throw new Error("Cannot authorize accounts without an established session");
2426
+ }
2427
+ const nextAccounts = [...value];
2428
+ const previousAccounts = this.connectedAccounts;
2429
+ const changed = !this.areArraysEqual(previousAccounts, nextAccounts);
2430
+ this.connectedAccounts = nextAccounts;
2431
+ try {
2432
+ await this.persistSession(context);
2433
+ this.assertSessionWorkCurrent(context);
2434
+ } catch (error2) {
2435
+ if (this.isSessionWorkCurrent(context)) this.connectedAccounts = previousAccounts;
2436
+ await this.teardownPersistenceFailedSession(error2, context);
2437
+ throw error2;
2438
+ }
2439
+ if (changed) this.emit("accounts_changed", [...this.connectedAccounts]);
2440
+ return [...this.connectedAccounts];
1422
2441
  }
1423
2442
  getChainId() {
1424
2443
  return this.chainId;
@@ -1428,9 +2447,9 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1428
2447
  }
1429
2448
  /** Check (sync) if a persisted session exists and has not expired. */
1430
2449
  hasStoredSession() {
1431
- if (typeof localStorage === "undefined") return false;
2450
+ if (!this.persistenceEnabled || !this.storage) return false;
1432
2451
  try {
1433
- const raw = localStorage.getItem(this.storageKey);
2452
+ const raw = this.storage.getItem(this.storageKey);
1434
2453
  if (!raw) return false;
1435
2454
  const session = parseStoredSession(raw);
1436
2455
  if (!session) return false;
@@ -1452,6 +2471,26 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1452
2471
  const timeout = new Promise((resolve) => setTimeout(resolve, TERMINATE_SEND_TIMEOUT_MS));
1453
2472
  await Promise.race([send, timeout]);
1454
2473
  }
2474
+ /**
2475
+ * Ensure this socket is a relay participant before closing a restorable or
2476
+ * temporarily disconnected channel. close_channel is intentionally
2477
+ * participant-only on the relay, so a cold stored session cannot be retired
2478
+ * by emitting the close event from an unjoined socket.
2479
+ */
2480
+ async prepareRelayChannelForRetirement() {
2481
+ const socketChannelId = this.socketClient.getChannelId();
2482
+ const fallbackChannelId = this.pendingRestore !== null || this.keyExchange !== null ? this.channelId : null;
2483
+ const channelId = socketChannelId ?? fallbackChannelId;
2484
+ if (!channelId) return { hadRelayChannel: false, alreadyRetired: false };
2485
+ if (!this.socketClient.isConnected() || socketChannelId !== channelId) {
2486
+ this.socketClient.connect();
2487
+ const result = await this.socketClient.joinChannel(channelId);
2488
+ if (result.terminated) {
2489
+ return { hadRelayChannel: true, alreadyRetired: true };
2490
+ }
2491
+ }
2492
+ return { hadRelayChannel: true, alreadyRetired: false };
2493
+ }
1455
2494
  /**
1456
2495
  * Reset to a fresh channel + keypair. Sends TERMINATE to any live peer,
1457
2496
  * drops the persisted session, and prepares a clean state for
@@ -1461,60 +2500,118 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1461
2500
  this.clearUnresponsiveTimer();
1462
2501
  this.clearReconnectProbe();
1463
2502
  this.walletPresent = false;
1464
- await this.flushTerminate();
2503
+ const retirement = await this.prepareRelayChannelForRetirement();
2504
+ let relayRetired = retirement.alreadyRetired;
2505
+ try {
2506
+ if (!relayRetired) {
2507
+ await this.flushTerminate();
2508
+ relayRetired = await this.socketClient.closeChannel();
2509
+ }
2510
+ } catch (err) {
2511
+ error("ConnectionManager", "Failed to tombstone reset channel:", err);
2512
+ }
2513
+ if (retirement.hadRelayChannel && !relayRetired) {
2514
+ throw new Error("Unable to retire the previous relay channel");
2515
+ }
2516
+ this.invalidateSessionWork();
1465
2517
  this.socketClient.leaveChannel();
1466
2518
  this.socketClient.disconnect();
1467
- this.clearSession();
2519
+ const storageInvalidated = this.clearSession();
2520
+ const accountsWereAuthorized = this.connectedAccounts.length > 0;
1468
2521
  this.connectedAccounts = [];
2522
+ if (accountsWereAuthorized) this.emit("accounts_changed", []);
1469
2523
  this.pendingRestore = null;
1470
2524
  this.channelId = randomUuid();
1471
2525
  this.keyExchange = null;
2526
+ this.persistenceBroken = false;
2527
+ this.sessionCreatedAt = Date.now();
1472
2528
  this.socketClient = new SocketClient(this.relayUrl, "dapp");
1473
2529
  this.setupSocketListeners();
1474
2530
  this.setStatus("disconnected" /* DISCONNECTED */);
2531
+ if (!storageInvalidated) {
2532
+ const error2 = new Error(
2533
+ "Unable to invalidate stored session; retaining browser-tab ownership"
2534
+ );
2535
+ this.emit("error", error2);
2536
+ throw error2;
2537
+ }
1475
2538
  }
1476
2539
  async disconnect() {
1477
2540
  this.clearUnresponsiveTimer();
1478
2541
  this.clearReconnectProbe();
1479
2542
  this.walletPresent = false;
1480
- await this.flushTerminate();
2543
+ let hadRelayChannel = this.socketClient.getChannelId() !== null || this.pendingRestore !== null || this.keyExchange !== null;
2544
+ let relayRetired = false;
2545
+ try {
2546
+ const retirement = await this.prepareRelayChannelForRetirement();
2547
+ hadRelayChannel = retirement.hadRelayChannel;
2548
+ relayRetired = retirement.alreadyRetired;
2549
+ if (hadRelayChannel && !relayRetired) {
2550
+ await this.flushTerminate();
2551
+ relayRetired = await this.socketClient.closeChannel();
2552
+ }
2553
+ } catch (err) {
2554
+ error("ConnectionManager", "Failed to tombstone disconnected channel:", err);
2555
+ }
2556
+ if (hadRelayChannel && !relayRetired) {
2557
+ throw new Error("Unable to retire the relay channel");
2558
+ }
2559
+ this.invalidateSessionWork();
1481
2560
  this.socketClient.leaveChannel();
1482
2561
  this.socketClient.disconnect();
1483
- this.setStatus("disconnected" /* DISCONNECTED */);
1484
- this.clearSession();
2562
+ const storageInvalidated = this.clearSession();
1485
2563
  this.connectedAccounts = [];
1486
2564
  this.pendingRestore = null;
1487
2565
  this.keyExchange = null;
2566
+ if (storageInvalidated) await this.releaseSessionOwnership();
2567
+ this.setStatus("disconnected" /* DISCONNECTED */);
2568
+ if (hadRelayChannel && !relayRetired) {
2569
+ warn("ConnectionManager", "Disconnected channel termination was not confirmed");
2570
+ }
2571
+ if (!storageInvalidated) {
2572
+ const error2 = new Error(
2573
+ "Unable to invalidate stored session; retaining browser-tab ownership"
2574
+ );
2575
+ this.emit("error", error2);
2576
+ throw error2;
2577
+ }
1488
2578
  }
1489
2579
  // ── Internals ──────────────────────────────────────────────
1490
- enqueueRelayMessage(data) {
2580
+ enqueueRelayMessage(data, context, settleConnection = true) {
1491
2581
  if (!isRelayMessage(data)) {
1492
2582
  warn("ConnectionManager", "Dropping malformed relay envelope");
1493
2583
  return;
1494
2584
  }
1495
- this.messageQueue = this.messageQueue.then(() => this.handleRelayMessage(data)).catch((err) => {
1496
- error("ConnectionManager", "messageQueue handler error:", err);
1497
- });
1498
- }
1499
- async handleRelayMessage(data) {
1500
- if (data.clientType === "wallet") {
1501
- this.walletPresent = true;
1502
- this.clearReconnectProbe();
1503
- if (this.keyExchange?.areKeysExchanged() && this.status !== "connected" /* CONNECTED */) {
2585
+ if (data.id !== context.channelId || data.clientType !== "wallet") {
2586
+ warn("ConnectionManager", "Dropping relay envelope for the wrong channel or peer role");
2587
+ return;
2588
+ }
2589
+ this.messageQueue = this.messageQueue.then(() => this.handleRelayMessage(data, context)).then(() => {
2590
+ if (settleConnection && this.isSessionWorkCurrent(context) && context.keyExchange.areKeysExchanged() && this.walletPresent) {
1504
2591
  this.failedReconnects = 0;
1505
2592
  this.setStatus("connected" /* CONNECTED */);
1506
2593
  }
1507
- }
2594
+ }).catch((err) => {
2595
+ error("ConnectionManager", "messageQueue handler error:", err);
2596
+ });
2597
+ }
2598
+ async handleRelayMessage(data, context) {
2599
+ if (!this.isSessionWorkCurrent(context)) return;
2600
+ this.walletPresent = true;
2601
+ this.clearReconnectProbe();
1508
2602
  const message = data.message;
1509
- if (isRecord(message)) {
2603
+ if (isRecord2(message)) {
1510
2604
  if (message.type === "key_handshake_SYNACK" /* SYNACK */) {
1511
- if (typeof message.ct === "string" && typeof message.c0 === "string") {
1512
- await this.handleSynAck({
1513
- type: "key_handshake_SYNACK" /* SYNACK */,
1514
- ct: message.ct,
1515
- c0: message.c0,
1516
- v: typeof message.v === "number" ? message.v : 0
1517
- });
2605
+ if (isCanonicalBase64OfLength(message.ct, ML_KEM_768_CT_LEN) && isCanonicalBase64OfLength(message.c0, SYNACK_C0_LEN)) {
2606
+ await this.handleSynAck(
2607
+ {
2608
+ type: "key_handshake_SYNACK" /* SYNACK */,
2609
+ ct: message.ct,
2610
+ c0: message.c0,
2611
+ v: typeof message.v === "number" ? message.v : 0
2612
+ },
2613
+ context
2614
+ );
1518
2615
  } else {
1519
2616
  warn("ConnectionManager", "Dropping malformed SYNACK");
1520
2617
  }
@@ -1525,60 +2622,157 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1525
2622
  return;
1526
2623
  }
1527
2624
  }
1528
- if (typeof message === "string" && this.keyExchange?.areKeysExchanged()) {
2625
+ if (typeof message === "string" && context.keyExchange.areKeysExchanged()) {
2626
+ let decrypted;
2627
+ try {
2628
+ decrypted = await context.keyExchange.decryptMessage(message);
2629
+ } catch (err) {
2630
+ if (!this.isSessionWorkCurrent(context)) return;
2631
+ error("ConnectionManager", "Failed to decrypt message:", err);
2632
+ this.consecutiveDecryptFailures++;
2633
+ if (this.consecutiveDecryptFailures >= _ConnectionManager.MAX_DECRYPT_FAILURES) {
2634
+ await this.teardownDesyncedSession(context);
2635
+ }
2636
+ return;
2637
+ }
2638
+ if (!this.isSessionWorkCurrent(context)) return;
2639
+ this.consecutiveDecryptFailures = 0;
2640
+ try {
2641
+ await this.persistSession(context);
2642
+ } catch (err) {
2643
+ await this.teardownPersistenceFailedSession(err, context);
2644
+ return;
2645
+ }
2646
+ if (!this.isSessionWorkCurrent(context)) return;
1529
2647
  try {
1530
- const decrypted = await this.keyExchange.decryptMessage(message);
1531
- await this.persistSession();
1532
2648
  const parsed = JSON.parse(decrypted);
1533
- if (isRecord(parsed)) {
1534
- this.handleDecryptedMessage(parsed);
2649
+ if (isRecord2(parsed)) {
2650
+ await this.handleDecryptedMessage(parsed, context);
1535
2651
  } else {
1536
2652
  warn("ConnectionManager", "Dropping non-object decrypted payload");
1537
2653
  }
1538
2654
  } catch (err) {
1539
- error("ConnectionManager", "Failed to decrypt message:", err);
2655
+ error("ConnectionManager", "Failed to handle decrypted message:", err);
1540
2656
  }
1541
2657
  }
1542
2658
  }
1543
- async handleSynAck(msg) {
1544
- if (!this.keyExchange) return;
2659
+ /**
2660
+ * The receive stream is cryptographically unrecoverable: the peer's send
2661
+ * counter is ahead of our recv counter for good (relay buffer TTL/cap
2662
+ * dropped a ciphertext), so every future message would fail its tag.
2663
+ * An encrypted TERMINATE cannot communicate this (the desynced peer could
2664
+ * not open it either); tombstone the channel on the relay instead so the
2665
+ * wallet learns the pairing is dead even if it only re-joins later, then
2666
+ * clear local state and surface the standard terminated teardown.
2667
+ */
2668
+ async teardownDesyncedSession(context) {
2669
+ if (!this.isSessionWorkCurrent(context)) return;
2670
+ warn("ConnectionManager", "AEAD stream desynced beyond recovery; terminating session");
2671
+ let relayRetired = false;
2672
+ try {
2673
+ relayRetired = await context.socketClient.closeChannel();
2674
+ } catch (err) {
2675
+ error("ConnectionManager", "Failed to tombstone desynced channel:", err);
2676
+ }
2677
+ if (!this.isSessionWorkCurrent(context)) return;
2678
+ this.handleSessionTerminated(relayRetired);
2679
+ }
2680
+ async teardownPersistenceFailedSession(cause, context = this.captureSessionWorkContext(), reason = "AEAD counter persistence failed; terminating session before further use") {
2681
+ if (!context || !this.isSessionWorkCurrent(context)) return;
2682
+ error("ConnectionManager", `${reason}:`, cause);
2683
+ let relayRetired = false;
2684
+ try {
2685
+ relayRetired = await context.socketClient.closeChannel();
2686
+ } catch (err) {
2687
+ error("ConnectionManager", "Failed to tombstone unusable channel:", err);
2688
+ }
2689
+ if (!this.isSessionWorkCurrent(context)) return;
2690
+ this.handleSessionTerminated(relayRetired);
2691
+ }
2692
+ async handleSynAck(msg, context) {
2693
+ if (!this.isSessionWorkCurrent(context)) return;
1545
2694
  this.setStatus("key_exchange" /* KEY_EXCHANGE */);
1546
2695
  let response;
1547
2696
  try {
1548
- response = await this.keyExchange.onSynAck(cidFromString(this.channelId), msg);
2697
+ response = await context.keyExchange.onSynAck(cidFromString(context.channelId), msg);
1549
2698
  } catch (err) {
2699
+ if (!this.isSessionWorkCurrent(context)) return;
1550
2700
  const e = err instanceof Error ? err : new Error(String(err));
1551
2701
  error("ConnectionManager", "SYNACK processing failed:", e);
1552
2702
  this.emit("error", e);
2703
+ let relayRetired = false;
2704
+ try {
2705
+ relayRetired = await context.socketClient.closeChannel();
2706
+ } catch (closeError) {
2707
+ error("ConnectionManager", "Failed to tombstone rejected handshake:", closeError);
2708
+ }
2709
+ if (this.isSessionWorkCurrent(context)) this.handleSessionTerminated(relayRetired);
1553
2710
  return;
1554
2711
  }
2712
+ if (!this.isSessionWorkCurrent(context)) return;
1555
2713
  if (response) {
1556
- this.sendPlaintext(response);
2714
+ try {
2715
+ await this.sendPlaintext(response, context);
2716
+ this.assertSessionWorkCurrent(context);
2717
+ context.keyExchange.confirmOriginatorAckDelivered();
2718
+ } catch (err) {
2719
+ if (!this.isSessionWorkCurrent(context)) return;
2720
+ const error2 = err instanceof Error ? err : new Error(String(err));
2721
+ error("ConnectionManager", "ACK delivery failed; retiring provisional session:", error2);
2722
+ this.emit("error", error2);
2723
+ let relayRetired = false;
2724
+ try {
2725
+ relayRetired = await context.socketClient.closeChannel();
2726
+ } catch (closeError) {
2727
+ error("ConnectionManager", "Failed to tombstone ambiguous handshake:", closeError);
2728
+ }
2729
+ if (this.isSessionWorkCurrent(context)) this.handleSessionTerminated(relayRetired);
2730
+ }
1557
2731
  return;
1558
2732
  }
1559
- const cachedAck = this.keyExchange.getLastAck();
1560
- if (this.keyExchange.areKeysExchanged() && cachedAck) {
2733
+ const cachedAck = context.keyExchange.getLastAck();
2734
+ if (context.keyExchange.areKeysExchanged() && cachedAck) {
1561
2735
  log("ConnectionManager", "Duplicate SYNACK after handshake; re-sending cached ACK");
1562
- this.sendPlaintext(cachedAck);
2736
+ try {
2737
+ await this.sendPlaintext(cachedAck, context);
2738
+ } catch (err) {
2739
+ error("ConnectionManager", "Failed to re-send cached ACK:", err);
2740
+ }
1563
2741
  }
1564
2742
  }
1565
- handleDecryptedMessage(msg) {
2743
+ async handleDecryptedMessage(msg, context) {
2744
+ if (!this.isSessionWorkCurrent(context)) return;
1566
2745
  this.clearUnresponsiveTimer();
1567
2746
  const type = typeof msg.type === "string" ? msg.type : "";
1568
2747
  switch (MESSAGE_TYPE_BY_VALUE[type]) {
1569
2748
  case "wallet_info" /* WALLET_INFO */: {
1570
- const nextAccounts = isStringArray(msg.accounts) ? msg.accounts : [];
1571
- const nextChainId = typeof msg.chainId === "string" && msg.chainId ? msg.chainId : this.chainId;
1572
- const accountsChanged = !this.areArraysEqual(this.connectedAccounts, nextAccounts);
1573
- const chainChanged = this.chainId !== nextChainId;
1574
- this.connectedAccounts = nextAccounts;
1575
- this.chainId = nextChainId;
1576
- void this.persistSession();
2749
+ if (!isCurrentQrlAddressArray(msg.accounts)) {
2750
+ warn("ConnectionManager", "Dropping wallet info with malformed account addresses");
2751
+ break;
2752
+ }
2753
+ const reportedAccounts = [...msg.accounts];
2754
+ const nextAccounts = this.connectedAccounts.length > 0 && this.areArraysEqual(this.connectedAccounts, reportedAccounts) ? this.connectedAccounts : [];
2755
+ let nextChainId = this.chainId;
2756
+ if (msg.chainId !== void 0) {
2757
+ const parsedChainId = canonicalChainId(msg.chainId);
2758
+ if (!parsedChainId || parsedChainId !== msg.chainId) {
2759
+ warn("ConnectionManager", "Dropping wallet info with malformed chain id");
2760
+ break;
2761
+ }
2762
+ nextChainId = parsedChainId;
2763
+ }
2764
+ const accountsChanged = !this.areArraysEqual(this.connectedAccounts, nextAccounts);
2765
+ const chainChanged = this.chainId !== nextChainId;
2766
+ this.connectedAccounts = [...nextAccounts];
2767
+ this.chainId = nextChainId;
2768
+ void this.persistSession(context).catch((err) => {
2769
+ void this.teardownPersistenceFailedSession(err, context);
2770
+ });
1577
2771
  this.emit("wallet_info", {
1578
- accounts: this.connectedAccounts,
2772
+ accounts: [...this.connectedAccounts],
1579
2773
  chainId: this.chainId
1580
2774
  });
1581
- if (accountsChanged) this.emit("accounts_changed", this.connectedAccounts);
2775
+ if (accountsChanged) this.emit("accounts_changed", [...this.connectedAccounts]);
1582
2776
  if (chainChanged) this.emit("chain_changed", this.chainId);
1583
2777
  break;
1584
2778
  }
@@ -1593,21 +2787,29 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1593
2787
  }
1594
2788
  case "terminate" /* TERMINATE */: {
1595
2789
  log("ConnectionManager", "Received terminate from wallet");
1596
- void this.disconnect();
2790
+ let relayRetired = false;
2791
+ try {
2792
+ relayRetired = await context.socketClient.closeChannel();
2793
+ } catch (err) {
2794
+ error("ConnectionManager", "Failed to tombstone terminated channel:", err);
2795
+ }
2796
+ if (this.isSessionWorkCurrent(context)) this.handleSessionTerminated(relayRetired);
1597
2797
  break;
1598
2798
  }
1599
2799
  default:
1600
2800
  log("ConnectionManager", `Unhandled message type: ${type}`);
1601
2801
  }
1602
2802
  }
1603
- sendPlaintext(message) {
1604
- this.socketClient.sendMessage({
1605
- id: this.channelId,
2803
+ async sendPlaintext(message, context = this.captureSessionWorkContext()) {
2804
+ if (!context || !this.isSessionWorkCurrent(context)) {
2805
+ throw new Error("Cannot send plaintext for a retired session");
2806
+ }
2807
+ await context.socketClient.sendMessage({
2808
+ id: context.channelId,
1606
2809
  clientType: "dapp",
1607
2810
  message
1608
- }).catch((err) => {
1609
- error("ConnectionManager", "Failed to send plaintext:", err);
1610
2811
  });
2812
+ this.assertSessionWorkCurrent(context);
1611
2813
  }
1612
2814
  /**
1613
2815
  * Serialize every encrypt+persist+send on a single outbound queue.
@@ -1618,24 +2820,53 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1618
2820
  * already prevented by the synchronous seq reservation in KeyExchange.)
1619
2821
  */
1620
2822
  sendEncrypted(message) {
1621
- const task = this.outboundQueue.then(() => this.sendEncryptedNow(message));
2823
+ const context = this.captureSessionWorkContext();
2824
+ if (!context) return Promise.reject(new Error("sendEncrypted: not connected"));
2825
+ const task = this.outboundQueue.then(() => this.sendEncryptedNow(message, context));
1622
2826
  this.outboundQueue = task.then(
1623
2827
  () => void 0,
1624
2828
  () => void 0
1625
2829
  );
1626
2830
  return task;
1627
2831
  }
1628
- async sendEncryptedNow(message) {
1629
- if (!this.keyExchange?.areKeysExchanged()) {
2832
+ async sendEncryptedNow(message, context) {
2833
+ if (!context.keyExchange.areKeysExchanged()) {
1630
2834
  throw new Error("sendEncrypted: not connected");
1631
2835
  }
1632
- const encrypted = await this.keyExchange.encryptMessage(JSON.stringify(message));
1633
- await this.persistSession();
1634
- await this.socketClient.sendMessage({
1635
- id: this.channelId,
1636
- clientType: "dapp",
1637
- message: encrypted
1638
- });
2836
+ this.assertSessionWorkCurrent(context);
2837
+ let encrypted;
2838
+ try {
2839
+ encrypted = await context.keyExchange.encryptMessage(JSON.stringify(message));
2840
+ } catch (err) {
2841
+ await this.teardownPersistenceFailedSession(
2842
+ err,
2843
+ context,
2844
+ "Message encryption failed after counter reservation; terminating session"
2845
+ );
2846
+ throw err;
2847
+ }
2848
+ this.assertSessionWorkCurrent(context);
2849
+ try {
2850
+ await this.persistSession(context);
2851
+ } catch (err) {
2852
+ await this.teardownPersistenceFailedSession(err, context);
2853
+ throw err;
2854
+ }
2855
+ this.assertSessionWorkCurrent(context);
2856
+ try {
2857
+ await context.socketClient.sendMessage({
2858
+ id: context.channelId,
2859
+ clientType: "dapp",
2860
+ message: encrypted
2861
+ });
2862
+ } catch (err) {
2863
+ await this.teardownPersistenceFailedSession(
2864
+ err,
2865
+ context,
2866
+ "Relay send outcome unknown; terminating session before further use"
2867
+ );
2868
+ throw err;
2869
+ }
1639
2870
  }
1640
2871
  startUnresponsiveTimer() {
1641
2872
  this.clearUnresponsiveTimer();
@@ -1665,40 +2896,93 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1665
2896
  return true;
1666
2897
  }
1667
2898
  // ── Persistence ────────────────────────────────────────────
1668
- async persistSession() {
1669
- if (typeof localStorage === "undefined") return;
1670
- if (!this.keyExchange) return;
1671
- const persistedKex = await this.keyExchange.exportPersisted();
1672
- if (!persistedKex) return;
1673
- const session = {
1674
- version: 3,
2899
+ invalidateSessionWork() {
2900
+ this.sessionGeneration++;
2901
+ this.keyExchange?.reset();
2902
+ this.messageQueue = Promise.resolve();
2903
+ this.outboundQueue = Promise.resolve();
2904
+ this.persistenceQueue = Promise.resolve();
2905
+ }
2906
+ captureSessionWorkContext() {
2907
+ if (!this.keyExchange) return null;
2908
+ return {
2909
+ generation: this.sessionGeneration,
1675
2910
  channelId: this.channelId,
2911
+ keyExchange: this.keyExchange,
2912
+ socketClient: this.socketClient
2913
+ };
2914
+ }
2915
+ isSessionWorkCurrent(context) {
2916
+ return context.generation === this.sessionGeneration && context.channelId === this.channelId && context.keyExchange === this.keyExchange && context.socketClient === this.socketClient;
2917
+ }
2918
+ assertSessionWorkCurrent(context) {
2919
+ if (!this.isSessionWorkCurrent(context)) {
2920
+ throw new Error("QRL Connect session changed while encrypted work was pending");
2921
+ }
2922
+ }
2923
+ async acquireSessionOwnership() {
2924
+ if (!this.persistenceEnabled) return true;
2925
+ if (!this.sessionOwnership) return false;
2926
+ return this.sessionOwnership.acquire();
2927
+ }
2928
+ async releaseSessionOwnership() {
2929
+ await this.sessionOwnership?.release();
2930
+ }
2931
+ persistSession(context) {
2932
+ const task = this.persistenceQueue.then(() => this.persistSessionNow(context));
2933
+ this.persistenceQueue = task.then(
2934
+ () => void 0,
2935
+ () => void 0
2936
+ );
2937
+ return task;
2938
+ }
2939
+ async persistSessionNow(context) {
2940
+ this.assertSessionWorkCurrent(context);
2941
+ if (!this.persistenceEnabled || !this.storage) return;
2942
+ if (!this.sessionOwnership?.isOwned()) {
2943
+ throw new Error("Cannot persist AEAD counters without browser-tab ownership");
2944
+ }
2945
+ if (this.persistenceBroken) {
2946
+ throw new Error("AEAD counter persistence is unavailable");
2947
+ }
2948
+ const persistedKex = await context.keyExchange.exportPersisted();
2949
+ if (!persistedKex) throw new Error("Cannot export established AEAD session");
2950
+ this.assertSessionWorkCurrent(context);
2951
+ if (!this.sessionOwnership?.isOwned()) {
2952
+ throw new Error("Browser-tab ownership changed while persisting AEAD counters");
2953
+ }
2954
+ const session = {
2955
+ version: 5,
2956
+ channelId: context.channelId,
1676
2957
  keyExchange: persistedKex,
1677
2958
  dappMetadata: this.dappMetadata,
1678
2959
  connectedAccounts: this.connectedAccounts,
1679
2960
  chainId: this.chainId,
1680
- createdAt: this.pendingRestore?.createdAt ?? Date.now(),
2961
+ createdAt: this.sessionCreatedAt,
1681
2962
  lastActivity: Date.now()
1682
2963
  };
1683
2964
  try {
1684
- localStorage.setItem(this.storageKey, JSON.stringify(session));
2965
+ this.storage.setItem(this.storageKey, JSON.stringify(session));
1685
2966
  } catch (err) {
1686
- warn("ConnectionManager", "localStorage.setItem failed:", err);
2967
+ this.persistenceBroken = true;
2968
+ this.removeStoredSession();
2969
+ error("ConnectionManager", "Failed to persist AEAD counters:", err);
2970
+ throw new Error("Failed to persist AEAD counters");
1687
2971
  }
1688
2972
  }
1689
2973
  readStoredSession() {
1690
- if (typeof localStorage === "undefined") return null;
2974
+ if (!this.persistenceEnabled || !this.storage) return null;
1691
2975
  try {
1692
- const raw = localStorage.getItem(this.storageKey);
2976
+ const raw = this.storage.getItem(this.storageKey);
1693
2977
  if (!raw) return null;
1694
2978
  const session = parseStoredSession(raw);
1695
2979
  if (!session) {
1696
2980
  log("ConnectionManager", "Dropping legacy or malformed session from storage");
1697
- localStorage.removeItem(this.storageKey);
2981
+ this.removeStoredSession();
1698
2982
  return null;
1699
2983
  }
1700
2984
  if (Date.now() - session.createdAt > SESSION_TTL_MS) {
1701
- localStorage.removeItem(this.storageKey);
2985
+ this.removeStoredSession();
1702
2986
  return null;
1703
2987
  }
1704
2988
  return session;
@@ -1706,452 +2990,207 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
1706
2990
  return null;
1707
2991
  }
1708
2992
  }
2993
+ removeStoredSession() {
2994
+ if (!this.storage) return true;
2995
+ try {
2996
+ this.storage.removeItem(this.storageKey);
2997
+ return true;
2998
+ } catch (removeError) {
2999
+ try {
3000
+ this.storage.setItem(this.storageKey, '{"version":0}');
3001
+ warn("ConnectionManager", "localStorage.removeItem failed; invalidated record instead");
3002
+ return true;
3003
+ } catch (overwriteError) {
3004
+ warn(
3005
+ "ConnectionManager",
3006
+ "Unable to invalidate persisted session:",
3007
+ removeError,
3008
+ overwriteError
3009
+ );
3010
+ return false;
3011
+ }
3012
+ }
3013
+ }
3014
+ removeStoredSessionOrThrow() {
3015
+ if (!this.removeStoredSession()) {
3016
+ throw new Error("Unable to clear the previous persisted AEAD session");
3017
+ }
3018
+ }
1709
3019
  clearSession() {
1710
- if (typeof localStorage === "undefined") return;
1711
- localStorage.removeItem(this.storageKey);
3020
+ return this.removeStoredSession();
1712
3021
  }
1713
3022
  };
1714
3023
  _ConnectionManager.MAX_RECONNECT_FAILURES = 5;
3024
+ // Nonces derive from the recv counter and there is no gap tolerance, so a
3025
+ // genuinely desynced stream (peer advanced past us, e.g. the relay's
3026
+ // buffer TTL dropped a ciphertext) fails EVERY subsequent open. Two in a
3027
+ // row cannot happen on a healthy stream; requiring the second guards
3028
+ // against a one-off injected junk ciphertext killing a live session.
3029
+ _ConnectionManager.MAX_DECRYPT_FAILURES = 2;
1715
3030
  var ConnectionManager = _ConnectionManager;
1716
3031
 
1717
- // src/utils/platform.ts
1718
- function isMobileBrowser() {
1719
- if (typeof navigator === "undefined") return false;
1720
- return /Android|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(navigator.userAgent);
1721
- }
1722
- function getAppStoreUrl() {
1723
- if (typeof navigator !== "undefined" && /android/i.test(navigator.userAgent)) {
1724
- return "https://play.google.com/store/apps/details?id=com.chiefdg.myqrlwallet";
3032
+ // src/signing/ctx.ts
3033
+ var SCHEME_VERSION_MSG = "QRL-SIGN-MSG-v1";
3034
+ var SCHEME_VERSION_TYPED = "QRL-SIGN-TYPED-v1";
3035
+ var SCHEME_TAG_MSG = new TextEncoder().encode(SCHEME_VERSION_MSG);
3036
+ var SCHEME_TAG_TYPED = new TextEncoder().encode(SCHEME_VERSION_TYPED);
3037
+ var DIGEST_LEN = 64;
3038
+
3039
+ // src/signing/bytes.ts
3040
+ function hexToBytes(hex) {
3041
+ if (typeof hex !== "string") throw new Error("hex must be a string");
3042
+ if (!/^0x([0-9a-fA-F]{2})*$/.test(hex)) {
3043
+ throw new Error("hex must be 0x-prefixed even-length");
1725
3044
  }
1726
- return "https://apps.apple.com/app/myqrlwallet/id6742219498";
3045
+ const n = (hex.length - 2) / 2;
3046
+ const out = new Uint8Array(n);
3047
+ for (let i = 0; i < n; i++) {
3048
+ out[i] = parseInt(hex.slice(2 + i * 2, 4 + i * 2), 16);
3049
+ }
3050
+ return out;
1727
3051
  }
1728
- function attemptWalletRedirect(uri, timeoutMs = 1800) {
1729
- if (typeof window === "undefined" || typeof document === "undefined") {
1730
- return Promise.resolve(false);
3052
+ function bytesToHex(bytes) {
3053
+ if (!(bytes instanceof Uint8Array)) {
3054
+ throw new Error("bytes must be a Uint8Array");
1731
3055
  }
1732
- if (!uri.trim().toLowerCase().startsWith("qrlconnect:")) {
1733
- return Promise.resolve(false);
3056
+ let s = "0x";
3057
+ for (const v of bytes) s += v.toString(16).padStart(2, "0");
3058
+ return s;
3059
+ }
3060
+ function concatBytes(...parts) {
3061
+ return concatBytesArr(parts);
3062
+ }
3063
+ function concatBytesArr(parts) {
3064
+ let total = 0;
3065
+ for (const p of parts) total += p.length;
3066
+ const out = new Uint8Array(total);
3067
+ let off = 0;
3068
+ for (const p of parts) {
3069
+ out.set(p, off);
3070
+ off += p.length;
1734
3071
  }
1735
- return new Promise((resolve) => {
1736
- let settled = false;
1737
- const finish = (opened) => {
1738
- if (settled) return;
1739
- settled = true;
1740
- document.removeEventListener("visibilitychange", onVisibility);
1741
- window.removeEventListener("pagehide", onHide);
1742
- window.clearTimeout(timer);
1743
- resolve(opened);
1744
- };
1745
- const onVisibility = () => {
1746
- if (document.visibilityState === "hidden") finish(true);
1747
- };
1748
- const onHide = () => {
1749
- finish(true);
1750
- };
1751
- document.addEventListener("visibilitychange", onVisibility);
1752
- window.addEventListener("pagehide", onHide);
1753
- const timer = window.setTimeout(() => {
1754
- finish(false);
1755
- }, timeoutMs);
1756
- try {
1757
- window.location.href = uri;
1758
- } catch {
1759
- finish(false);
1760
- }
1761
- });
3072
+ return out;
1762
3073
  }
1763
3074
 
1764
- // src/QRLConnectProvider.ts
1765
- var QRL_CONNECT_PROVIDER_INFO = {
1766
- name: "MyQRLWallet",
1767
- rdns: "com.qrlwallet.connect",
1768
- icon: "data:image/png;base64,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"
1769
- };
1770
- var EIP6963_ANNOUNCE_EVENT = "eip6963:announceProvider";
1771
- var EIP6963_REQUEST_EVENT = "eip6963:requestProvider";
1772
- var QRLConnectProvider = class extends import_eventemitter34.default {
1773
- constructor(options) {
1774
- super();
1775
- this.pendingRequests = /* @__PURE__ */ new Map();
1776
- this.eip6963Detail = null;
1777
- this.eip6963RequestListener = null;
1778
- this.resumeListener = null;
1779
- this.resumeDebounce = null;
1780
- // Random per-instance prefix keeps request ids unique across page loads.
1781
- // A bare counter restarts at 1 on reload, and the relay buffers messages
1782
- // for 5 minutes, so a stale buffered response could otherwise be matched
1783
- // to a fresh request that drew the same small id.
1784
- this.requestIdPrefix = randomUuid().slice(0, 8);
1785
- this.requestCounter = 0;
1786
- this.isQRLConnect = true;
1787
- this.options = options;
1788
- if (options.debug) {
1789
- setDebug(true);
1790
- }
1791
- this.connectionManager = new ConnectionManager({
1792
- dappMetadata: options.dappMetadata,
1793
- relayUrl: options.relayUrl,
1794
- chainId: options.chainId,
1795
- storageKey: options.storageKey
1796
- });
1797
- this.setupConnectionListeners();
1798
- if (options.autoReconnect !== false) {
1799
- void this.connectionManager.reconnect();
1800
- }
1801
- this.setupResumeListeners();
1802
- if (options.announceProvider !== false) {
1803
- this.startEip6963Announce();
3075
+ // src/signing/typedData.ts
3076
+ var SLOT = 32;
3077
+ var TYPED_DATA_LIMITS = Object.freeze({
3078
+ maxTypes: 32,
3079
+ maxFieldsPerType: 32,
3080
+ maxTotalFields: 256,
3081
+ maxTypeGraphDepth: 12,
3082
+ maxArrayNesting: 12,
3083
+ maxArrayLength: 256,
3084
+ maxEncodedValues: 2048,
3085
+ maxDynamicBytes: 16 * 1024,
3086
+ maxIdentifierLength: 64,
3087
+ maxFieldTypeLength: 128
3088
+ });
3089
+ function isMessageObject(value) {
3090
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3091
+ }
3092
+ function hasExactOwnKeys2(value, expected) {
3093
+ const keys = Object.keys(value);
3094
+ return keys.length === expected.length && expected.every((key) => keys.includes(key));
3095
+ }
3096
+ function isTypedField(value) {
3097
+ return isMessageObject(value) && hasExactOwnKeys2(value, ["name", "type"]) && typeof value.name === "string" && typeof value.type === "string";
3098
+ }
3099
+ function isTypeMap(value) {
3100
+ if (!isMessageObject(value)) return false;
3101
+ let typeCount = 0;
3102
+ for (const name in value) {
3103
+ if (!Object.prototype.hasOwnProperty.call(value, name)) continue;
3104
+ typeCount++;
3105
+ if (typeCount > TYPED_DATA_LIMITS.maxTypes) return false;
3106
+ const def = value[name];
3107
+ if (!Array.isArray(def) || def.length > TYPED_DATA_LIMITS.maxFieldsPerType || !def.every(isTypedField)) {
3108
+ return false;
1804
3109
  }
1805
3110
  }
1806
- startEip6963Announce() {
1807
- if (typeof window === "undefined" || typeof CustomEvent === "undefined") {
1808
- return;
1809
- }
1810
- const overrides = this.options.providerInfo ?? {};
1811
- this.eip6963Detail = Object.freeze({
1812
- info: Object.freeze({
1813
- uuid: randomUuid(),
1814
- name: overrides.name ?? QRL_CONNECT_PROVIDER_INFO.name,
1815
- icon: overrides.icon ?? QRL_CONNECT_PROVIDER_INFO.icon,
1816
- rdns: overrides.rdns ?? QRL_CONNECT_PROVIDER_INFO.rdns
1817
- }),
1818
- provider: this
1819
- });
1820
- const announce = () => {
1821
- if (!this.eip6963Detail) return;
1822
- window.dispatchEvent(new CustomEvent(EIP6963_ANNOUNCE_EVENT, { detail: this.eip6963Detail }));
1823
- };
1824
- this.eip6963RequestListener = announce;
1825
- window.addEventListener(EIP6963_REQUEST_EVENT, announce);
1826
- announce();
3111
+ return true;
3112
+ }
3113
+ function parsePayload(payload) {
3114
+ if (!isMessageObject(payload)) throw new Error("invalid typed data payload");
3115
+ if (!hasExactOwnKeys2(payload, ["types", "primaryType", "domain", "message"])) {
3116
+ throw new Error("typed data payload contains unknown top-level fields");
3117
+ }
3118
+ const { types, primaryType, domain, message } = payload;
3119
+ if (!isTypeMap(types)) throw new Error("typed data types must be a struct map");
3120
+ if (typeof primaryType !== "string") throw new Error("typed data primaryType must be a string");
3121
+ if (!isMessageObject(domain)) throw new Error("typed data domain must be an object");
3122
+ if (!isMessageObject(message)) throw new Error("typed data message must be an object");
3123
+ return { types, primaryType, domain, message };
3124
+ }
3125
+ var ATOMIC_RE = /^(?:(address|bool|string|bytes)|(u?int)(\d+)|bytes(\d+)|(.+?)\[(\d*)\])$/;
3126
+ var IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
3127
+ var UNSAFE_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3128
+ function consumeValue(budget) {
3129
+ budget.remainingValues--;
3130
+ if (budget.remainingValues < 0) throw new Error("typed data contains too many encoded values");
3131
+ }
3132
+ function assertIdentifier(value, label) {
3133
+ if (!IDENTIFIER_RE.test(value) || value.length > TYPED_DATA_LIMITS.maxIdentifierLength || UNSAFE_OBJECT_KEYS.has(value)) {
3134
+ throw new Error(`invalid ${label}: ${value}`);
1827
3135
  }
1828
- /**
1829
- * Re-open the relay socket when the dApp tab regains focus or the network
1830
- * comes back. Debounced because visibilitychange + online can fire together.
1831
- * No-op when already connected (ConnectionManager.resume guards that).
1832
- */
1833
- setupResumeListeners() {
1834
- if (typeof window === "undefined" || typeof document === "undefined") return;
1835
- if (this.resumeListener) return;
1836
- const resume = () => {
1837
- if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
1838
- if (this.resumeDebounce) clearTimeout(this.resumeDebounce);
1839
- this.resumeDebounce = setTimeout(() => {
1840
- this.resumeDebounce = null;
1841
- this.connectionManager.resume();
1842
- }, 300);
1843
- };
1844
- this.resumeListener = resume;
1845
- document.addEventListener("visibilitychange", resume);
1846
- window.addEventListener("online", resume);
1847
- window.addEventListener("pageshow", resume);
3136
+ }
3137
+ function parseFieldType(type, types, depth = 0) {
3138
+ if (depth > TYPED_DATA_LIMITS.maxArrayNesting) {
3139
+ throw new Error(`type nesting too deep: ${type}`);
1848
3140
  }
1849
- /**
1850
- * Remove the foreground/online resume listeners. Called on disconnect so a
1851
- * torn-down session can't be silently revived by a later tab focus; the
1852
- * listeners are re-armed by getConnectionURI()/newConnection() if the same
1853
- * provider is re-paired. Safe in any env.
1854
- */
1855
- teardownResumeListeners() {
1856
- if (typeof window !== "undefined" && typeof document !== "undefined" && this.resumeListener) {
1857
- document.removeEventListener("visibilitychange", this.resumeListener);
1858
- window.removeEventListener("online", this.resumeListener);
1859
- window.removeEventListener("pageshow", this.resumeListener);
3141
+ if (type.length > TYPED_DATA_LIMITS.maxFieldTypeLength) {
3142
+ throw new Error(`field type too long: ${type}`);
3143
+ }
3144
+ const m = ATOMIC_RE.exec(type);
3145
+ if (m) {
3146
+ const [, atomic, intKind, intWidthStr, bytesWidthStr, innerType, sizeStr] = m;
3147
+ if (atomic === "address" || atomic === "bool" || atomic === "string" || atomic === "bytes") {
3148
+ return { kind: atomic };
1860
3149
  }
1861
- this.resumeListener = null;
1862
- if (this.resumeDebounce) {
1863
- clearTimeout(this.resumeDebounce);
1864
- this.resumeDebounce = null;
3150
+ if (intKind) {
3151
+ const width = Number(intWidthStr);
3152
+ if (!Number.isInteger(width) || width < 8 || width > 256 || width % 8 !== 0) {
3153
+ throw new Error(`invalid int width: ${type}`);
3154
+ }
3155
+ return { kind: intKind === "uint" ? "uintN" : "intN", width };
1865
3156
  }
1866
- }
1867
- /**
1868
- * Stop announcing this provider over EIP-6963. Safe to call from any env.
1869
- */
1870
- stopEip6963Announce() {
1871
- if (typeof window !== "undefined" && this.eip6963RequestListener) {
1872
- window.removeEventListener(EIP6963_REQUEST_EVENT, this.eip6963RequestListener);
1873
- }
1874
- this.eip6963RequestListener = null;
1875
- this.eip6963Detail = null;
1876
- }
1877
- setupConnectionListeners() {
1878
- this.connectionManager.on("status_changed", (status) => {
1879
- log("Provider", `Connection status: ${status}`);
1880
- this.emit("statusChanged", status);
1881
- if (status === "connected" /* CONNECTED */) {
1882
- this.emit("connect", { chainId: this.connectionManager.getChainId() });
1883
- }
1884
- if (status === "disconnected" /* DISCONNECTED */) {
1885
- this.emit("disconnect", {
1886
- code: 4900,
1887
- message: "Disconnected from QRL Wallet"
1888
- });
1889
- }
1890
- });
1891
- this.connectionManager.on("accounts_changed", (accounts) => {
1892
- this.emit("accountsChanged", accounts);
1893
- });
1894
- this.connectionManager.on("chain_changed", (chainId) => {
1895
- this.emit("chainChanged", chainId);
1896
- });
1897
- this.connectionManager.on("jsonrpc_response", (response) => {
1898
- const pending = this.pendingRequests.get(response.id);
1899
- if (!pending) {
1900
- warn("Provider", `No pending request for id ${response.id}`);
1901
- return;
1902
- }
1903
- this.pendingRequests.delete(response.id);
1904
- if (response.error) {
1905
- pending.reject(new Error(response.error.message || "Request failed"));
1906
- } else {
1907
- pending.resolve(response.result);
1908
- }
1909
- });
1910
- this.connectionManager.on("wallet_info", (info) => {
1911
- for (const [id, pending] of this.pendingRequests) {
1912
- if (pending.method === "qrl_requestAccounts" || pending.method === "qrl_accounts") {
1913
- pending.resolve(info.accounts);
1914
- this.pendingRequests.delete(id);
1915
- }
1916
- }
1917
- });
1918
- this.connectionManager.on("error", (err) => {
1919
- warn("Provider", `ConnectionManager error: ${err.message}`);
1920
- this.emit("message", { type: "error", data: err.message });
1921
- });
1922
- this.connectionManager.on("connection_lost", () => {
1923
- for (const [, pending] of this.pendingRequests) {
1924
- pending.reject(new Error("Connection to QRL Wallet lost"));
3157
+ if (bytesWidthStr) {
3158
+ const width = Number(bytesWidthStr);
3159
+ if (!Number.isInteger(width) || width < 1 || width > 32) {
3160
+ throw new Error(`invalid bytesN width: ${type}`);
1925
3161
  }
1926
- this.pendingRequests.clear();
1927
- });
1928
- }
1929
- /**
1930
- * Generate a connection URI for QR code display or deep link redirect.
1931
- */
1932
- async getConnectionURI() {
1933
- this.setupResumeListeners();
1934
- return this.connectionManager.getConnectionURI();
1935
- }
1936
- /**
1937
- * Check if the current browser is mobile.
1938
- */
1939
- isMobile() {
1940
- return isMobileBrowser();
1941
- }
1942
- /**
1943
- * Get the app store URL for the QRL Wallet app.
1944
- */
1945
- getAppStoreUrl() {
1946
- return getAppStoreUrl();
1947
- }
1948
- /**
1949
- * EIP-1193 request method.
1950
- */
1951
- async request(args) {
1952
- const { method, params } = args;
1953
- if (method === "qrl_chainId") {
1954
- return this.connectionManager.getChainId();
3162
+ return { kind: "bytesN", width };
1955
3163
  }
1956
- if (method === "qrl_accounts") {
1957
- const accounts = this.connectionManager.getAccounts();
1958
- if (accounts.length > 0) return accounts;
1959
- }
1960
- if (!RESTRICTED_METHODS.has(method) && !UNRESTRICTED_METHODS.has(method)) {
1961
- throw new Error(`Unsupported method: ${method}`);
1962
- }
1963
- if (this.connectionManager.getStatus() !== "connected" /* CONNECTED */) {
1964
- throw new Error("Not connected to QRL Wallet");
1965
- }
1966
- const id = `${this.requestIdPrefix}-${++this.requestCounter}`;
1967
- return new Promise((resolve, reject) => {
1968
- const pending = {
1969
- id,
1970
- method,
1971
- params,
1972
- resolve,
1973
- reject,
1974
- timestamp: Date.now()
1975
- };
1976
- this.pendingRequests.set(id, pending);
1977
- const timeout = setTimeout(() => {
1978
- if (this.pendingRequests.has(id)) {
1979
- this.pendingRequests.delete(id);
1980
- reject(new Error(`Request timeout: ${method} (${REQUEST_TIMEOUT_MS}ms)`));
3164
+ if (innerType !== void 0) {
3165
+ parseFieldType(innerType, types, depth + 1);
3166
+ if (sizeStr) {
3167
+ const size = Number(sizeStr);
3168
+ if (!Number.isSafeInteger(size) || size <= 0 || size > TYPED_DATA_LIMITS.maxArrayLength) {
3169
+ throw new Error(`invalid array size: ${type}`);
1981
3170
  }
1982
- }, REQUEST_TIMEOUT_MS);
1983
- const originalResolve = pending.resolve;
1984
- const originalReject = pending.reject;
1985
- pending.resolve = (result) => {
1986
- clearTimeout(timeout);
1987
- originalResolve(result);
1988
- };
1989
- pending.reject = (error2) => {
1990
- clearTimeout(timeout);
1991
- originalReject(error2);
1992
- };
1993
- this.connectionManager.sendJsonRpc({
1994
- jsonrpc: "2.0",
1995
- id,
1996
- method,
1997
- params
1998
- });
1999
- });
2000
- }
2001
- /**
2002
- * Get the current connection status.
2003
- */
2004
- getStatus() {
2005
- return this.connectionManager.getStatus();
2006
- }
2007
- /**
2008
- * Get connected accounts.
2009
- */
2010
- getAccounts() {
2011
- return this.connectionManager.getAccounts();
2012
- }
2013
- /**
2014
- * Get the channel ID for this connection.
2015
- */
2016
- getChannelId() {
2017
- return this.connectionManager.getChannelId();
2018
- }
2019
- /**
2020
- * Check if connected and keys exchanged.
2021
- */
2022
- isConnected() {
2023
- return this.connectionManager.getStatus() === "connected" /* CONNECTED */;
2024
- }
2025
- /**
2026
- * Check if a stored session exists that can be reconnected.
2027
- */
2028
- hasStoredSession() {
2029
- return this.connectionManager.hasStoredSession();
2030
- }
2031
- /**
2032
- * Reset the connection and start a fresh pairing with a new channel.
2033
- * Use this when the user wants to create a new connection instead of
2034
- * reconnecting to an existing session.
2035
- */
2036
- async newConnection() {
2037
- for (const [, pending] of this.pendingRequests) {
2038
- pending.reject(new Error("Connection reset"));
2039
- }
2040
- this.pendingRequests.clear();
2041
- await this.connectionManager.resetForNewChannel();
2042
- return this.getConnectionURI();
2043
- }
2044
- /**
2045
- * Disconnect from wallet and clean up. Returns once the TERMINATE has
2046
- * either been flushed to the relay or the 800ms best-effort window has
2047
- * elapsed - the wallet gets an instant disconnect instead of landing in
2048
- * its stale-session grace period.
2049
- */
2050
- async disconnect() {
2051
- for (const [, pending] of this.pendingRequests) {
2052
- pending.reject(new Error("Disconnected"));
3171
+ return { kind: "array", inner: innerType, size };
3172
+ }
3173
+ return { kind: "array", inner: innerType };
2053
3174
  }
2054
- this.pendingRequests.clear();
2055
- this.teardownResumeListeners();
2056
- await this.connectionManager.disconnect();
2057
- }
2058
- };
2059
-
2060
- // src/signing/ctx.ts
2061
- var SCHEME_VERSION_MSG = "QRL-SIGN-MSG-v1";
2062
- var SCHEME_VERSION_TYPED = "QRL-SIGN-TYPED-v1";
2063
- var SCHEME_TAG_MSG = new TextEncoder().encode(SCHEME_VERSION_MSG);
2064
- var SCHEME_TAG_TYPED = new TextEncoder().encode(SCHEME_VERSION_TYPED);
2065
- var DIGEST_LEN = 64;
2066
-
2067
- // src/signing/bytes.ts
2068
- function hexToBytes(hex) {
2069
- if (typeof hex !== "string") throw new Error("hex must be a string");
2070
- if (!/^0x([0-9a-fA-F]{2})*$/.test(hex)) {
2071
- throw new Error("hex must be 0x-prefixed even-length");
2072
- }
2073
- const n = (hex.length - 2) / 2;
2074
- const out = new Uint8Array(n);
2075
- for (let i = 0; i < n; i++) {
2076
- out[i] = parseInt(hex.slice(2 + i * 2, 4 + i * 2), 16);
2077
- }
2078
- return out;
2079
- }
2080
- function bytesToHex(bytes) {
2081
- if (!(bytes instanceof Uint8Array)) {
2082
- throw new Error("bytes must be a Uint8Array");
2083
- }
2084
- let s = "0x";
2085
- for (const v of bytes) s += v.toString(16).padStart(2, "0");
2086
- return s;
2087
- }
2088
- function concatBytes(...parts) {
2089
- return concatBytesArr(parts);
2090
- }
2091
- function concatBytesArr(parts) {
2092
- let total = 0;
2093
- for (const p of parts) total += p.length;
2094
- const out = new Uint8Array(total);
2095
- let off = 0;
2096
- for (const p of parts) {
2097
- out.set(p, off);
2098
- off += p.length;
2099
- }
2100
- return out;
2101
- }
2102
-
2103
- // src/signing/messageDigest.ts
2104
- function computeMessageDigest(messageBytes) {
2105
- if (!(messageBytes instanceof Uint8Array)) {
2106
- throw new Error("messageBytes must be a Uint8Array");
2107
3175
  }
2108
- return shake256Digest(concatBytes(SCHEME_TAG_MSG, messageBytes), DIGEST_LEN);
2109
- }
2110
-
2111
- // src/signing/typedData.ts
2112
- var SLOT = 32;
2113
- function isMessageObject(v) {
2114
- return typeof v === "object" && v !== null && !Array.isArray(v);
2115
- }
2116
- var ATOMIC_RE = /^(?:(address|bool|string|bytes)|(u?int)(\d+)|bytes(\d+)|(.+?)\[(\d*)\])$/;
2117
- var MAX_TYPE_DEPTH = 12;
2118
- function parseFieldType(type, types, depth = 0) {
2119
- if (depth > MAX_TYPE_DEPTH) throw new Error(`type nesting too deep: ${type}`);
2120
3176
  if (Object.prototype.hasOwnProperty.call(types, type)) {
2121
3177
  return { kind: "ref", name: type };
2122
3178
  }
2123
- const m = ATOMIC_RE.exec(type);
2124
- if (!m) throw new Error(`unknown type: ${type}`);
2125
- const [, atomic, intKind, intWidthStr, bytesWidthStr, innerType, sizeStr] = m;
2126
- if (atomic === "address" || atomic === "bool" || atomic === "string" || atomic === "bytes") {
2127
- return { kind: atomic };
2128
- }
2129
- if (intKind) {
2130
- const width = Number(intWidthStr);
2131
- if (!Number.isInteger(width) || width < 8 || width > 256 || width % 8 !== 0) {
2132
- throw new Error(`invalid int width: ${type}`);
2133
- }
2134
- return { kind: intKind === "uint" ? "uintN" : "intN", width };
2135
- }
2136
- if (bytesWidthStr) {
2137
- const width = Number(bytesWidthStr);
2138
- if (!Number.isInteger(width) || width < 1 || width > 32) {
2139
- throw new Error(`invalid bytesN width: ${type}`);
2140
- }
2141
- return { kind: "bytesN", width };
2142
- }
2143
- if (innerType !== void 0) {
2144
- parseFieldType(innerType, types, depth + 1);
2145
- if (sizeStr) {
2146
- const size = Number(sizeStr);
2147
- if (!Number.isInteger(size) || size <= 0) {
2148
- throw new Error(`invalid array size: ${type}`);
2149
- }
2150
- return { kind: "array", inner: innerType, size };
2151
- }
2152
- return { kind: "array", inner: innerType };
2153
- }
2154
- throw new Error(`unhandled type: ${type}`);
3179
+ throw new Error(`unknown type: ${type}`);
3180
+ }
3181
+ function isReservedAtomicTypeName(name) {
3182
+ if (/^(?:address|bool|string|bytes)$/.test(name)) return true;
3183
+ const intMatch = /^(?:u?int)(\d+)$/.exec(name);
3184
+ if (intMatch) {
3185
+ const width = Number(intMatch[1]);
3186
+ return Number.isInteger(width) && width >= 8 && width <= 256 && width % 8 === 0;
3187
+ }
3188
+ const bytesMatch = /^bytes(\d+)$/.exec(name);
3189
+ if (bytesMatch) {
3190
+ const width = Number(bytesMatch[1]);
3191
+ return Number.isInteger(width) && width >= 1 && width <= 32;
3192
+ }
3193
+ return false;
2155
3194
  }
2156
3195
  function baseTypeName(type) {
2157
3196
  return type.replace(/(\[\d*\])+$/, "");
@@ -2162,6 +3201,9 @@ function collectDependencies(primary, types) {
2162
3201
  }
2163
3202
  const visited = /* @__PURE__ */ new Set();
2164
3203
  const visit = (name, path) => {
3204
+ if (path.length > TYPED_DATA_LIMITS.maxTypeGraphDepth) {
3205
+ throw new Error(`type graph nesting too deep: ${[...path, name].join(" -> ")}`);
3206
+ }
2165
3207
  if (path.includes(name)) {
2166
3208
  throw new Error(`cyclic type reference: ${[...path, name].join(" -> ")}`);
2167
3209
  }
@@ -2181,16 +3223,30 @@ function collectDependencies(primary, types) {
2181
3223
  return visited;
2182
3224
  }
2183
3225
  function validateTypeMap(types) {
2184
- for (const [name, def] of Object.entries(types)) {
3226
+ const entries = Object.entries(types);
3227
+ if (entries.length === 0 || entries.length > TYPED_DATA_LIMITS.maxTypes) {
3228
+ throw new Error(`typed data must contain 1-${TYPED_DATA_LIMITS.maxTypes} struct types`);
3229
+ }
3230
+ let totalFields = 0;
3231
+ for (const [name, def] of entries) {
3232
+ assertIdentifier(name, "struct name");
3233
+ if (name !== "QRLDomain" && isReservedAtomicTypeName(name)) {
3234
+ throw new Error(`struct name is reserved by an atomic type: ${name}`);
3235
+ }
2185
3236
  const defUnknown = def;
2186
- if (!Array.isArray(defUnknown) || def.length === 0) {
3237
+ if (!Array.isArray(defUnknown) || def.length === 0 || def.length > TYPED_DATA_LIMITS.maxFieldsPerType) {
2187
3238
  throw new Error(`empty or invalid struct: ${name}`);
2188
3239
  }
3240
+ totalFields += def.length;
3241
+ if (totalFields > TYPED_DATA_LIMITS.maxTotalFields) {
3242
+ throw new Error("typed data contains too many fields");
3243
+ }
2189
3244
  const seen = /* @__PURE__ */ new Set();
2190
3245
  for (const f of def) {
2191
3246
  if (!f || typeof f.name !== "string" || !f.name) {
2192
3247
  throw new Error(`bad field in ${name}`);
2193
3248
  }
3249
+ assertIdentifier(f.name, `field name in ${name}`);
2194
3250
  if (typeof f.type !== "string" || !f.type) {
2195
3251
  throw new Error(`bad field type in ${name}.${f.name}`);
2196
3252
  }
@@ -2202,6 +3258,7 @@ function validateTypeMap(types) {
2202
3258
  }
2203
3259
  }
2204
3260
  function encodeType(primary, types) {
3261
+ validateTypeMap(types);
2205
3262
  const deps = collectDependencies(primary, types);
2206
3263
  const others = [...deps].filter((n) => n !== primary).sort();
2207
3264
  return [primary, ...others].map((name) => {
@@ -2256,6 +3313,7 @@ function bigIntToSlot(value, width, signed) {
2256
3313
  function parseIntValue(v, typeLabel) {
2257
3314
  if (typeof v === "bigint") return v;
2258
3315
  if (typeof v === "string") {
3316
+ if (v.length > 80) throw new Error(`${typeLabel} value is too long`);
2259
3317
  if (/^-?0x[0-9a-fA-F]+$/i.test(v)) {
2260
3318
  const isNegative = v.startsWith("-");
2261
3319
  const abs = BigInt(isNegative ? v.slice(1) : v);
@@ -2274,14 +3332,18 @@ function parseIntValue(v, typeLabel) {
2274
3332
  }
2275
3333
  throw new Error(`unsupported ${typeLabel} value: ${typeof v}`);
2276
3334
  }
2277
- function encodeField(type, value, types) {
3335
+ function encodeFieldWithBudget(type, value, types, budget, depth) {
3336
+ if (depth > TYPED_DATA_LIMITS.maxTypeGraphDepth + TYPED_DATA_LIMITS.maxArrayNesting) {
3337
+ throw new Error(`value nesting too deep: ${type}`);
3338
+ }
3339
+ consumeValue(budget);
2278
3340
  const parsed = parseFieldType(type, types);
2279
3341
  switch (parsed.kind) {
2280
3342
  case "ref":
2281
3343
  if (!isMessageObject(value)) {
2282
3344
  throw new Error(`struct field expects object: ${type}`);
2283
3345
  }
2284
- return hashStruct(parsed.name, value, types);
3346
+ return hashStructWithBudget(parsed.name, value, types, budget, depth + 1);
2285
3347
  case "address":
2286
3348
  if (typeof value !== "string") {
2287
3349
  throw new Error(`address field expects string: ${typeof value}`);
@@ -2292,15 +3354,26 @@ function encodeField(type, value, types) {
2292
3354
  throw new Error(`bool field expects boolean: ${typeof value}`);
2293
3355
  }
2294
3356
  return padLeft32(new Uint8Array([value ? 1 : 0]));
2295
- case "string":
3357
+ case "string": {
2296
3358
  if (typeof value !== "string") {
2297
3359
  throw new Error(`string field expects string: ${typeof value}`);
2298
3360
  }
2299
- return shake256Digest(new TextEncoder().encode(value), DIGEST_LEN);
3361
+ if (value.length > TYPED_DATA_LIMITS.maxDynamicBytes) {
3362
+ throw new Error("string field exceeds typed data byte limit");
3363
+ }
3364
+ const encoded = new TextEncoder().encode(value);
3365
+ if (encoded.length > TYPED_DATA_LIMITS.maxDynamicBytes) {
3366
+ throw new Error("string field exceeds typed data byte limit");
3367
+ }
3368
+ return shake256Digest(encoded, DIGEST_LEN);
3369
+ }
2300
3370
  case "bytes":
2301
3371
  if (typeof value !== "string") {
2302
3372
  throw new Error(`bytes field expects 0x-hex string: ${typeof value}`);
2303
3373
  }
3374
+ if (value.length > TYPED_DATA_LIMITS.maxDynamicBytes * 2 + 2) {
3375
+ throw new Error("bytes field exceeds typed data byte limit");
3376
+ }
2304
3377
  return shake256Digest(hexToBytes(value), DIGEST_LEN);
2305
3378
  case "uintN":
2306
3379
  return bigIntToSlot(parseIntValue(value, `uint${parsed.width}`), parsed.width, false);
@@ -2310,6 +3383,9 @@ function encodeField(type, value, types) {
2310
3383
  if (typeof value !== "string") {
2311
3384
  throw new Error(`bytes${parsed.width} expects 0x-hex string: ${typeof value}`);
2312
3385
  }
3386
+ if (value.length !== parsed.width * 2 + 2) {
3387
+ throw new Error(`bytes${parsed.width} requires ${parsed.width} bytes`);
3388
+ }
2313
3389
  const raw = hexToBytes(value);
2314
3390
  if (raw.length !== parsed.width) {
2315
3391
  throw new Error(`bytes${parsed.width} requires ${parsed.width} bytes, got ${raw.length}`);
@@ -2320,28 +3396,62 @@ function encodeField(type, value, types) {
2320
3396
  if (!Array.isArray(value)) {
2321
3397
  throw new Error(`array field expects array: ${typeof value}`);
2322
3398
  }
3399
+ if (value.length > TYPED_DATA_LIMITS.maxArrayLength) {
3400
+ throw new Error(`array ${type} exceeds length limit`);
3401
+ }
2323
3402
  if (parsed.size !== void 0 && value.length !== parsed.size) {
2324
3403
  throw new Error(`fixed array ${type} requires length ${parsed.size}, got ${value.length}`);
2325
3404
  }
2326
- const chunks = value.map((v) => encodeField(parsed.inner, v, types));
3405
+ const chunks = value.map(
3406
+ (v) => encodeFieldWithBudget(parsed.inner, v, types, budget, depth + 1)
3407
+ );
2327
3408
  return shake256Digest(concatBytesArr(chunks), DIGEST_LEN);
2328
3409
  }
2329
3410
  }
2330
3411
  }
2331
- function hashStruct(primary, data, types) {
3412
+ function encodeField(type, value, types) {
3413
+ validateTypeMap(types);
3414
+ return encodeFieldWithBudget(
3415
+ type,
3416
+ value,
3417
+ types,
3418
+ { remainingValues: TYPED_DATA_LIMITS.maxEncodedValues },
3419
+ 0
3420
+ );
3421
+ }
3422
+ function hashStructWithBudget(primary, data, types, budget, depth) {
3423
+ consumeValue(budget);
2332
3424
  const fields = types[primary];
2333
3425
  if (!fields) throw new Error(`unknown struct: ${primary}`);
2334
3426
  const expected = new Set(fields.map((f) => f.name));
2335
- for (const k of Object.keys(data)) {
3427
+ let ownFieldCount = 0;
3428
+ for (const k in data) {
3429
+ if (!Object.prototype.hasOwnProperty.call(data, k)) continue;
3430
+ ownFieldCount++;
3431
+ if (ownFieldCount > TYPED_DATA_LIMITS.maxEncodedValues) {
3432
+ throw new Error("typed data object contains too many fields");
3433
+ }
2336
3434
  if (!expected.has(k)) throw new Error(`unknown field in ${primary}: ${k}`);
2337
3435
  }
2338
3436
  const parts = [typeHash(primary, types)];
2339
3437
  for (const f of fields) {
2340
- if (!(f.name in data)) throw new Error(`missing field ${primary}.${f.name}`);
2341
- parts.push(encodeField(f.type, data[f.name], types));
3438
+ if (!Object.prototype.hasOwnProperty.call(data, f.name)) {
3439
+ throw new Error(`missing field ${primary}.${f.name}`);
3440
+ }
3441
+ parts.push(encodeFieldWithBudget(f.type, data[f.name], types, budget, depth + 1));
2342
3442
  }
2343
3443
  return shake256Digest(concatBytesArr(parts), DIGEST_LEN);
2344
3444
  }
3445
+ function hashStruct(primary, data, types) {
3446
+ validateTypeMap(types);
3447
+ return hashStructWithBudget(
3448
+ primary,
3449
+ data,
3450
+ types,
3451
+ { remainingValues: TYPED_DATA_LIMITS.maxEncodedValues },
3452
+ 0
3453
+ );
3454
+ }
2345
3455
  var RESERVED_DOMAIN_FIELDS = {
2346
3456
  name: "string",
2347
3457
  version: "string",
@@ -2377,60 +3487,948 @@ function validatePayloadReachability(primary, types) {
2377
3487
  }
2378
3488
  }
2379
3489
  function computeTypedDataDigest(payload) {
2380
- if (!payload || typeof payload !== "object") {
2381
- throw new Error("invalid typed data payload");
2382
- }
2383
- validateTypeMap(payload.types);
2384
- validateDomainTypes(payload.types);
2385
- validatePayloadReachability(payload.primaryType, payload.types);
2386
- const domainHash = hashStruct("QRLDomain", payload.domain, payload.types);
2387
- const messageHash = hashStruct(payload.primaryType, payload.message, payload.types);
3490
+ const parsed = parsePayload(payload);
3491
+ validateTypeMap(parsed.types);
3492
+ assertIdentifier(parsed.primaryType, "primary type");
3493
+ if (parsed.primaryType === "QRLDomain") {
3494
+ throw new Error("QRLDomain cannot be the primary type");
3495
+ }
3496
+ validateDomainTypes(parsed.types);
3497
+ validatePayloadReachability(parsed.primaryType, parsed.types);
3498
+ const budget = { remainingValues: TYPED_DATA_LIMITS.maxEncodedValues };
3499
+ const domainHash = hashStructWithBudget("QRLDomain", parsed.domain, parsed.types, budget, 0);
3500
+ const messageHash = hashStructWithBudget(
3501
+ parsed.primaryType,
3502
+ parsed.message,
3503
+ parsed.types,
3504
+ budget,
3505
+ 0
3506
+ );
2388
3507
  return shake256Digest(concatBytes(SCHEME_TAG_TYPED, domainHash, messageHash), DIGEST_LEN);
2389
3508
  }
2390
3509
 
2391
- // src/signing/verify.ts
2392
- function bytesOrHex(v) {
2393
- if (v instanceof Uint8Array) return v;
2394
- if (typeof v === "string") return hexToBytes(v);
2395
- throw new Error("expected Uint8Array or 0x-hex string");
3510
+ // src/utils/platform.ts
3511
+ function isMobileBrowser() {
3512
+ if (typeof navigator === "undefined") return false;
3513
+ return /Android|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(navigator.userAgent);
2396
3514
  }
2397
- function verifyMessage({
2398
- signature,
2399
- publicKey,
3515
+ function getAppStoreUrl() {
3516
+ if (typeof navigator !== "undefined" && /android/i.test(navigator.userAgent)) {
3517
+ return "https://play.google.com/store/apps/details?id=com.chiefdg.myqrlwallet";
3518
+ }
3519
+ return "https://apps.apple.com/app/myqrlwallet/id6742219498";
3520
+ }
3521
+ function attemptWalletRedirect(uri, timeoutMs = 1800) {
3522
+ if (typeof window === "undefined" || typeof document === "undefined") {
3523
+ return Promise.resolve(false);
3524
+ }
3525
+ if (!uri.trim().toLowerCase().startsWith("qrlconnect:")) {
3526
+ return Promise.resolve(false);
3527
+ }
3528
+ return new Promise((resolve) => {
3529
+ let settled = false;
3530
+ const finish = (opened) => {
3531
+ if (settled) return;
3532
+ settled = true;
3533
+ document.removeEventListener("visibilitychange", onVisibility);
3534
+ window.removeEventListener("pagehide", onHide);
3535
+ window.clearTimeout(timer);
3536
+ resolve(opened);
3537
+ };
3538
+ const onVisibility = () => {
3539
+ if (document.visibilityState === "hidden") finish(true);
3540
+ };
3541
+ const onHide = () => {
3542
+ finish(true);
3543
+ };
3544
+ document.addEventListener("visibilitychange", onVisibility);
3545
+ window.addEventListener("pagehide", onHide);
3546
+ const timer = window.setTimeout(() => {
3547
+ finish(false);
3548
+ }, timeoutMs);
3549
+ try {
3550
+ window.location.href = uri;
3551
+ } catch {
3552
+ finish(false);
3553
+ }
3554
+ });
3555
+ }
3556
+
3557
+ // src/QRLConnectProvider.ts
3558
+ var QRL_CONNECT_PROVIDER_INFO = {
3559
+ name: "MyQRLWallet",
3560
+ rdns: "com.qrlwallet.connect",
3561
+ icon: "data:image/png;base64,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"
3562
+ };
3563
+ var EIP6963_ANNOUNCE_EVENT = "eip6963:announceProvider";
3564
+ var EIP6963_REQUEST_EVENT = "eip6963:requestProvider";
3565
+ function isRecordObj(v) {
3566
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3567
+ }
3568
+ function isUnknownArray(v) {
3569
+ return Array.isArray(v);
3570
+ }
3571
+ function parseInflightRecord(v) {
3572
+ if (!isRecordObj(v)) return null;
3573
+ const { id, method, ts } = v;
3574
+ if (!isValidJsonRpcId(id) || !isValidJsonRpcMethod(method)) return null;
3575
+ if (typeof ts !== "number" || !Number.isFinite(ts)) return null;
3576
+ if (v.expectedChainId !== void 0 && typeof v.expectedChainId !== "string") return null;
3577
+ return { id, method, ts, expectedChainId: v.expectedChainId };
3578
+ }
3579
+ function canonicalChainId2(value) {
3580
+ if (typeof value !== "string" || value.length > 66 || !/^0x[0-9a-fA-F]+$/.test(value)) {
3581
+ throw new Error("wallet_switchQrlChain requires a 0x-prefixed chainId");
3582
+ }
3583
+ return `0x${BigInt(value).toString(16)}`;
3584
+ }
3585
+ function requestedSwitchChainId(params) {
3586
+ if (params?.length !== 1) {
3587
+ throw new Error("wallet_switchQrlChain requires one chain configuration object");
3588
+ }
3589
+ const request = params?.[0];
3590
+ if (!isRecordObj(request)) {
3591
+ throw new Error("wallet_switchQrlChain requires one chain configuration object");
3592
+ }
3593
+ return canonicalChainId2(request.chainId);
3594
+ }
3595
+ var TRANSACTION_FIELDS = /* @__PURE__ */ new Set(["from", "to", "value", "gas", "data"]);
3596
+ var QRL_TRANSACTION_MAX_DATA_BYTES = 128 * 1024;
3597
+ var QRL_TRANSACTION_MAX_QUANTITY_HEX_DIGITS = 64;
3598
+ var RPC_QUANTITY_RE = /^0x(?:0|[1-9a-fA-F][0-9a-fA-F]*)$/;
3599
+ function validateRpcQuantity(value, field) {
3600
+ if (field === "gas" && typeof value === "number") {
3601
+ if (Number.isSafeInteger(value) && value >= 0) return;
3602
+ throw new Error("transaction gas must be a non-negative safe integer");
3603
+ }
3604
+ if (typeof value !== "string" || value.length > QRL_TRANSACTION_MAX_QUANTITY_HEX_DIGITS + 2 || !RPC_QUANTITY_RE.test(value)) {
3605
+ throw new Error(`transaction ${field} must be a canonical 0x quantity`);
3606
+ }
3607
+ if (field === "gas" && BigInt(value) > BigInt(Number.MAX_SAFE_INTEGER)) {
3608
+ throw new Error("transaction gas exceeds the wallet safe-integer limit");
3609
+ }
3610
+ }
3611
+ function validateTransactionRequest(method, params, authorizedAccount) {
3612
+ if (params?.length !== 1 || !isRecordObj(params[0])) {
3613
+ throw new Error(`${method} requires exactly one transaction object`);
3614
+ }
3615
+ const tx = params[0];
3616
+ const unsupported = Object.keys(tx).find((field) => !TRANSACTION_FIELDS.has(field));
3617
+ if (unsupported) throw new Error(`transaction field is not supported: ${unsupported}`);
3618
+ if (!isCurrentQrlAddress(tx.from)) {
3619
+ throw new Error("transaction from must be a valid Q-address");
3620
+ }
3621
+ if (tx.from !== authorizedAccount) {
3622
+ throw new Error("transaction from is not the authorized account");
3623
+ }
3624
+ if (!isCurrentQrlAddress(tx.to)) {
3625
+ throw new Error("transaction to must be a valid Q-address");
3626
+ }
3627
+ if ("value" in tx) validateRpcQuantity(tx.value, "value");
3628
+ if ("gas" in tx) validateRpcQuantity(tx.gas, "gas");
3629
+ if ("data" in tx && (typeof tx.data !== "string" || tx.data.length > QRL_TRANSACTION_MAX_DATA_BYTES * 2 + 2 || !/^0x(?:[0-9a-fA-F]{2})*$/.test(tx.data))) {
3630
+ throw new Error("transaction data must be bounded 0x-prefixed bytes");
3631
+ }
3632
+ }
3633
+ function snapshotRequestParams(params) {
3634
+ if (params === void 0) return void 0;
3635
+ try {
3636
+ const encoded = JSON.stringify(params);
3637
+ if (typeof encoded !== "string") throw new Error("params did not serialize");
3638
+ const snapshot = JSON.parse(encoded);
3639
+ if (!isUnknownArray(snapshot)) throw new Error("params must serialize to an array");
3640
+ return snapshot;
3641
+ } catch (error2) {
3642
+ const detail = error2 instanceof Error ? error2.message : String(error2);
3643
+ throw new Error(`Request params must be JSON-serializable: ${detail}`);
3644
+ }
3645
+ }
3646
+ function validateRestrictedRequest(method, params, authorizedAccounts) {
3647
+ if (method === "qrl_requestAccounts") {
3648
+ if (params !== void 0 && params.length !== 0) {
3649
+ throw new Error("qrl_requestAccounts does not accept parameters");
3650
+ }
3651
+ return void 0;
3652
+ }
3653
+ if (method === "wallet_switchQrlChain") {
3654
+ return requestedSwitchChainId(params);
3655
+ }
3656
+ const authorizedAccount = authorizedAccounts[0];
3657
+ if (!authorizedAccount || authorizedAccounts.length !== 1) {
3658
+ throw new Error("No authorized account: call qrl_requestAccounts first");
3659
+ }
3660
+ if (method === "qrl_sendTransaction" || method === "qrl_signTransaction") {
3661
+ validateTransactionRequest(method, params, authorizedAccount);
3662
+ return void 0;
3663
+ }
3664
+ if (method === "qrl_signMessage") {
3665
+ if (params?.length !== 2) {
3666
+ throw new Error("qrl_signMessage requires [signer, messageHex]");
3667
+ }
3668
+ const [signer, messageHex] = params;
3669
+ if (!isCurrentQrlAddress(signer)) {
3670
+ throw new Error("qrl_signMessage requires a valid Q-address signer");
3671
+ }
3672
+ if (signer !== authorizedAccount) {
3673
+ throw new Error("qrl_signMessage signer is not the authorized account");
3674
+ }
3675
+ if (typeof messageHex !== "string" || messageHex.length > TYPED_DATA_LIMITS.maxDynamicBytes * 2 + 2 || !/^0x([0-9a-fA-F]{2})*$/.test(messageHex)) {
3676
+ throw new Error("qrl_signMessage requires bounded 0x-prefixed bytes");
3677
+ }
3678
+ return void 0;
3679
+ }
3680
+ if (method !== "qrl_signTypedData") return void 0;
3681
+ if (params?.length !== 2) {
3682
+ throw new Error("qrl_signTypedData requires [signer, payload]");
3683
+ }
3684
+ if (!isCurrentQrlAddress(params[0])) {
3685
+ throw new Error("qrl_signTypedData requires a valid Q-address signer");
3686
+ }
3687
+ if (params[0] !== authorizedAccount) {
3688
+ throw new Error("qrl_signTypedData signer is not the authorized account");
3689
+ }
3690
+ computeTypedDataDigest(params[1]);
3691
+ return void 0;
3692
+ }
3693
+ function requiresAuthorizedAccount(method) {
3694
+ return method === "qrl_sendTransaction" || method === "qrl_signTransaction" || method === "qrl_signMessage" || method === "qrl_signTypedData";
3695
+ }
3696
+ var QRLConnectProvider = class extends import_eventemitter34.default {
3697
+ constructor(options) {
3698
+ super();
3699
+ this.pendingRequests = /* @__PURE__ */ new Map();
3700
+ this.eip6963Detail = null;
3701
+ this.eip6963RequestListener = null;
3702
+ this.resumeListener = null;
3703
+ this.resumeDebounce = null;
3704
+ this.orphanedRequests = /* @__PURE__ */ new Map();
3705
+ // Random per-instance prefix keeps request ids unique across page loads.
3706
+ // A bare counter restarts at 1 on reload, and the relay buffers messages
3707
+ // for 5 minutes, so a stale buffered response could otherwise be matched
3708
+ // to a fresh request that drew the same small id.
3709
+ this.requestIdPrefix = randomUuid().slice(0, 8);
3710
+ this.requestCounter = 0;
3711
+ // Cancels work that is still awaiting channel restoration and therefore has
3712
+ // no pendingRequests entry yet. Without this barrier, disconnect() or
3713
+ // newConnection() could miss that request and let it cross into a later
3714
+ // channel once the stale ensureChannelJoined() promise settled.
3715
+ this.requestGeneration = 0;
3716
+ this.requestCancellationMessage = "Connection changed while request was pending";
3717
+ this.lifecycleTransition = null;
3718
+ this.disconnectInFlight = null;
3719
+ this.restrictedRequestActive = false;
3720
+ this.restrictedRequestQueue = [];
3721
+ this.isQRLConnect = true;
3722
+ this.options = options;
3723
+ this.inflightKey = `${options.storageKey ?? `${STORAGE_KEY_PREFIX}:session`}:inflight`;
3724
+ for (const rec of this.readInflight()) {
3725
+ this.orphanedRequests.set(rec.id, {
3726
+ method: rec.method,
3727
+ expectedChainId: rec.expectedChainId
3728
+ });
3729
+ }
3730
+ if (options.debug) {
3731
+ setDebug(true);
3732
+ }
3733
+ this.connectionManager = new ConnectionManager({
3734
+ dappMetadata: options.dappMetadata,
3735
+ relayUrl: options.relayUrl,
3736
+ chainId: options.chainId,
3737
+ storageKey: options.storageKey
3738
+ });
3739
+ this.setupConnectionListeners();
3740
+ if (options.autoReconnect !== false) {
3741
+ void this.connectionManager.reconnect();
3742
+ }
3743
+ this.setupResumeListeners();
3744
+ if (options.announceProvider !== false) {
3745
+ this.startEip6963Announce();
3746
+ }
3747
+ }
3748
+ startEip6963Announce() {
3749
+ if (typeof window === "undefined" || typeof CustomEvent === "undefined") {
3750
+ return;
3751
+ }
3752
+ const overrides = this.options.providerInfo ?? {};
3753
+ this.eip6963Detail = Object.freeze({
3754
+ info: Object.freeze({
3755
+ uuid: randomUuid(),
3756
+ name: overrides.name ?? QRL_CONNECT_PROVIDER_INFO.name,
3757
+ icon: overrides.icon ?? QRL_CONNECT_PROVIDER_INFO.icon,
3758
+ rdns: overrides.rdns ?? QRL_CONNECT_PROVIDER_INFO.rdns
3759
+ }),
3760
+ provider: this
3761
+ });
3762
+ const announce = () => {
3763
+ if (!this.eip6963Detail) return;
3764
+ window.dispatchEvent(new CustomEvent(EIP6963_ANNOUNCE_EVENT, { detail: this.eip6963Detail }));
3765
+ };
3766
+ this.eip6963RequestListener = announce;
3767
+ window.addEventListener(EIP6963_REQUEST_EVENT, announce);
3768
+ announce();
3769
+ }
3770
+ /**
3771
+ * Re-open the relay socket when the dApp tab regains focus or the network
3772
+ * comes back. Debounced because visibilitychange + online can fire together.
3773
+ * No-op when already connected (ConnectionManager.resume guards that).
3774
+ */
3775
+ setupResumeListeners() {
3776
+ if (typeof window === "undefined" || typeof document === "undefined") return;
3777
+ if (this.resumeListener) return;
3778
+ const resume = () => {
3779
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
3780
+ if (this.resumeDebounce) clearTimeout(this.resumeDebounce);
3781
+ this.resumeDebounce = setTimeout(() => {
3782
+ this.resumeDebounce = null;
3783
+ this.connectionManager.resume();
3784
+ }, 300);
3785
+ };
3786
+ this.resumeListener = resume;
3787
+ document.addEventListener("visibilitychange", resume);
3788
+ window.addEventListener("online", resume);
3789
+ window.addEventListener("pageshow", resume);
3790
+ }
3791
+ /**
3792
+ * Remove the foreground/online resume listeners. Called on disconnect so a
3793
+ * torn-down session can't be silently revived by a later tab focus; the
3794
+ * listeners are re-armed by getConnectionURI()/newConnection() if the same
3795
+ * provider is re-paired. Safe in any env.
3796
+ */
3797
+ teardownResumeListeners() {
3798
+ if (typeof window !== "undefined" && typeof document !== "undefined" && this.resumeListener) {
3799
+ document.removeEventListener("visibilitychange", this.resumeListener);
3800
+ window.removeEventListener("online", this.resumeListener);
3801
+ window.removeEventListener("pageshow", this.resumeListener);
3802
+ }
3803
+ this.resumeListener = null;
3804
+ if (this.resumeDebounce) {
3805
+ clearTimeout(this.resumeDebounce);
3806
+ this.resumeDebounce = null;
3807
+ }
3808
+ }
3809
+ /**
3810
+ * Stop announcing this provider over EIP-6963. Safe to call from any env.
3811
+ */
3812
+ stopEip6963Announce() {
3813
+ if (typeof window !== "undefined" && this.eip6963RequestListener) {
3814
+ window.removeEventListener(EIP6963_REQUEST_EVENT, this.eip6963RequestListener);
3815
+ }
3816
+ this.eip6963RequestListener = null;
3817
+ this.eip6963Detail = null;
3818
+ }
3819
+ setupConnectionListeners() {
3820
+ this.connectionManager.on("status_changed", (status) => {
3821
+ log("Provider", `Connection status: ${status}`);
3822
+ this.emit("statusChanged", status);
3823
+ if (status === "connected" /* CONNECTED */) {
3824
+ this.emit("connect", { chainId: this.connectionManager.getChainId() });
3825
+ }
3826
+ if (status === "disconnected" /* DISCONNECTED */) {
3827
+ this.emit("disconnect", {
3828
+ code: 4900,
3829
+ message: "Disconnected from QRL Wallet"
3830
+ });
3831
+ }
3832
+ });
3833
+ this.connectionManager.on("accounts_changed", (accounts) => {
3834
+ this.emit("accountsChanged", accounts);
3835
+ });
3836
+ this.connectionManager.on("chain_changed", (chainId) => {
3837
+ this.emit("chainChanged", chainId);
3838
+ });
3839
+ this.connectionManager.on("jsonrpc_response", (response) => {
3840
+ void this.handleJsonRpcResponse(response);
3841
+ });
3842
+ this.connectionManager.on("error", (err) => {
3843
+ warn("Provider", `ConnectionManager error: ${err.message}`);
3844
+ this.emit("message", { type: "error", data: err.message });
3845
+ });
3846
+ this.connectionManager.on("connection_lost", () => {
3847
+ this.cancelAllRequests(new Error("Connection to QRL Wallet lost"));
3848
+ });
3849
+ this.connectionManager.on("session_terminated", () => {
3850
+ this.cancelAllRequests(new Error("Session terminated by wallet"));
3851
+ this.clearAllInflight();
3852
+ });
3853
+ }
3854
+ async handleJsonRpcResponse(response) {
3855
+ const pending = this.pendingRequests.get(response.id);
3856
+ if (!pending) {
3857
+ const orphan = this.orphanedRequests.get(response.id);
3858
+ if (!orphan) {
3859
+ warn("Provider", `No pending request for id ${response.id}`);
3860
+ return;
3861
+ }
3862
+ this.settleInflight(response.id);
3863
+ if (!response.error && orphan.method === "qrl_requestAccounts") {
3864
+ try {
3865
+ const accounts = await this.connectionManager.authorizeAccounts(response.result);
3866
+ this.emit("late_response", { id: response.id, method: orphan.method, result: accounts });
3867
+ } catch (error2) {
3868
+ this.emit("late_response", {
3869
+ id: response.id,
3870
+ method: orphan.method,
3871
+ error: {
3872
+ code: -32e3,
3873
+ message: error2 instanceof Error ? error2.message : "Invalid account approval response"
3874
+ }
3875
+ });
3876
+ }
3877
+ return;
3878
+ }
3879
+ if (!response.error && orphan.method === "wallet_switchQrlChain" && (!orphan.expectedChainId || !this.isCurrentChain(orphan.expectedChainId))) {
3880
+ this.emit("late_response", {
3881
+ id: response.id,
3882
+ method: orphan.method,
3883
+ error: {
3884
+ code: -32e3,
3885
+ message: `Wallet reported success but did not switch to requested chain ${orphan.expectedChainId ?? "(unknown)"}`
3886
+ }
3887
+ });
3888
+ return;
3889
+ }
3890
+ this.emit("late_response", {
3891
+ id: response.id,
3892
+ method: orphan.method,
3893
+ ...response.error ? { error: response.error } : { result: response.result }
3894
+ });
3895
+ return;
3896
+ }
3897
+ this.pendingRequests.delete(response.id);
3898
+ if (response.error) {
3899
+ pending.reject(new Error(response.error.message || "Request failed"));
3900
+ return;
3901
+ }
3902
+ try {
3903
+ if (pending.method === "qrl_requestAccounts") {
3904
+ const accounts = await this.connectionManager.authorizeAccounts(response.result);
3905
+ pending.resolve(accounts);
3906
+ return;
3907
+ }
3908
+ if (pending.method === "wallet_switchQrlChain") {
3909
+ const requested = pending.expectedChainId;
3910
+ if (!requested) {
3911
+ throw new Error("Missing wallet_switchQrlChain postcondition");
3912
+ }
3913
+ if (!this.isCurrentChain(requested)) {
3914
+ throw new Error(
3915
+ `Wallet reported success but did not switch to requested chain ${requested}`
3916
+ );
3917
+ }
3918
+ }
3919
+ pending.resolve(response.result);
3920
+ } catch (error2) {
3921
+ pending.reject(error2 instanceof Error ? error2 : new Error(String(error2)));
3922
+ }
3923
+ }
3924
+ cancelAllRequests(error2) {
3925
+ this.requestGeneration++;
3926
+ this.requestCancellationMessage = error2.message;
3927
+ for (const [, pending] of this.pendingRequests) pending.reject(error2);
3928
+ this.pendingRequests.clear();
3929
+ for (const queued of this.restrictedRequestQueue) queued.reject(error2);
3930
+ this.restrictedRequestQueue = [];
3931
+ }
3932
+ assertRequestGeneration(generation) {
3933
+ if (generation !== this.requestGeneration) {
3934
+ throw new Error(this.requestCancellationMessage);
3935
+ }
3936
+ }
3937
+ isCurrentChain(expected) {
3938
+ try {
3939
+ return canonicalChainId2(this.connectionManager.getChainId()) === expected;
3940
+ } catch {
3941
+ return false;
3942
+ }
3943
+ }
3944
+ /**
3945
+ * Generate a connection URI for QR code display or deep link redirect.
3946
+ */
3947
+ async getConnectionURI() {
3948
+ if (this.lifecycleTransition) {
3949
+ throw new Error("Connection lifecycle transition is already in progress");
3950
+ }
3951
+ this.lifecycleTransition = "reset";
3952
+ this.cancelAllRequests(new Error("Connection reset"));
3953
+ this.clearAllInflight();
3954
+ try {
3955
+ this.setupResumeListeners();
3956
+ return await this.connectionManager.getConnectionURI();
3957
+ } finally {
3958
+ this.lifecycleTransition = null;
3959
+ }
3960
+ }
3961
+ /**
3962
+ * Check if the current browser is mobile.
3963
+ */
3964
+ isMobile() {
3965
+ return isMobileBrowser();
3966
+ }
3967
+ /**
3968
+ * Get the app store URL for the QRL Wallet app.
3969
+ */
3970
+ getAppStoreUrl() {
3971
+ return getAppStoreUrl();
3972
+ }
3973
+ /**
3974
+ * EIP-1193 request method.
3975
+ */
3976
+ async request(args) {
3977
+ const { method, params } = args;
3978
+ if (!isValidJsonRpcMethod(method)) {
3979
+ throw new Error("Invalid JSON-RPC method");
3980
+ }
3981
+ if (this.lifecycleTransition) {
3982
+ throw new Error(this.requestCancellationMessage);
3983
+ }
3984
+ if (method === "qrl_chainId") {
3985
+ return this.connectionManager.getChainId();
3986
+ }
3987
+ if (method === "qrl_accounts") {
3988
+ return this.connectionManager.getAccounts();
3989
+ }
3990
+ const policy = classifyRpcMethod(method);
3991
+ if (policy === "unsupported") {
3992
+ throw new Error(`Unsupported method: ${method}`);
3993
+ }
3994
+ const stableParams = snapshotRequestParams(params);
3995
+ const generation = this.requestGeneration;
3996
+ if (policy === "restricted") {
3997
+ if (requiresAuthorizedAccount(method) && this.connectionManager.getAccounts().length === 0) {
3998
+ throw new Error("No authorized account: call qrl_requestAccounts first");
3999
+ }
4000
+ const expectedChainId = validateRestrictedRequest(
4001
+ method,
4002
+ stableParams,
4003
+ this.connectionManager.getAccounts()
4004
+ );
4005
+ return this.enqueueRestrictedRequest(method, stableParams, expectedChainId, generation);
4006
+ }
4007
+ return this.performRequest(method, stableParams, false, void 0, generation);
4008
+ }
4009
+ /**
4010
+ * Keep at most one approval-bound request active. Wallet approval screens
4011
+ * are a serial human interaction, and request-id ordering must remain
4012
+ * stable even when a dApp fires several signing calls concurrently.
4013
+ */
4014
+ enqueueRestrictedRequest(method, params, expectedChainId, generation) {
4015
+ if (!this.restrictedRequestActive) {
4016
+ this.restrictedRequestActive = true;
4017
+ return this.runRestrictedRequest(method, params, expectedChainId, generation);
4018
+ }
4019
+ return new Promise((resolve, reject) => {
4020
+ this.restrictedRequestQueue.push({
4021
+ method,
4022
+ params,
4023
+ expectedChainId,
4024
+ generation,
4025
+ resolve,
4026
+ reject
4027
+ });
4028
+ });
4029
+ }
4030
+ runRestrictedRequest(method, params, expectedChainId, generation) {
4031
+ const task = this.performRequest(method, params, true, expectedChainId, generation);
4032
+ void task.then(
4033
+ () => {
4034
+ this.finishRestrictedRequest();
4035
+ },
4036
+ () => {
4037
+ this.finishRestrictedRequest();
4038
+ }
4039
+ );
4040
+ return task;
4041
+ }
4042
+ finishRestrictedRequest() {
4043
+ const next = this.restrictedRequestQueue.shift();
4044
+ if (!next) {
4045
+ this.restrictedRequestActive = false;
4046
+ return;
4047
+ }
4048
+ const task = this.runRestrictedRequest(
4049
+ next.method,
4050
+ next.params,
4051
+ next.expectedChainId,
4052
+ next.generation
4053
+ );
4054
+ void task.then(next.resolve, next.reject);
4055
+ }
4056
+ async performRequest(method, params, restricted, expectedChainId, generation) {
4057
+ this.assertRequestGeneration(generation);
4058
+ if (this.connectionManager.getStatus() !== "connected" /* CONNECTED */) {
4059
+ if (method === "qrl_requestAccounts" && this.connectionManager.isPaired() && this.connectionManager.getAccounts().length > 0) {
4060
+ return this.connectionManager.getAccounts();
4061
+ }
4062
+ const joined = await this.connectionManager.ensureChannelJoined();
4063
+ this.assertRequestGeneration(generation);
4064
+ if (!joined) {
4065
+ throw new Error("Not connected to QRL Wallet");
4066
+ }
4067
+ }
4068
+ if (this.requestCounter >= Number.MAX_SAFE_INTEGER) {
4069
+ throw new Error("JSON-RPC request id counter exhausted");
4070
+ }
4071
+ const id = `${this.requestIdPrefix}-${++this.requestCounter}`;
4072
+ return new Promise((resolve, reject) => {
4073
+ const pending = {
4074
+ id,
4075
+ method,
4076
+ params,
4077
+ expectedChainId,
4078
+ resolve,
4079
+ reject,
4080
+ timestamp: Date.now()
4081
+ };
4082
+ this.pendingRequests.set(id, pending);
4083
+ const timeout = setTimeout(() => {
4084
+ const timedOut = this.pendingRequests.get(id);
4085
+ if (!timedOut) return;
4086
+ this.pendingRequests.delete(id);
4087
+ timedOut.reject(new Error(`Request timeout: ${method} (${REQUEST_TIMEOUT_MS}ms)`));
4088
+ }, REQUEST_TIMEOUT_MS);
4089
+ const originalResolve = pending.resolve;
4090
+ const originalReject = pending.reject;
4091
+ pending.resolve = (result) => {
4092
+ clearTimeout(timeout);
4093
+ this.settleInflight(id);
4094
+ originalResolve(result);
4095
+ };
4096
+ pending.reject = (error2) => {
4097
+ clearTimeout(timeout);
4098
+ this.settleInflight(id);
4099
+ originalReject(error2);
4100
+ };
4101
+ if (restricted) {
4102
+ this.persistInflight(id, method, expectedChainId);
4103
+ }
4104
+ let sent;
4105
+ try {
4106
+ sent = this.connectionManager.sendJsonRpc({
4107
+ jsonrpc: "2.0",
4108
+ id,
4109
+ method,
4110
+ params
4111
+ });
4112
+ } catch (err) {
4113
+ this.pendingRequests.delete(id);
4114
+ pending.reject(err instanceof Error ? err : new Error(String(err)));
4115
+ return;
4116
+ }
4117
+ sent.catch((err) => {
4118
+ const failed = this.pendingRequests.get(id);
4119
+ if (!failed) return;
4120
+ this.pendingRequests.delete(id);
4121
+ failed.reject(err instanceof Error ? err : new Error(String(err)));
4122
+ });
4123
+ if (this.shouldRedirectToWallet(method)) {
4124
+ void sent.then(
4125
+ () => attemptWalletRedirect(this.walletWakeUri()),
4126
+ () => void 0
4127
+ );
4128
+ }
4129
+ });
4130
+ }
4131
+ /**
4132
+ * Redirect only for calls the user must approve, on mobile, and only when
4133
+ * the wallet is actually absent from the channel: right after pairing its
4134
+ * socket lingers and a pointless app flash would be jarring.
4135
+ *
4136
+ * qrl_requestAccounts is excluded even though it is restricted: dApps call
4137
+ * it on page load to restore a session, so it can run with no user gesture
4138
+ * (an unsolicited "Open in MyQRLWallet?" sheet on load) and pairing UIs
4139
+ * already own their own deep-link step.
4140
+ */
4141
+ shouldRedirectToWallet(method) {
4142
+ return this.options.walletRedirectOnRequest !== false && classifyRpcMethod(method) === "restricted" && method !== "qrl_requestAccounts" && isMobileBrowser() && !this.connectionManager.isWalletPresent();
4143
+ }
4144
+ // ── In-flight persistence (survive the return-redirect reload) ──────
4145
+ readInflight() {
4146
+ if (typeof localStorage === "undefined") return [];
4147
+ try {
4148
+ const raw = localStorage.getItem(this.inflightKey);
4149
+ if (!raw) return [];
4150
+ const parsed = JSON.parse(raw);
4151
+ if (!Array.isArray(parsed)) return [];
4152
+ const now = Date.now();
4153
+ const records = [];
4154
+ for (const v of parsed) {
4155
+ const rec = parseInflightRecord(v);
4156
+ if (rec && now - rec.ts <= REQUEST_TIMEOUT_MS) records.push(rec);
4157
+ }
4158
+ return records;
4159
+ } catch {
4160
+ return [];
4161
+ }
4162
+ }
4163
+ writeInflight(records) {
4164
+ if (typeof localStorage === "undefined") return;
4165
+ try {
4166
+ if (records.length === 0) {
4167
+ localStorage.removeItem(this.inflightKey);
4168
+ } else {
4169
+ localStorage.setItem(this.inflightKey, JSON.stringify(records));
4170
+ }
4171
+ } catch {
4172
+ }
4173
+ }
4174
+ persistInflight(id, method, expectedChainId) {
4175
+ const records = this.readInflight().filter((r) => r.id !== id);
4176
+ records.push({ id, method, ts: Date.now(), expectedChainId });
4177
+ this.writeInflight(records);
4178
+ }
4179
+ settleInflight(id) {
4180
+ this.orphanedRequests.delete(id);
4181
+ this.writeInflight(this.readInflight().filter((r) => r.id !== id));
4182
+ }
4183
+ clearAllInflight() {
4184
+ this.orphanedRequests.clear();
4185
+ this.writeInflight([]);
4186
+ }
4187
+ /**
4188
+ * Wake URI for the wallet app. Carries no pairing payload: the app
4189
+ * foregrounds on any `qrlconnect:` URL and re-joins its sessions (draining
4190
+ * the buffered request); the cid lets a future wallet jump straight to the
4191
+ * right approval.
4192
+ *
4193
+ * MUST stay query-only (no host/path). The mobile app routes deep links
4194
+ * with expo-router, which maps anything after the scheme to an in-app
4195
+ * route: `qrlconnect://resume?...` lands on the "+not-found" screen and
4196
+ * unmounts the WebView, so the wake achieves nothing. The hostless form
4197
+ * routes to the WebView tab exactly like the pairing URIs
4198
+ * (`qrlconnect://?q=...`), which is the whole point of the wake.
4199
+ */
4200
+ walletWakeUri() {
4201
+ return `qrlconnect://?wake=${encodeURIComponent(this.connectionManager.getChannelId())}`;
4202
+ }
4203
+ /**
4204
+ * Get the current connection status.
4205
+ */
4206
+ getStatus() {
4207
+ return this.connectionManager.getStatus();
4208
+ }
4209
+ /**
4210
+ * Get connected accounts.
4211
+ */
4212
+ getAccounts() {
4213
+ return this.connectionManager.getAccounts();
4214
+ }
4215
+ /**
4216
+ * Get the channel ID for this connection.
4217
+ */
4218
+ getChannelId() {
4219
+ return this.connectionManager.getChannelId();
4220
+ }
4221
+ /**
4222
+ * Check if connected and keys exchanged.
4223
+ */
4224
+ isConnected() {
4225
+ return this.connectionManager.getStatus() === "connected" /* CONNECTED */;
4226
+ }
4227
+ /**
4228
+ * Check if a stored session exists that can be reconnected.
4229
+ */
4230
+ hasStoredSession() {
4231
+ return this.connectionManager.hasStoredSession();
4232
+ }
4233
+ /**
4234
+ * True while a pairing exists (keys exchanged), even when the wallet app
4235
+ * is backgrounded and its socket is out of the channel. Combine with
4236
+ * getStatus() to tell "wallet away, session intact" (paired + WAITING)
4237
+ * apart from "waiting for a first scan" in pairing UIs.
4238
+ */
4239
+ isPaired() {
4240
+ return this.connectionManager.isPaired();
4241
+ }
4242
+ /** True while the wallet is actually present in the relay channel. */
4243
+ isWalletPresent() {
4244
+ return this.connectionManager.isWalletPresent();
4245
+ }
4246
+ /**
4247
+ * Reset the connection and start a fresh pairing with a new channel.
4248
+ * Use this when the user wants to create a new connection instead of
4249
+ * reconnecting to an existing session.
4250
+ */
4251
+ async newConnection() {
4252
+ if (this.lifecycleTransition) {
4253
+ throw new Error("Connection lifecycle transition is already in progress");
4254
+ }
4255
+ this.lifecycleTransition = "reset";
4256
+ this.cancelAllRequests(new Error("Connection reset"));
4257
+ this.clearAllInflight();
4258
+ try {
4259
+ await this.connectionManager.resetForNewChannel();
4260
+ this.setupResumeListeners();
4261
+ return await this.connectionManager.getConnectionURI();
4262
+ } finally {
4263
+ this.lifecycleTransition = null;
4264
+ }
4265
+ }
4266
+ /**
4267
+ * Disconnect from wallet and clean up. Returns once the TERMINATE has
4268
+ * either been flushed to the relay or the 800ms best-effort window has
4269
+ * elapsed - the wallet gets an instant disconnect instead of landing in
4270
+ * its stale-session grace period.
4271
+ */
4272
+ disconnect() {
4273
+ if (this.disconnectInFlight) return this.disconnectInFlight;
4274
+ if (this.lifecycleTransition) {
4275
+ return Promise.reject(new Error("Connection lifecycle transition is already in progress"));
4276
+ }
4277
+ this.lifecycleTransition = "disconnect";
4278
+ this.cancelAllRequests(new Error("Disconnected"));
4279
+ this.clearAllInflight();
4280
+ this.teardownResumeListeners();
4281
+ const task = Promise.resolve().then(() => this.connectionManager.disconnect()).finally(() => {
4282
+ this.lifecycleTransition = null;
4283
+ if (this.disconnectInFlight === task) this.disconnectInFlight = null;
4284
+ });
4285
+ this.disconnectInFlight = task;
4286
+ return task;
4287
+ }
4288
+ };
4289
+
4290
+ // src/signing/messageDigest.ts
4291
+ function computeMessageDigest(messageBytes) {
4292
+ if (!(messageBytes instanceof Uint8Array)) {
4293
+ throw new Error("messageBytes must be a Uint8Array");
4294
+ }
4295
+ return shake256Digest(concatBytes(SCHEME_TAG_MSG, messageBytes), DIGEST_LEN);
4296
+ }
4297
+
4298
+ // src/signing/verify.ts
4299
+ var ML_DSA_87_DESCRIPTOR_TYPE = 1;
4300
+ var ML_DSA_DESCRIPTOR_BYTES = 3;
4301
+ var ML_DSA_87_PUBLIC_KEY_BYTES = 2592;
4302
+ var ML_DSA_87_SIGNATURE_BYTES = 4627;
4303
+ var CURRENT_QRL_ADDRESS_BYTES = 20;
4304
+ function bytesOrHex(v) {
4305
+ if (v instanceof Uint8Array) return new Uint8Array(v);
4306
+ if (typeof v === "string") return hexToBytes(v);
4307
+ throw new Error("expected Uint8Array or 0x-hex string");
4308
+ }
4309
+ function fixedBytesOrHex(value, expectedBytes) {
4310
+ if (value instanceof Uint8Array) {
4311
+ return value.length === expectedBytes ? new Uint8Array(value) : void 0;
4312
+ }
4313
+ if (typeof value !== "string" || value.length !== expectedBytes * 2 + 2 || !/^0x[0-9a-fA-F]+$/.test(value)) {
4314
+ return void 0;
4315
+ }
4316
+ return hexToBytes(value);
4317
+ }
4318
+ function verifyMessageBytes(signature, publicKey, messageBytes) {
4319
+ const digest = computeMessageDigest(messageBytes);
4320
+ return mldsaVerify(signature, digest, publicKey, SCHEME_TAG_MSG);
4321
+ }
4322
+ function verifyTypedDataBytes(signature, publicKey, payload) {
4323
+ const digest = computeTypedDataDigest(payload);
4324
+ return mldsaVerify(signature, digest, publicKey, SCHEME_TAG_TYPED);
4325
+ }
4326
+ function verifyMessageSignature({
4327
+ signature,
4328
+ publicKey,
2400
4329
  messageBytes
2401
4330
  }) {
2402
4331
  try {
2403
- const sig = bytesOrHex(signature);
2404
- const pk = bytesOrHex(publicKey);
4332
+ const sig = fixedBytesOrHex(signature, ML_DSA_87_SIGNATURE_BYTES);
4333
+ const pk = fixedBytesOrHex(publicKey, ML_DSA_87_PUBLIC_KEY_BYTES);
4334
+ if (!sig || !pk) return false;
2405
4335
  const msg = bytesOrHex(messageBytes);
2406
- const digest = computeMessageDigest(msg);
2407
- return mldsaVerify(sig, digest, pk, SCHEME_TAG_MSG);
4336
+ return verifyMessageBytes(sig, pk, msg);
2408
4337
  } catch {
2409
4338
  return false;
2410
4339
  }
2411
4340
  }
2412
- function verifyTypedData({ signature, publicKey, payload }) {
4341
+ function verifyTypedDataSignature({
4342
+ signature,
4343
+ publicKey,
4344
+ payload
4345
+ }) {
2413
4346
  try {
2414
- const sig = bytesOrHex(signature);
2415
- const pk = bytesOrHex(publicKey);
2416
- const digest = computeTypedDataDigest(payload);
2417
- return mldsaVerify(sig, digest, pk, SCHEME_TAG_TYPED);
4347
+ const sig = fixedBytesOrHex(signature, ML_DSA_87_SIGNATURE_BYTES);
4348
+ const pk = fixedBytesOrHex(publicKey, ML_DSA_87_PUBLIC_KEY_BYTES);
4349
+ if (!sig || !pk) return false;
4350
+ return verifyTypedDataBytes(sig, pk, payload);
2418
4351
  } catch {
2419
4352
  return false;
2420
4353
  }
2421
4354
  }
4355
+ function publicKeyMatchesSigner(expectedSigner, descriptor, publicKey) {
4356
+ if (!isCurrentQrlAddress(expectedSigner)) return false;
4357
+ if (descriptor[0] !== ML_DSA_87_DESCRIPTOR_TYPE) {
4358
+ return false;
4359
+ }
4360
+ const addressBytes = shake256Digest(
4361
+ concatBytes(descriptor, publicKey),
4362
+ CURRENT_QRL_ADDRESS_BYTES
4363
+ );
4364
+ const derived = `Q${bytesToHex(addressBytes).slice(2)}`;
4365
+ return derived.toLowerCase() === expectedSigner.toLowerCase();
4366
+ }
4367
+ function verifyMessageForSigner({
4368
+ expectedSigner,
4369
+ descriptor,
4370
+ signature,
4371
+ publicKey,
4372
+ messageBytes
4373
+ }) {
4374
+ try {
4375
+ if (descriptor === void 0) return false;
4376
+ const descriptorBytes = fixedBytesOrHex(descriptor, ML_DSA_DESCRIPTOR_BYTES);
4377
+ const publicKeyBytes = fixedBytesOrHex(publicKey, ML_DSA_87_PUBLIC_KEY_BYTES);
4378
+ const signatureBytes = fixedBytesOrHex(signature, ML_DSA_87_SIGNATURE_BYTES);
4379
+ if (!descriptorBytes || !publicKeyBytes || !signatureBytes) return false;
4380
+ const message = bytesOrHex(messageBytes);
4381
+ return publicKeyMatchesSigner(expectedSigner, descriptorBytes, publicKeyBytes) && verifyMessageBytes(signatureBytes, publicKeyBytes, message);
4382
+ } catch {
4383
+ return false;
4384
+ }
4385
+ }
4386
+ function verifyTypedDataForSigner({
4387
+ expectedSigner,
4388
+ descriptor,
4389
+ signature,
4390
+ publicKey,
4391
+ payload
4392
+ }) {
4393
+ try {
4394
+ if (descriptor === void 0) return false;
4395
+ const descriptorBytes = fixedBytesOrHex(descriptor, ML_DSA_DESCRIPTOR_BYTES);
4396
+ const publicKeyBytes = fixedBytesOrHex(publicKey, ML_DSA_87_PUBLIC_KEY_BYTES);
4397
+ const signatureBytes = fixedBytesOrHex(signature, ML_DSA_87_SIGNATURE_BYTES);
4398
+ if (!descriptorBytes || !publicKeyBytes || !signatureBytes) return false;
4399
+ return publicKeyMatchesSigner(expectedSigner, descriptorBytes, publicKeyBytes) && verifyTypedDataBytes(signatureBytes, publicKeyBytes, payload);
4400
+ } catch {
4401
+ return false;
4402
+ }
4403
+ }
4404
+ function verifyMessage(params) {
4405
+ return verifyMessageSignature(params);
4406
+ }
4407
+ function verifyTypedData(params) {
4408
+ return verifyTypedDataSignature(params);
4409
+ }
2422
4410
  // Annotate the CommonJS export names for ESM import in node:
2423
4411
  0 && (module.exports = {
2424
4412
  BLOB_LEN,
4413
+ CAP_LEN,
2425
4414
  CID_LEN,
2426
4415
  ConnectionManager,
2427
4416
  ConnectionStatus,
2428
4417
  DEFAULT_RELAY_URL,
2429
4418
  DIGEST_LEN,
4419
+ EXPLICITLY_UNSUPPORTED_METHODS,
2430
4420
  FP_LEN,
2431
4421
  KeyExchange,
2432
4422
  KeyExchangeMessageType,
4423
+ MAX_CONNECTION_URI_LENGTH,
4424
+ MAX_JSON_RPC_ID_LENGTH,
4425
+ MAX_JSON_RPC_METHOD_LENGTH,
4426
+ MAX_RELAY_URL_LENGTH,
4427
+ ML_DSA_87_PUBLIC_KEY_BYTES,
4428
+ ML_DSA_87_SIGNATURE_BYTES,
4429
+ ML_DSA_DESCRIPTOR_BYTES,
2433
4430
  MessageType,
4431
+ PAIRING_CAPABILITY_LEN,
2434
4432
  PROTOCOL_VERSION,
2435
4433
  QRLConnect,
2436
4434
  QRLConnectProvider,
@@ -2441,12 +4439,14 @@ function verifyTypedData({ signature, publicKey, payload }) {
2441
4439
  SCHEME_VERSION_MSG,
2442
4440
  SCHEME_VERSION_TYPED,
2443
4441
  SocketClient,
4442
+ TYPED_DATA_LIMITS,
2444
4443
  UNRESTRICTED_METHODS,
2445
4444
  attemptWalletRedirect,
2446
4445
  bytesToHex,
2447
4446
  cidFromString,
2448
4447
  cidRandom,
2449
4448
  cidToString,
4449
+ classifyRpcMethod,
2450
4450
  computeFingerprint,
2451
4451
  computeMessageDigest,
2452
4452
  computeTypedDataDigest,
@@ -2456,11 +4456,22 @@ function verifyTypedData({ signature, publicKey, payload }) {
2456
4456
  fingerprintEquals,
2457
4457
  generateConnectionURI,
2458
4458
  getAppStoreUrl,
4459
+ hasSigningDescriptor,
2459
4460
  hashStruct,
2460
4461
  hexToBytes,
2461
4462
  isMobileBrowser,
4463
+ isQrlSignedMessageResult,
4464
+ isQrlSignedResult,
4465
+ isQrlSignedTypedDataResult,
4466
+ isValidJsonRpcId,
4467
+ isValidJsonRpcMethod,
4468
+ normalizeRelayUrl,
2462
4469
  parseConnectionURI,
2463
4470
  typeHash,
2464
4471
  verifyMessage,
2465
- verifyTypedData
4472
+ verifyMessageForSigner,
4473
+ verifyMessageSignature,
4474
+ verifyTypedData,
4475
+ verifyTypedDataForSigner,
4476
+ verifyTypedDataSignature
2466
4477
  });