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