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