@qrlwallet/connect 3.3.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",
@@ -250,28 +272,65 @@ interface EIP6963ProviderInfoOverride {
250
272
  * Rich response object returned by both `qrl_signMessage` and
251
273
  * `qrl_signTypedData`. The signature alone is not enough to verify since
252
274
  * ML-DSA-87 public keys cannot be recovered from a signature, so the
253
- * wallet always returns the public key explicitly. Stateless verifiers
254
- * (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.
255
277
  */
256
278
  interface QrlSignedResult {
257
- /** 0x-hex of the 4595-byte ML-DSA-87 signature. */
279
+ /** 0x-hex of the 4627-byte ML-DSA-87 signature. */
258
280
  signature: string;
259
281
  /** 0x-hex of the 2592-byte ML-DSA-87 public key. */
260
282
  publicKey: string;
261
- /** 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. */
262
290
  signer: string;
263
291
  /** 0x-hex of the 64-byte SHAKE256 digest that was signed. */
264
292
  digest: string;
265
293
  /** Scheme tag: 'QRL-SIGN-MSG-v1' or 'QRL-SIGN-TYPED-v1'. */
266
- 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';
267
304
  }
268
305
  /**
269
306
  * Response from `qrl_signTypedData`. Echoes `domain` so a stateless
270
307
  * verifier doesn't need to be told the domain out-of-band.
271
308
  */
272
309
  interface QrlSignedTypedDataResult extends QrlSignedResult {
310
+ schemeVersion: 'QRL-SIGN-TYPED-v1';
273
311
  domain: Record<string, unknown>;
274
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;
275
334
  /**
276
335
  * `qrl_signMessage` params: `[signer, messageHex]`. `signer` must equal the
277
336
  * dApp's currently-connected account; the wallet rejects mismatches before
@@ -353,6 +412,12 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
353
412
  private orphanedRequests;
354
413
  private readonly requestIdPrefix;
355
414
  private requestCounter;
415
+ private requestGeneration;
416
+ private requestCancellationMessage;
417
+ private lifecycleTransition;
418
+ private disconnectInFlight;
419
+ private restrictedRequestActive;
420
+ private restrictedRequestQueue;
356
421
  readonly isQRLConnect = true;
357
422
  constructor(options: QRLConnectOptions);
358
423
  private startEip6963Announce;
@@ -374,6 +439,10 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
374
439
  */
375
440
  stopEip6963Announce(): void;
376
441
  private setupConnectionListeners;
442
+ private handleJsonRpcResponse;
443
+ private cancelAllRequests;
444
+ private assertRequestGeneration;
445
+ private isCurrentChain;
377
446
  /**
378
447
  * Generate a connection URI for QR code display or deep link redirect.
379
448
  */
@@ -393,6 +462,15 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
393
462
  method: string;
394
463
  params?: unknown[];
395
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;
396
474
  /**
397
475
  * Redirect only for calls the user must approve, on mobile, and only when
398
476
  * the wallet is actually absent from the channel: right after pairing its
@@ -470,7 +548,7 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
470
548
  /**
471
549
  * Connection Manager - orchestrates socket lifecycle, the post-quantum
472
550
  * handshake, encrypted message routing, and session persistence for the
473
- * dApp side of QRL Connect v2.
551
+ * dApp side of QRL Connect v3.
474
552
  */
475
553
 
476
554
  interface ConnectionManagerEvents {
@@ -510,6 +588,13 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
510
588
  private pendingRestore;
511
589
  private messageQueue;
512
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;
513
598
  private consecutiveDecryptFailures;
514
599
  private static MAX_RECONNECT_FAILURES;
515
600
  private static MAX_DECRYPT_FAILURES;
@@ -520,9 +605,11 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
520
605
  storageKey?: string | undefined;
521
606
  });
522
607
  private setupSocketListeners;
608
+ private handleAutoReconnected;
523
609
  private setupKeyExchangeListeners;
524
610
  /**
525
- * 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.
526
613
  * Returns a `qrlconnect://?q=…` URI safe for QR-rendering or deep-link.
527
614
  */
528
615
  getConnectionURI(retryOnConflict?: boolean): Promise<string>;
@@ -585,6 +672,11 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
585
672
  sendJsonRpc(request: JsonRpcRequest): Promise<void>;
586
673
  getStatus(): ConnectionStatus;
587
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[]>;
588
680
  getChainId(): string;
589
681
  getChannelId(): string;
590
682
  /** Check (sync) if a persisted session exists and has not expired. */
@@ -597,6 +689,13 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
597
689
  * instant disconnect. Mirrors the wallet side's pattern.
598
690
  */
599
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;
600
699
  /**
601
700
  * Reset to a fresh channel + keypair. Sends TERMINATE to any live peer,
602
701
  * drops the persisted session, and prepares a clean state for
@@ -616,6 +715,7 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
616
715
  * clear local state and surface the standard terminated teardown.
617
716
  */
618
717
  private teardownDesyncedSession;
718
+ private teardownPersistenceFailedSession;
619
719
  private handleSynAck;
620
720
  private handleDecryptedMessage;
621
721
  private sendPlaintext;
@@ -634,8 +734,17 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
634
734
  private setStatus;
635
735
  private isDappParticipantConflictError;
636
736
  private areArraysEqual;
737
+ private invalidateSessionWork;
738
+ private captureSessionWorkContext;
739
+ private isSessionWorkCurrent;
740
+ private assertSessionWorkCurrent;
741
+ private acquireSessionOwnership;
742
+ private releaseSessionOwnership;
637
743
  private persistSession;
744
+ private persistSessionNow;
638
745
  private readStoredSession;
746
+ private removeStoredSession;
747
+ private removeStoredSessionOrThrow;
639
748
  private clearSession;
640
749
  }
641
750
 
@@ -644,7 +753,7 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
644
753
  */
645
754
 
646
755
  interface SocketClientEvents {
647
- message: (data: RelayMessage) => void;
756
+ message: (data: unknown) => void;
648
757
  connected: () => void;
649
758
  disconnected: (reason: string) => void;
650
759
  reconnected: (result: JoinResult) => void;
@@ -658,7 +767,7 @@ interface JoinResult {
658
767
  bufferedMessages: unknown[];
659
768
  /**
660
769
  * Base64-encoded KEM public key the relay has bound to this channel.
661
- * 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
662
771
  * registered a PK yet - wallet callers must treat this as a retry signal.
663
772
  */
664
773
  channelPublicKey: string | null;
@@ -683,6 +792,9 @@ declare class SocketClient extends EventEmitter<SocketClientEvents> {
683
792
  private publicKeyBase64;
684
793
  private seq;
685
794
  private pendingJoin;
795
+ private pendingSends;
796
+ private pendingJoinAttempts;
797
+ private get expectedPeerType();
686
798
  constructor(relayUrl: string, clientType: 'dapp' | 'wallet');
687
799
  /**
688
800
  * dApp-only: stash the PK so it gets uploaded on every join_channel
@@ -705,6 +817,7 @@ declare class SocketClient extends EventEmitter<SocketClientEvents> {
705
817
  */
706
818
  joinChannel(channelId: string): Promise<JoinResult>;
707
819
  private joinChannelNow;
820
+ private rejectPendingJoinAttempts;
708
821
  /**
709
822
  * Send a message through the relay.
710
823
  */
@@ -712,6 +825,7 @@ declare class SocketClient extends EventEmitter<SocketClientEvents> {
712
825
  success: boolean;
713
826
  buffered: boolean;
714
827
  }>;
828
+ private rejectPendingSends;
715
829
  /**
716
830
  * Emit an event and resolve once the relay acks it, or after a bounded
717
831
  * flush window. Lets a caller await transmission before tearing the
@@ -725,7 +839,7 @@ declare class SocketClient extends EventEmitter<SocketClientEvents> {
725
839
  * peer could not open it). Resolves once the close is flushed or the
726
840
  * bounded window elapses, so the caller can safely disconnect afterwards.
727
841
  */
728
- closeChannel(): Promise<void>;
842
+ closeChannel(): Promise<boolean>;
729
843
  /**
730
844
  * Leave the current channel.
731
845
  */
@@ -781,6 +895,18 @@ declare function computeMessageDigest(messageBytes: Uint8Array): Uint8Array;
781
895
  * the wallet copy for the spec-level comments; behavior must stay in
782
896
  * lock-step or the cross-repo parity test fails on next CI.
783
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
+ }>;
784
910
  type FieldType = string;
785
911
  interface TypedField {
786
912
  name: string;
@@ -800,28 +926,70 @@ declare function encodeType(primary: string, types: TypeMap): string;
800
926
  declare function typeHash(primary: string, types: TypeMap): Uint8Array;
801
927
  declare function encodeField(type: FieldType, value: unknown, types: TypeMap): Uint8Array;
802
928
  declare function hashStruct(primary: string, data: Message, types: TypeMap): Uint8Array;
803
- declare function computeTypedDataDigest(payload: TypedDataPayload): Uint8Array;
929
+ declare function computeTypedDataDigest(payload: unknown): Uint8Array;
804
930
 
805
931
  /**
806
932
  * Stateless verifiers for the two PQ signing schemes. The SDK exposes these
807
933
  * so a dApp can re-verify the rich response object returned by the wallet
808
- * without round-tripping back through the relay. They never touch a secret
809
- * 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.
810
939
  */
811
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;
812
944
  interface VerifyMessageParams {
813
945
  signature: Uint8Array | string;
814
946
  publicKey: Uint8Array | string;
815
947
  /** 0x-hex bytes that were originally signed. */
816
948
  messageBytes: Uint8Array | string;
817
949
  }
818
- 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;
819
956
  interface VerifyTypedDataParams {
820
957
  signature: Uint8Array | string;
821
958
  publicKey: Uint8Array | string;
822
959
  payload: TypedDataPayload;
823
960
  }
824
- 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;
825
993
 
826
994
  /**
827
995
  * Local hex/bytes utilities for the signing module.
@@ -833,38 +1001,40 @@ declare function bytesToHex(bytes: Uint8Array): string;
833
1001
  declare function concatBytes(...parts: Uint8Array[]): Uint8Array;
834
1002
 
835
1003
  /**
836
- * v2 QR URI codec (PQP2).
1004
+ * v3 QR URI codec (PQP3).
837
1005
  *
838
- * Format: qrlconnect://?q=<base45(PQP2 || cid || fp)>[&r=<relayUrl>]
1006
+ * Format: qrlconnect://?q=<base45(PQP3 || cid || fp || cap)>[&r=<relayUrl>]
839
1007
  *
840
- * PQP2 = "PQP2" magic (4 B)
841
- * cid = 16 bytes (UUIDv4 raw) (16 B)
842
- * fp = SHA-256("pq-fp/v2" || cid || pk) (full 32B) (32 B)
843
- * 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)
844
1013
  *
845
- * 52 bytes → ~78 alphanumeric chars after base45 → URI under 150 chars →
846
- * 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.
847
1016
  *
848
1017
  * Security: the public key is NOT carried in the QR. It's uploaded by the
849
1018
  * dApp to the relay at channel creation, the relay binds it to the cid, and
850
- * the wallet fetches it via the join_channel ack. The 32-byte fp acts as
851
- * the out-of-band commitment: the wallet rejects the PK served by the relay
852
- * unless SHA-256("pq-fp/v2" || cid || pk) matches the fp from the QR.
853
- * Full-width SHA-256 (2^128 collision resistance) rules out brute-force
854
- * 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.
855
1023
  *
856
- * Legacy v2.0 URIs (magic=PQP1, 1208-byte blob with embedded PK) are
857
- * rejected with a clear error by parseConnectionURI - this is a hard
858
- * 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.
859
1027
  */
860
1028
  declare const CID_LEN = 16;
861
1029
  declare const FP_LEN = 32;
1030
+ declare const CAP_LEN = 32;
862
1031
  declare const BLOB_LEN: number;
1032
+ declare const MAX_CONNECTION_URI_LENGTH = 4096;
863
1033
  /**
864
- * Compute the full 32-byte fingerprint binding (label || cid || pk).
1034
+ * Compute the full 32-byte fingerprint binding (label || cid || pk || cap).
865
1035
  * Exported so the wallet side can re-derive and verify.
866
1036
  */
867
- declare function computeFingerprint(cid: Uint8Array, pk: Uint8Array): Promise<Uint8Array>;
1037
+ declare function computeFingerprint(cid: Uint8Array, pk: Uint8Array, capability: Uint8Array): Promise<Uint8Array>;
868
1038
  /**
869
1039
  * Constant-time comparison of two byte arrays. Both must be the same length.
870
1040
  * The verifier MUST use this rather than `===` or `indexOf` so timing signals
@@ -872,18 +1042,19 @@ declare function computeFingerprint(cid: Uint8Array, pk: Uint8Array): Promise<Ui
872
1042
  */
873
1043
  declare function fingerprintEquals(a: Uint8Array, b: Uint8Array): boolean;
874
1044
  /**
875
- * Encode (cid, pk) as a qrlconnect:// URI. Note the PK is not stored in the
876
- * URI - we compute its fingerprint and embed only that. The caller must
877
- * 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.
878
1048
  *
879
1049
  * The optional `relayUrl` rides as a sibling query param (not inside the
880
1050
  * fp-bound blob) so a tampered relay can cause the pairing to fail but
881
1051
  * cannot substitute the PK - the fp still pins that.
882
1052
  */
883
- 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>;
884
1054
  interface ParsedURI {
885
1055
  cid: Uint8Array;
886
1056
  fp: Uint8Array;
1057
+ capability: Uint8Array;
887
1058
  relayUrl?: string | undefined;
888
1059
  }
889
1060
  declare function parseConnectionURI(uri: string): Promise<ParsedURI>;
@@ -923,20 +1094,28 @@ declare function getAppStoreUrl(): string;
923
1094
  declare function attemptWalletRedirect(uri: string, timeoutMs?: number): Promise<boolean>;
924
1095
 
925
1096
  declare const DEFAULT_RELAY_URL = "https://qrlwallet.com";
926
- 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;
927
1102
  /**
928
- * RPC methods that require user approval in the wallet.
929
- *
930
- * v3.0.0 replaces the Ethereum-flavored signing surface with two
931
- * post-quantum-native methods: `qrl_signMessage` for opaque bytes and
932
- * `qrl_signTypedData` for structured payloads (EIP-712-shaped, but with
933
- * SHAKE256 + native Dilithium ctx). The legacy methods (`personal_sign`,
934
- * `qrl_sign`, `qrl_signTypedData_v3`, `qrl_signTypedData_v4`) are no
935
- * longer accepted; a dApp calling them via this SDK will get a
936
- * "method not supported" error before the relay round-trip.
937
- */
938
- declare const RESTRICTED_METHODS: Set<string>;
939
- /** RPC methods that can be auto-proxied without approval */
940
- 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;
941
1120
 
942
- 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 };