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