@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.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import EventEmitter from 'eventemitter3';
2
2
 
3
3
  /**
4
- * Post-quantum handshake (v2).
4
+ * Post-quantum handshake (v3).
5
5
  *
6
6
  * Flow:
7
7
  * dApp relay wallet
8
8
  * ────────────────────────────────────────────────────────────────────
9
- * initiate() → (pk, sk)
10
- * QR(cid, pk) rendered scan QR
11
- * receiveQR(cid, pk)
9
+ * initiate() → (pk, sk, cap)
10
+ * QR(cid, fp, cap) rendered scan QR
11
+ * receiveQR(cid, pk, cap)
12
12
  * → SYNACK{ct, c0}
13
13
  * ◄── SYNACK ─────── send
14
14
  * onSynAck(cid, SYNACK)
@@ -20,6 +20,7 @@ import EventEmitter from 'eventemitter3';
20
20
  */
21
21
 
22
22
  interface Session {
23
+ protocolVersion: 3;
23
24
  cid: Uint8Array;
24
25
  key: CryptoKey;
25
26
  htx: Uint8Array;
@@ -30,6 +31,7 @@ interface Session {
30
31
  }
31
32
  /** Wire-safe persisted form for localStorage. */
32
33
  interface PersistedSession {
34
+ protocolVersion: 3;
33
35
  cid: string;
34
36
  kAeadRaw: string;
35
37
  htx: string;
@@ -49,6 +51,10 @@ interface AckMessage {
49
51
  c1: string;
50
52
  v: number;
51
53
  }
54
+ interface InitiatedKeyExchange {
55
+ publicKey: Uint8Array;
56
+ capability: Uint8Array;
57
+ }
52
58
  interface KeyExchangeEvents {
53
59
  keys_exchanged: () => void;
54
60
  step_change: (step: KeyExchangeMessageType) => void;
@@ -56,29 +62,40 @@ interface KeyExchangeEvents {
56
62
  declare class KeyExchange extends EventEmitter<KeyExchangeEvents> {
57
63
  private isOriginator;
58
64
  private keypair;
65
+ private capability;
59
66
  private session;
60
67
  private step;
61
68
  private awaitingSynAck;
62
69
  private awaitingAck;
70
+ private awaitingAckDelivery;
63
71
  private keysExchanged;
72
+ private stateGeneration;
64
73
  private lastAck;
65
74
  constructor(isOriginator: boolean, restored?: Session);
66
75
  /**
67
76
  * Originator (dApp) side: begin a fresh handshake by generating an
68
- * ephemeral ML-KEM-768 keypair. Returns pk to embed in the QR URI.
77
+ * ephemeral ML-KEM-768 keypair and a fresh 32-byte pairing capability.
78
+ * Both returned arrays are copies; the originator retains its own secret
79
+ * capability until the handshake succeeds or is retired.
69
80
  */
70
- initiate(): Uint8Array;
81
+ initiate(): InitiatedKeyExchange;
71
82
  /**
72
83
  * Originator: handle a SYNACK received over the relay.
73
84
  * Returns an ACK wire message to send, or null if the SYNACK was a
74
85
  * duplicate (handshake already complete).
75
86
  */
76
87
  onSynAck(cid: Uint8Array, msg: SynAckMessage): Promise<AckMessage | null>;
88
+ /**
89
+ * Originator: publish completion only after the relay acknowledged ACK.
90
+ * This is deliberately separate from onSynAck() so transport ambiguity
91
+ * cannot leave a live or persistable one-sided session.
92
+ */
93
+ confirmOriginatorAckDelivered(): void;
77
94
  /**
78
95
  * Responder (wallet) side: begin handshake from a scanned QR.
79
96
  * Returns the SYNACK wire message to emit on the relay.
80
97
  */
81
- receiveQR(cid: Uint8Array, pk: Uint8Array): Promise<SynAckMessage>;
98
+ receiveQR(cid: Uint8Array, pk: Uint8Array, capability: Uint8Array): Promise<SynAckMessage>;
82
99
  /**
83
100
  * Responder: verify an incoming ACK and finalize the session.
84
101
  * Duplicates are silently ignored (handshake is idempotent).
@@ -108,10 +125,11 @@ interface DAppMetadata {
108
125
  url: string;
109
126
  icon?: string;
110
127
  /**
111
- * Optional "return to dApp" target (a URL or app deep-link scheme). Sent to
112
- * the wallet in ORIGINATOR_INFO; after the wallet resolves a restricted
113
- * request it bounces the user back here (WalletConnect-style peer redirect)
114
- * so a same-device deep-link flow does not strand the user in the wallet.
128
+ * Optional credential-free HTTP(S) "return to dApp" target. HTTPS is
129
+ * required except for explicit loopback development URLs. Sent to the
130
+ * wallet in ORIGINATOR_INFO; after the wallet resolves a restricted request
131
+ * it bounces the user back here so a same-device flow does not strand the
132
+ * user in the wallet.
115
133
  */
116
134
  redirectUrl?: string;
117
135
  }
@@ -122,11 +140,12 @@ interface DAppMetadata {
122
140
  * the ML-KEM keypair is ephemeral and zeroized after the handshake.
123
141
  * Re-pair (generate a new QR) to rotate the session key.
124
142
  *
125
- * v3 checkpoints the AEAD counters on every seal/open. v2 records (sparse
126
- * checkpoints, stale-counter nonce-reuse risk on restore) are dropped.
143
+ * v5 checkpoints every seal/open, requires exclusive browser-tab ownership,
144
+ * and proves the AEAD key came from a PQP3 capability-bound handshake.
145
+ * Older records cannot prove all three invariants and are dropped.
127
146
  */
128
147
  interface DAppSession {
129
- version: 3;
148
+ version: 5;
130
149
  channelId: string;
131
150
  keyExchange: PersistedSession;
132
151
  dappMetadata: DAppMetadata;
@@ -140,6 +159,8 @@ interface PendingRequest {
140
159
  id: string | number;
141
160
  method: string;
142
161
  params?: unknown[] | undefined;
162
+ /** Immutable postcondition captured before caller-owned params can change. */
163
+ expectedChainId?: string | undefined;
143
164
  resolve: (result: unknown) => void;
144
165
  reject: (error: Error) => void;
145
166
  timestamp: number;
@@ -147,9 +168,10 @@ interface PendingRequest {
147
168
  /**
148
169
  * Key-exchange wire-message types.
149
170
  *
150
- * v2 note: only SYNACK (wallet→dApp) and ACK (dApp→wallet) are transmitted
151
- * over the relay. The SYN step is carried by the QR code itself - the dApp's
152
- * ML-KEM-768 encapsulation key is embedded in the URI, not sent on the wire.
171
+ * v3 note: only SYNACK (wallet to dApp) and ACK (dApp to wallet) are
172
+ * transmitted over the relay. The SYN step is represented by the pairing URI,
173
+ * while the relay carries only the dApp's ML-KEM public key. The URI contains
174
+ * the key fingerprint and a capability that is never uploaded to the relay.
153
175
  */
154
176
  declare enum KeyExchangeMessageType {
155
177
  SYN = "key_handshake_SYN",
@@ -182,6 +204,22 @@ declare enum ConnectionStatus {
182
204
  CONNECTED = "connected",
183
205
  RECONNECTING = "reconnecting"
184
206
  }
207
+ /**
208
+ * A response for a request issued by a PREVIOUS page load. Same-device
209
+ * approvals bounce back via a URL open, which reloads the dApp page and
210
+ * destroys the awaiting promise; the SDK persists in-flight restricted
211
+ * request ids and surfaces their eventual responses through this event.
212
+ */
213
+ interface LateResponse {
214
+ id: string | number;
215
+ method: string;
216
+ result?: unknown;
217
+ error?: {
218
+ code: number;
219
+ message: string;
220
+ data?: unknown;
221
+ };
222
+ }
185
223
  /** EIP-1193 provider events */
186
224
  interface ProviderEvents {
187
225
  connect: (info: {
@@ -198,6 +236,7 @@ interface ProviderEvents {
198
236
  data: unknown;
199
237
  }) => void;
200
238
  statusChanged: (status: ConnectionStatus) => void;
239
+ late_response: (payload: LateResponse) => void;
201
240
  }
202
241
  /** JSON-RPC request */
203
242
  interface JsonRpcRequest {
@@ -233,28 +272,65 @@ interface EIP6963ProviderInfoOverride {
233
272
  * Rich response object returned by both `qrl_signMessage` and
234
273
  * `qrl_signTypedData`. The signature alone is not enough to verify since
235
274
  * ML-DSA-87 public keys cannot be recovered from a signature, so the
236
- * wallet always returns the public key explicitly. Stateless verifiers
237
- * (e.g. the SDK's `verifyMessage` / `verifyTypedData`) need every field.
275
+ * wallet always returns the public key explicitly. Stateless verifiers need
276
+ * the original payload plus the signature fields below.
238
277
  */
239
278
  interface QrlSignedResult {
240
- /** 0x-hex of the 4595-byte ML-DSA-87 signature. */
279
+ /** 0x-hex of the 4627-byte ML-DSA-87 signature. */
241
280
  signature: string;
242
281
  /** 0x-hex of the 2592-byte ML-DSA-87 public key. */
243
282
  publicKey: string;
244
- /** 41-char checksummed Q-address derived from `publicKey`. */
283
+ /**
284
+ * 0x-hex of the 3-byte wallet descriptor used to derive `signer`.
285
+ * Optional while older wallet builds are upgraded; bound verification
286
+ * requires it.
287
+ */
288
+ descriptor?: string;
289
+ /** Current 41-character checksummed Q-address derived from descriptor + key. */
245
290
  signer: string;
246
291
  /** 0x-hex of the 64-byte SHAKE256 digest that was signed. */
247
292
  digest: string;
248
293
  /** Scheme tag: 'QRL-SIGN-MSG-v1' or 'QRL-SIGN-TYPED-v1'. */
249
- schemeVersion: string;
294
+ schemeVersion: QrlSigningSchemeVersion;
295
+ }
296
+ /** A current wallet response whose descriptor can be signer-bound. */
297
+ interface QrlSignedResultWithDescriptor extends QrlSignedResult {
298
+ descriptor: string;
299
+ }
300
+ type QrlSigningSchemeVersion = 'QRL-SIGN-MSG-v1' | 'QRL-SIGN-TYPED-v1';
301
+ /** Strict wire shape returned by `qrl_signMessage`. */
302
+ interface QrlSignedMessageResult extends QrlSignedResult {
303
+ schemeVersion: 'QRL-SIGN-MSG-v1';
250
304
  }
251
305
  /**
252
306
  * Response from `qrl_signTypedData`. Echoes `domain` so a stateless
253
307
  * verifier doesn't need to be told the domain out-of-band.
254
308
  */
255
309
  interface QrlSignedTypedDataResult extends QrlSignedResult {
310
+ schemeVersion: 'QRL-SIGN-TYPED-v1';
256
311
  domain: Record<string, unknown>;
257
312
  }
313
+ /** Every strict signing-result wire shape understood by this SDK. */
314
+ type QrlSigningResult = QrlSignedMessageResult | QrlSignedTypedDataResult;
315
+ /**
316
+ * Validate an unknown value as the exact `qrl_signMessage` result wire shape.
317
+ * This checks structure and fixed widths only. Use `verifyMessageForSigner`
318
+ * with the original message to authenticate the signature and signer binding.
319
+ */
320
+ declare function isQrlSignedMessageResult(value: unknown): value is QrlSignedMessageResult;
321
+ /**
322
+ * Validate an unknown value as the exact `qrl_signTypedData` result wire shape.
323
+ * This checks structure and fixed widths only. Use `verifyTypedDataForSigner`
324
+ * with the original payload to authenticate the signature and signer binding.
325
+ */
326
+ declare function isQrlSignedTypedDataResult(value: unknown): value is QrlSignedTypedDataResult;
327
+ /** Validate either exact signing-result wire shape from an unknown value. */
328
+ declare function isQrlSignedResult(value: unknown): value is QrlSigningResult;
329
+ /**
330
+ * Strictly validate an unknown signing result and narrow it to one carrying
331
+ * the exact 3-byte ML-DSA descriptor required for signer-bound verification.
332
+ */
333
+ declare function hasSigningDescriptor(result: unknown): result is QrlSigningResult & QrlSignedResultWithDescriptor;
258
334
  /**
259
335
  * `qrl_signMessage` params: `[signer, messageHex]`. `signer` must equal the
260
336
  * dApp's currently-connected account; the wallet rejects mismatches before
@@ -297,6 +373,16 @@ interface QRLConnectOptions {
297
373
  announceProvider?: boolean;
298
374
  /** Override the EIP-6963 announce metadata (name / icon / rdns). */
299
375
  providerInfo?: EIP6963ProviderInfoOverride;
376
+ /**
377
+ * On mobile browsers, deep-link (`qrlconnect://resume`) to foreground the
378
+ * wallet app when a restricted request is made while the wallet is absent
379
+ * from the relay channel. A backgrounded wallet app loses its socket
380
+ * within seconds; the relay buffers the request, and without the redirect
381
+ * it would sit there until the user remembers to switch apps on their
382
+ * own. Defaults to true. Set false when the dApp runs its own
383
+ * "open wallet" UX.
384
+ */
385
+ walletRedirectOnRequest?: boolean;
300
386
  }
301
387
 
302
388
  /**
@@ -322,8 +408,16 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
322
408
  private eip6963RequestListener;
323
409
  private resumeListener;
324
410
  private resumeDebounce;
411
+ private inflightKey;
412
+ private orphanedRequests;
325
413
  private readonly requestIdPrefix;
326
414
  private requestCounter;
415
+ private requestGeneration;
416
+ private requestCancellationMessage;
417
+ private lifecycleTransition;
418
+ private disconnectInFlight;
419
+ private restrictedRequestActive;
420
+ private restrictedRequestQueue;
327
421
  readonly isQRLConnect = true;
328
422
  constructor(options: QRLConnectOptions);
329
423
  private startEip6963Announce;
@@ -345,6 +439,10 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
345
439
  */
346
440
  stopEip6963Announce(): void;
347
441
  private setupConnectionListeners;
442
+ private handleJsonRpcResponse;
443
+ private cancelAllRequests;
444
+ private assertRequestGeneration;
445
+ private isCurrentChain;
348
446
  /**
349
447
  * Generate a connection URI for QR code display or deep link redirect.
350
448
  */
@@ -364,6 +462,45 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
364
462
  method: string;
365
463
  params?: unknown[];
366
464
  }): Promise<unknown>;
465
+ /**
466
+ * Keep at most one approval-bound request active. Wallet approval screens
467
+ * are a serial human interaction, and request-id ordering must remain
468
+ * stable even when a dApp fires several signing calls concurrently.
469
+ */
470
+ private enqueueRestrictedRequest;
471
+ private runRestrictedRequest;
472
+ private finishRestrictedRequest;
473
+ private performRequest;
474
+ /**
475
+ * Redirect only for calls the user must approve, on mobile, and only when
476
+ * the wallet is actually absent from the channel: right after pairing its
477
+ * socket lingers and a pointless app flash would be jarring.
478
+ *
479
+ * qrl_requestAccounts is excluded even though it is restricted: dApps call
480
+ * it on page load to restore a session, so it can run with no user gesture
481
+ * (an unsolicited "Open in MyQRLWallet?" sheet on load) and pairing UIs
482
+ * already own their own deep-link step.
483
+ */
484
+ private shouldRedirectToWallet;
485
+ private readInflight;
486
+ private writeInflight;
487
+ private persistInflight;
488
+ private settleInflight;
489
+ private clearAllInflight;
490
+ /**
491
+ * Wake URI for the wallet app. Carries no pairing payload: the app
492
+ * foregrounds on any `qrlconnect:` URL and re-joins its sessions (draining
493
+ * the buffered request); the cid lets a future wallet jump straight to the
494
+ * right approval.
495
+ *
496
+ * MUST stay query-only (no host/path). The mobile app routes deep links
497
+ * with expo-router, which maps anything after the scheme to an in-app
498
+ * route: `qrlconnect://resume?...` lands on the "+not-found" screen and
499
+ * unmounts the WebView, so the wake achieves nothing. The hostless form
500
+ * routes to the WebView tab exactly like the pairing URIs
501
+ * (`qrlconnect://?q=...`), which is the whole point of the wake.
502
+ */
503
+ private walletWakeUri;
367
504
  /**
368
505
  * Get the current connection status.
369
506
  */
@@ -384,6 +521,15 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
384
521
  * Check if a stored session exists that can be reconnected.
385
522
  */
386
523
  hasStoredSession(): boolean;
524
+ /**
525
+ * True while a pairing exists (keys exchanged), even when the wallet app
526
+ * is backgrounded and its socket is out of the channel. Combine with
527
+ * getStatus() to tell "wallet away, session intact" (paired + WAITING)
528
+ * apart from "waiting for a first scan" in pairing UIs.
529
+ */
530
+ isPaired(): boolean;
531
+ /** True while the wallet is actually present in the relay channel. */
532
+ isWalletPresent(): boolean;
387
533
  /**
388
534
  * Reset the connection and start a fresh pairing with a new channel.
389
535
  * Use this when the user wants to create a new connection instead of
@@ -402,7 +548,7 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
402
548
  /**
403
549
  * Connection Manager - orchestrates socket lifecycle, the post-quantum
404
550
  * handshake, encrypted message routing, and session persistence for the
405
- * dApp side of QRL Connect v2.
551
+ * dApp side of QRL Connect v3.
406
552
  */
407
553
 
408
554
  interface ConnectionManagerEvents {
@@ -415,6 +561,13 @@ interface ConnectionManagerEvents {
415
561
  chainId: string;
416
562
  }) => void;
417
563
  connection_lost: () => void;
564
+ /**
565
+ * The session is dead for good (wallet TERMINATE, relay 'close', or a
566
+ * tombstone observed on join). Distinct from a transient DISCONNECTED:
567
+ * in-flight requests can never be answered and must be failed promptly
568
+ * instead of running out the full request timeout.
569
+ */
570
+ session_terminated: () => void;
418
571
  error: (error: Error) => void;
419
572
  }
420
573
  declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
@@ -429,12 +582,22 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
429
582
  private storageKey;
430
583
  private unresponsiveTimer;
431
584
  private reconnectProbeTimer;
585
+ private restoreInFlight;
432
586
  private failedReconnects;
433
587
  private walletPresent;
434
588
  private pendingRestore;
435
589
  private messageQueue;
436
590
  private outboundQueue;
591
+ private persistenceQueue;
592
+ private readonly storage;
593
+ private readonly persistenceEnabled;
594
+ private readonly sessionOwnership;
595
+ private persistenceBroken;
596
+ private sessionCreatedAt;
597
+ private sessionGeneration;
598
+ private consecutiveDecryptFailures;
437
599
  private static MAX_RECONNECT_FAILURES;
600
+ private static MAX_DECRYPT_FAILURES;
438
601
  constructor(options: {
439
602
  dappMetadata: DAppMetadata;
440
603
  relayUrl?: string | undefined;
@@ -442,9 +605,11 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
442
605
  storageKey?: string | undefined;
443
606
  });
444
607
  private setupSocketListeners;
608
+ private handleAutoReconnected;
445
609
  private setupKeyExchangeListeners;
446
610
  /**
447
- * Generate a new v2 connection URI. Rotates channel id and keypair.
611
+ * Generate a new v3 connection URI. Rotates channel id, keypair, and the
612
+ * QR-only pairing capability.
448
613
  * Returns a `qrlconnect://?q=…` URI safe for QR-rendering or deep-link.
449
614
  */
450
615
  getConnectionURI(retryOnConflict?: boolean): Promise<string>;
@@ -453,6 +618,7 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
453
618
  * Returns false if there is nothing to restore.
454
619
  */
455
620
  reconnect(): Promise<boolean>;
621
+ private reconnectNow;
456
622
  /**
457
623
  * (Re)open the socket, join the persisted channel, drain buffered relay
458
624
  * messages, and settle status. Shared by the cold-restore reconnect() and
@@ -469,6 +635,25 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
469
635
  * own connect handler re-joins the channel and drains the relay buffer.
470
636
  */
471
637
  resume(): void;
638
+ /**
639
+ * Make sure the socket is open and joined to the paired session's channel
640
+ * so an outbound request can be routed - or relay-buffered if the wallet is
641
+ * currently absent (its socket dies within seconds of the app
642
+ * backgrounding; the relay holds channel traffic for it). Revives the
643
+ * socket after a probe-timeout teardown and hydrates a cold stored
644
+ * session. Returns false when there is no session to revive (never paired,
645
+ * explicit disconnect, or the channel was tombstoned).
646
+ */
647
+ ensureChannelJoined(): Promise<boolean>;
648
+ /**
649
+ * True while a pairing exists (handshake complete, session keys in
650
+ * memory), even when the wallet's socket is momentarily out of the
651
+ * channel. Survives the reconnect-probe teardown; false after an explicit
652
+ * disconnect or a relay tombstone.
653
+ */
654
+ isPaired(): boolean;
655
+ /** True while the relay roster last showed the wallet in the channel. */
656
+ isWalletPresent(): boolean;
472
657
  /**
473
658
  * A session was terminated for good (wallet sent a relay 'close', or the
474
659
  * join ack reported a tombstone). Clear local state and surface
@@ -478,11 +663,20 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
478
663
  private armReconnectProbe;
479
664
  private clearReconnectProbe;
480
665
  /**
481
- * Send a JSON-RPC request to the wallet (async, fire-and-forget).
666
+ * Send a JSON-RPC request to the wallet. Returns the outbound send promise
667
+ * (settled on the relay's ack) so callers that must sequence on delivery -
668
+ * e.g. the provider's wallet-wake redirect, which cannot navigate away
669
+ * before the ciphertext reaches the relay - can await it. Callers may also
670
+ * ignore it: failures are logged here either way.
482
671
  */
483
- sendJsonRpc(request: JsonRpcRequest): void;
672
+ sendJsonRpc(request: JsonRpcRequest): Promise<void>;
484
673
  getStatus(): ConnectionStatus;
485
674
  getAccounts(): string[];
675
+ /**
676
+ * Commit accounts returned by an approved qrl_requestAccounts response.
677
+ * WALLET_INFO alone cannot grant this authorization.
678
+ */
679
+ authorizeAccounts(value: unknown): Promise<string[]>;
486
680
  getChainId(): string;
487
681
  getChannelId(): string;
488
682
  /** Check (sync) if a persisted session exists and has not expired. */
@@ -495,6 +689,13 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
495
689
  * instant disconnect. Mirrors the wallet side's pattern.
496
690
  */
497
691
  private flushTerminate;
692
+ /**
693
+ * Ensure this socket is a relay participant before closing a restorable or
694
+ * temporarily disconnected channel. close_channel is intentionally
695
+ * participant-only on the relay, so a cold stored session cannot be retired
696
+ * by emitting the close event from an unjoined socket.
697
+ */
698
+ private prepareRelayChannelForRetirement;
498
699
  /**
499
700
  * Reset to a fresh channel + keypair. Sends TERMINATE to any live peer,
500
701
  * drops the persisted session, and prepares a clean state for
@@ -504,6 +705,17 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
504
705
  disconnect(): Promise<void>;
505
706
  private enqueueRelayMessage;
506
707
  private handleRelayMessage;
708
+ /**
709
+ * The receive stream is cryptographically unrecoverable: the peer's send
710
+ * counter is ahead of our recv counter for good (relay buffer TTL/cap
711
+ * dropped a ciphertext), so every future message would fail its tag.
712
+ * An encrypted TERMINATE cannot communicate this (the desynced peer could
713
+ * not open it either); tombstone the channel on the relay instead so the
714
+ * wallet learns the pairing is dead even if it only re-joins later, then
715
+ * clear local state and surface the standard terminated teardown.
716
+ */
717
+ private teardownDesyncedSession;
718
+ private teardownPersistenceFailedSession;
507
719
  private handleSynAck;
508
720
  private handleDecryptedMessage;
509
721
  private sendPlaintext;
@@ -522,8 +734,17 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
522
734
  private setStatus;
523
735
  private isDappParticipantConflictError;
524
736
  private areArraysEqual;
737
+ private invalidateSessionWork;
738
+ private captureSessionWorkContext;
739
+ private isSessionWorkCurrent;
740
+ private assertSessionWorkCurrent;
741
+ private acquireSessionOwnership;
742
+ private releaseSessionOwnership;
525
743
  private persistSession;
744
+ private persistSessionNow;
526
745
  private readStoredSession;
746
+ private removeStoredSession;
747
+ private removeStoredSessionOrThrow;
527
748
  private clearSession;
528
749
  }
529
750
 
@@ -532,7 +753,7 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
532
753
  */
533
754
 
534
755
  interface SocketClientEvents {
535
- message: (data: RelayMessage) => void;
756
+ message: (data: unknown) => void;
536
757
  connected: () => void;
537
758
  disconnected: (reason: string) => void;
538
759
  reconnected: (result: JoinResult) => void;
@@ -546,7 +767,7 @@ interface JoinResult {
546
767
  bufferedMessages: unknown[];
547
768
  /**
548
769
  * Base64-encoded KEM public key the relay has bound to this channel.
549
- * Populated for wallet joins (v2 protocol). `null` if the dApp hasn't
770
+ * Populated for wallet joins (v3 protocol). `null` if the dApp hasn't
550
771
  * registered a PK yet - wallet callers must treat this as a retry signal.
551
772
  */
552
773
  channelPublicKey: string | null;
@@ -571,6 +792,9 @@ declare class SocketClient extends EventEmitter<SocketClientEvents> {
571
792
  private publicKeyBase64;
572
793
  private seq;
573
794
  private pendingJoin;
795
+ private pendingSends;
796
+ private pendingJoinAttempts;
797
+ private get expectedPeerType();
574
798
  constructor(relayUrl: string, clientType: 'dapp' | 'wallet');
575
799
  /**
576
800
  * dApp-only: stash the PK so it gets uploaded on every join_channel
@@ -593,6 +817,7 @@ declare class SocketClient extends EventEmitter<SocketClientEvents> {
593
817
  */
594
818
  joinChannel(channelId: string): Promise<JoinResult>;
595
819
  private joinChannelNow;
820
+ private rejectPendingJoinAttempts;
596
821
  /**
597
822
  * Send a message through the relay.
598
823
  */
@@ -600,6 +825,21 @@ declare class SocketClient extends EventEmitter<SocketClientEvents> {
600
825
  success: boolean;
601
826
  buffered: boolean;
602
827
  }>;
828
+ private rejectPendingSends;
829
+ /**
830
+ * Emit an event and resolve once the relay acks it, or after a bounded
831
+ * flush window. Lets a caller await transmission before tearing the
832
+ * socket down.
833
+ */
834
+ private flushEmit;
835
+ /**
836
+ * Explicitly terminate the channel on the relay (durable tombstone), as
837
+ * opposed to the transient leaveChannel(). Used when the session is dead
838
+ * in a way an encrypted TERMINATE cannot communicate (AEAD desync: the
839
+ * peer could not open it). Resolves once the close is flushed or the
840
+ * bounded window elapses, so the caller can safely disconnect afterwards.
841
+ */
842
+ closeChannel(): Promise<boolean>;
603
843
  /**
604
844
  * Leave the current channel.
605
845
  */
@@ -655,6 +895,18 @@ declare function computeMessageDigest(messageBytes: Uint8Array): Uint8Array;
655
895
  * the wallet copy for the spec-level comments; behavior must stay in
656
896
  * lock-step or the cross-repo parity test fails on next CI.
657
897
  */
898
+ declare const TYPED_DATA_LIMITS: Readonly<{
899
+ maxTypes: 32;
900
+ maxFieldsPerType: 32;
901
+ maxTotalFields: 256;
902
+ maxTypeGraphDepth: 12;
903
+ maxArrayNesting: 12;
904
+ maxArrayLength: 256;
905
+ maxEncodedValues: 2048;
906
+ maxDynamicBytes: number;
907
+ maxIdentifierLength: 64;
908
+ maxFieldTypeLength: 128;
909
+ }>;
658
910
  type FieldType = string;
659
911
  interface TypedField {
660
912
  name: string;
@@ -674,28 +926,70 @@ declare function encodeType(primary: string, types: TypeMap): string;
674
926
  declare function typeHash(primary: string, types: TypeMap): Uint8Array;
675
927
  declare function encodeField(type: FieldType, value: unknown, types: TypeMap): Uint8Array;
676
928
  declare function hashStruct(primary: string, data: Message, types: TypeMap): Uint8Array;
677
- declare function computeTypedDataDigest(payload: TypedDataPayload): Uint8Array;
929
+ declare function computeTypedDataDigest(payload: unknown): Uint8Array;
678
930
 
679
931
  /**
680
932
  * Stateless verifiers for the two PQ signing schemes. The SDK exposes these
681
933
  * so a dApp can re-verify the rich response object returned by the wallet
682
- * without round-tripping back through the relay. They never touch a secret
683
- * key and never derive an address; that's the wallet's job.
934
+ * without round-tripping back through the relay. The lower-level
935
+ * verifyMessageSignature / verifyTypedDataSignature prove only that a
936
+ * supplied public key made the signature. Authorization decisions should use
937
+ * the ForSigner variants, which also bind that key to the expected
938
+ * current-format address.
684
939
  */
685
940
 
941
+ declare const ML_DSA_DESCRIPTOR_BYTES = 3;
942
+ declare const ML_DSA_87_PUBLIC_KEY_BYTES = 2592;
943
+ declare const ML_DSA_87_SIGNATURE_BYTES = 4627;
686
944
  interface VerifyMessageParams {
687
945
  signature: Uint8Array | string;
688
946
  publicKey: Uint8Array | string;
689
947
  /** 0x-hex bytes that were originally signed. */
690
948
  messageBytes: Uint8Array | string;
691
949
  }
692
- declare function verifyMessage({ signature, publicKey, messageBytes, }: VerifyMessageParams): boolean;
950
+ /**
951
+ * Verify against an explicitly supplied ML-DSA public key only. This does not
952
+ * prove that the key derives the signer claimed by a wallet response. Use
953
+ * verifyMessageForSigner for authentication or authorization decisions.
954
+ */
955
+ declare function verifyMessageSignature({ signature, publicKey, messageBytes, }: VerifyMessageParams): boolean;
693
956
  interface VerifyTypedDataParams {
694
957
  signature: Uint8Array | string;
695
958
  publicKey: Uint8Array | string;
696
959
  payload: TypedDataPayload;
697
960
  }
698
- declare function verifyTypedData({ signature, publicKey, payload }: VerifyTypedDataParams): boolean;
961
+ /**
962
+ * Verify against an explicitly supplied ML-DSA public key only. This does not
963
+ * bind that key to a Q-address. Use verifyTypedDataForSigner when signer
964
+ * identity matters.
965
+ */
966
+ declare function verifyTypedDataSignature({ signature, publicKey, payload, }: VerifyTypedDataParams): boolean;
967
+ interface VerifyMessageForSignerParams extends VerifyMessageParams {
968
+ /** Current Q + 40 hex address expected by the dApp. */
969
+ expectedSigner: string;
970
+ /** Exact 3-byte wallet descriptor. Missing legacy values fail closed. */
971
+ descriptor?: Uint8Array | string | undefined;
972
+ }
973
+ /** Verify both the message signature and public-key-to-signer binding. */
974
+ declare function verifyMessageForSigner({ expectedSigner, descriptor, signature, publicKey, messageBytes, }: VerifyMessageForSignerParams): boolean;
975
+ interface VerifyTypedDataForSignerParams extends VerifyTypedDataParams {
976
+ /** Current Q + 40 hex address expected by the dApp. */
977
+ expectedSigner: string;
978
+ /** Exact 3-byte wallet descriptor. Missing legacy values fail closed. */
979
+ descriptor?: Uint8Array | string | undefined;
980
+ }
981
+ /** Verify both the typed-data signature and public-key-to-signer binding. */
982
+ declare function verifyTypedDataForSigner({ expectedSigner, descriptor, signature, publicKey, payload, }: VerifyTypedDataForSignerParams): boolean;
983
+ /**
984
+ * @deprecated Use verifyMessageSignature for a key-only check, or
985
+ * verifyMessageForSigner when signer identity matters.
986
+ */
987
+ declare function verifyMessage(params: VerifyMessageParams): boolean;
988
+ /**
989
+ * @deprecated Use verifyTypedDataSignature for a key-only check, or
990
+ * verifyTypedDataForSigner when signer identity matters.
991
+ */
992
+ declare function verifyTypedData(params: VerifyTypedDataParams): boolean;
699
993
 
700
994
  /**
701
995
  * Local hex/bytes utilities for the signing module.
@@ -707,38 +1001,40 @@ declare function bytesToHex(bytes: Uint8Array): string;
707
1001
  declare function concatBytes(...parts: Uint8Array[]): Uint8Array;
708
1002
 
709
1003
  /**
710
- * v2 QR URI codec (PQP2).
1004
+ * v3 QR URI codec (PQP3).
711
1005
  *
712
- * Format: qrlconnect://?q=<base45(PQP2 || cid || fp)>[&r=<relayUrl>]
1006
+ * Format: qrlconnect://?q=<base45(PQP3 || cid || fp || cap)>[&r=<relayUrl>]
713
1007
  *
714
- * PQP2 = "PQP2" magic (4 B)
715
- * cid = 16 bytes (UUIDv4 raw) (16 B)
716
- * fp = SHA-256("pq-fp/v2" || cid || pk) (full 32B) (32 B)
717
- * total (52 B)
1008
+ * PQP3 = "PQP3" magic (4 B)
1009
+ * cid = 16 bytes (UUIDv4 raw) (16 B)
1010
+ * fp = SHA-256("pq-fp/v3" || cid || pk || cap) (full 32B) (32 B)
1011
+ * cap = fresh CSPRNG pairing capability (32 B)
1012
+ * total (84 B)
718
1013
  *
719
- * 52 bytes → ~78 alphanumeric chars after base45 → URI under 150 chars →
720
- * fits a version-5 QR at ECC-M. Scannable from any 2015-vintage camera.
1014
+ * The capability is a bearer secret. It is never uploaded to the relay and
1015
+ * must not be logged or persisted before the handshake consumes it.
721
1016
  *
722
1017
  * Security: the public key is NOT carried in the QR. It's uploaded by the
723
1018
  * dApp to the relay at channel creation, the relay binds it to the cid, and
724
- * the wallet fetches it via the join_channel ack. The 32-byte fp acts as
725
- * the out-of-band commitment: the wallet rejects the PK served by the relay
726
- * unless SHA-256("pq-fp/v2" || cid || pk) matches the fp from the QR.
727
- * Full-width SHA-256 (2^128 collision resistance) rules out brute-force
728
- * substitution of a maliciously-crafted PK with the same fingerprint.
1019
+ * the wallet fetches it via the join_channel ack. The fingerprint commits
1020
+ * the relay-served key to the exact out-of-band capability. The capability
1021
+ * is also bound into the transcript and HKDF by the key-exchange layer, so a
1022
+ * relay cannot fabricate an authenticated wallet hello.
729
1023
  *
730
- * Legacy v2.0 URIs (magic=PQP1, 1208-byte blob with embedded PK) are
731
- * rejected with a clear error by parseConnectionURI - this is a hard
732
- * break before 2.0.0 ships, no backcompat.
1024
+ * Legacy PQP1 and PQP2 URIs are rejected with a clear migration error. This
1025
+ * intentional v4.0 break prevents fallback to a pairing mode where the relay
1026
+ * could impersonate a wallet.
733
1027
  */
734
1028
  declare const CID_LEN = 16;
735
1029
  declare const FP_LEN = 32;
1030
+ declare const CAP_LEN = 32;
736
1031
  declare const BLOB_LEN: number;
1032
+ declare const MAX_CONNECTION_URI_LENGTH = 4096;
737
1033
  /**
738
- * Compute the full 32-byte fingerprint binding (label || cid || pk).
1034
+ * Compute the full 32-byte fingerprint binding (label || cid || pk || cap).
739
1035
  * Exported so the wallet side can re-derive and verify.
740
1036
  */
741
- declare function computeFingerprint(cid: Uint8Array, pk: Uint8Array): Promise<Uint8Array>;
1037
+ declare function computeFingerprint(cid: Uint8Array, pk: Uint8Array, capability: Uint8Array): Promise<Uint8Array>;
742
1038
  /**
743
1039
  * Constant-time comparison of two byte arrays. Both must be the same length.
744
1040
  * The verifier MUST use this rather than `===` or `indexOf` so timing signals
@@ -746,18 +1042,19 @@ declare function computeFingerprint(cid: Uint8Array, pk: Uint8Array): Promise<Ui
746
1042
  */
747
1043
  declare function fingerprintEquals(a: Uint8Array, b: Uint8Array): boolean;
748
1044
  /**
749
- * Encode (cid, pk) as a qrlconnect:// URI. Note the PK is not stored in the
750
- * URI - we compute its fingerprint and embed only that. The caller must
751
- * upload the PK to the relay separately before publishing this URI.
1045
+ * Encode (cid, pk, capability) as a qrlconnect:// URI. The PK is not stored
1046
+ * in the URI; only its capability-bound fingerprint is embedded. The caller
1047
+ * must upload the PK to the relay separately before publishing this URI.
752
1048
  *
753
1049
  * The optional `relayUrl` rides as a sibling query param (not inside the
754
1050
  * fp-bound blob) so a tampered relay can cause the pairing to fail but
755
1051
  * cannot substitute the PK - the fp still pins that.
756
1052
  */
757
- declare function generateConnectionURI(cid: Uint8Array, pk: Uint8Array, relayUrl?: string): Promise<string>;
1053
+ declare function generateConnectionURI(cid: Uint8Array, pk: Uint8Array, capability: Uint8Array, relayUrl?: string): Promise<string>;
758
1054
  interface ParsedURI {
759
1055
  cid: Uint8Array;
760
1056
  fp: Uint8Array;
1057
+ capability: Uint8Array;
761
1058
  relayUrl?: string | undefined;
762
1059
  }
763
1060
  declare function parseConnectionURI(uri: string): Promise<ParsedURI>;
@@ -797,20 +1094,28 @@ declare function getAppStoreUrl(): string;
797
1094
  declare function attemptWalletRedirect(uri: string, timeoutMs?: number): Promise<boolean>;
798
1095
 
799
1096
  declare const DEFAULT_RELAY_URL = "https://qrlwallet.com";
800
- declare const PROTOCOL_VERSION = 2;
1097
+ declare const PROTOCOL_VERSION = 3;
1098
+ declare const PAIRING_CAPABILITY_LEN = 32;
1099
+ declare const MAX_JSON_RPC_ID_LENGTH = 128;
1100
+ declare const MAX_JSON_RPC_METHOD_LENGTH = 128;
1101
+ declare const MAX_RELAY_URL_LENGTH = 2048;
801
1102
  /**
802
- * RPC methods that require user approval in the wallet.
803
- *
804
- * v3.0.0 replaces the Ethereum-flavored signing surface with two
805
- * post-quantum-native methods: `qrl_signMessage` for opaque bytes and
806
- * `qrl_signTypedData` for structured payloads (EIP-712-shaped, but with
807
- * SHAKE256 + native Dilithium ctx). The legacy methods (`personal_sign`,
808
- * `qrl_sign`, `qrl_signTypedData_v3`, `qrl_signTypedData_v4`) are no
809
- * longer accepted; a dApp calling them via this SDK will get a
810
- * "method not supported" error before the relay round-trip.
811
- */
812
- declare const RESTRICTED_METHODS: Set<string>;
813
- /** RPC methods that can be auto-proxied without approval */
814
- declare const UNRESTRICTED_METHODS: Set<string>;
1103
+ * Validate and canonicalize relay endpoints before they reach logging,
1104
+ * Socket.IO, or a QR URI. Production relays require HTTPS; plain HTTP is
1105
+ * limited to explicit loopback hostnames for local development.
1106
+ */
1107
+ declare function normalizeRelayUrl(value: unknown): string;
1108
+ /** Public snapshots for discovery. Provider authorization uses private sets. */
1109
+ declare const RESTRICTED_METHODS: ReadonlySet<string>;
1110
+ declare const UNRESTRICTED_METHODS: ReadonlySet<string>;
1111
+ declare const EXPLICITLY_UNSUPPORTED_METHODS: ReadonlySet<string>;
1112
+ type RpcMethodPolicy = 'restricted' | 'unrestricted' | 'unsupported';
1113
+ declare function isValidJsonRpcId(value: unknown): value is string | number;
1114
+ declare function isValidJsonRpcMethod(value: unknown): value is string;
1115
+ /**
1116
+ * Classify an RPC method with a closed allowlist. Any method absent from the
1117
+ * two positive policy sets is unsupported, including future signing methods.
1118
+ */
1119
+ declare function classifyRpcMethod(method: string): RpcMethodPolicy;
815
1120
 
816
- export { type AckMessage, BLOB_LEN, CID_LEN, ConnectionManager, ConnectionStatus, type DAppMetadata, type DAppSession, DEFAULT_RELAY_URL, DIGEST_LEN, type Domain, type EIP6963ProviderInfoOverride, FP_LEN, type JsonRpcRequest, type JsonRpcResponse, KeyExchange, KeyExchangeMessageType, type Message, MessageType, PROTOCOL_VERSION, type ParsedURI, type PendingRequest, type PersistedSession, type ProviderEvents, QRLConnectProvider as QRLConnect, type QRLConnectOptions, QRLConnectProvider, QRL_CONNECT_PROVIDER_INFO, type QrlSignMessageParams, type QrlSignTypedDataParams, type QrlSignedResult, type QrlSignedTypedDataResult, type QrlTypedDataPayload, RESTRICTED_METHODS, type RelayMessage, SCHEME_TAG_MSG, SCHEME_TAG_TYPED, SCHEME_VERSION_MSG, SCHEME_VERSION_TYPED, type Session, SocketClient, type StructDef, type SynAckMessage, type TypeMap, type TypedDataPayload, type TypedField, UNRESTRICTED_METHODS, type VerifyMessageParams, type VerifyTypedDataParams, attemptWalletRedirect, bytesToHex, cidFromString, cidRandom, cidToString, computeFingerprint, computeMessageDigest, computeTypedDataDigest, concatBytes, encodeField, encodeType, fingerprintEquals, generateConnectionURI, getAppStoreUrl, hashStruct, hexToBytes, isMobileBrowser, parseConnectionURI, typeHash, verifyMessage, verifyTypedData };
1121
+ export { type AckMessage, BLOB_LEN, CAP_LEN, CID_LEN, ConnectionManager, ConnectionStatus, type DAppMetadata, type DAppSession, DEFAULT_RELAY_URL, DIGEST_LEN, type Domain, type EIP6963ProviderInfoOverride, EXPLICITLY_UNSUPPORTED_METHODS, FP_LEN, type InitiatedKeyExchange, type JsonRpcRequest, type JsonRpcResponse, KeyExchange, KeyExchangeMessageType, MAX_CONNECTION_URI_LENGTH, MAX_JSON_RPC_ID_LENGTH, MAX_JSON_RPC_METHOD_LENGTH, MAX_RELAY_URL_LENGTH, ML_DSA_87_PUBLIC_KEY_BYTES, ML_DSA_87_SIGNATURE_BYTES, ML_DSA_DESCRIPTOR_BYTES, type Message, MessageType, PAIRING_CAPABILITY_LEN, PROTOCOL_VERSION, type ParsedURI, type PendingRequest, type PersistedSession, type ProviderEvents, QRLConnectProvider as QRLConnect, type QRLConnectOptions, QRLConnectProvider, QRL_CONNECT_PROVIDER_INFO, type QrlSignMessageParams, type QrlSignTypedDataParams, type QrlSignedMessageResult, type QrlSignedResult, type QrlSignedResultWithDescriptor, type QrlSignedTypedDataResult, type QrlSigningResult, type QrlSigningSchemeVersion, type QrlTypedDataPayload, RESTRICTED_METHODS, type RelayMessage, type RpcMethodPolicy, SCHEME_TAG_MSG, SCHEME_TAG_TYPED, SCHEME_VERSION_MSG, SCHEME_VERSION_TYPED, type Session, SocketClient, type StructDef, type SynAckMessage, TYPED_DATA_LIMITS, type TypeMap, type TypedDataPayload, type TypedField, UNRESTRICTED_METHODS, type VerifyMessageForSignerParams, type VerifyMessageParams, type VerifyTypedDataForSignerParams, type VerifyTypedDataParams, attemptWalletRedirect, bytesToHex, cidFromString, cidRandom, cidToString, classifyRpcMethod, computeFingerprint, computeMessageDigest, computeTypedDataDigest, concatBytes, encodeField, encodeType, fingerprintEquals, generateConnectionURI, getAppStoreUrl, hasSigningDescriptor, hashStruct, hexToBytes, isMobileBrowser, isQrlSignedMessageResult, isQrlSignedResult, isQrlSignedTypedDataResult, isValidJsonRpcId, isValidJsonRpcMethod, normalizeRelayUrl, parseConnectionURI, typeHash, verifyMessage, verifyMessageForSigner, verifyMessageSignature, verifyTypedData, verifyTypedDataForSigner, verifyTypedDataSignature };