@qrlwallet/connect 0.1.0 → 3.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 +13 -32
- package/dist/index.d.mts +483 -113
- package/dist/index.d.ts +483 -113
- package/dist/index.js +1409 -444
- package/dist/index.mjs +1382 -442
- package/package.json +8 -6
package/dist/index.js
CHANGED
|
@@ -30,22 +30,47 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
BLOB_LEN: () => BLOB_LEN,
|
|
34
|
+
CID_LEN: () => CID_LEN,
|
|
33
35
|
ConnectionManager: () => ConnectionManager,
|
|
34
36
|
ConnectionStatus: () => ConnectionStatus,
|
|
35
37
|
DEFAULT_RELAY_URL: () => DEFAULT_RELAY_URL,
|
|
36
|
-
|
|
38
|
+
DIGEST_LEN: () => DIGEST_LEN,
|
|
39
|
+
FP_LEN: () => FP_LEN,
|
|
37
40
|
KeyExchange: () => KeyExchange,
|
|
38
41
|
KeyExchangeMessageType: () => KeyExchangeMessageType,
|
|
39
42
|
MessageType: () => MessageType,
|
|
43
|
+
PROTOCOL_VERSION: () => PROTOCOL_VERSION,
|
|
40
44
|
QRLConnect: () => QRLConnectProvider,
|
|
41
45
|
QRLConnectProvider: () => QRLConnectProvider,
|
|
46
|
+
QRL_CONNECT_PROVIDER_INFO: () => QRL_CONNECT_PROVIDER_INFO,
|
|
42
47
|
RESTRICTED_METHODS: () => RESTRICTED_METHODS,
|
|
48
|
+
SCHEME_TAG_MSG: () => SCHEME_TAG_MSG,
|
|
49
|
+
SCHEME_TAG_TYPED: () => SCHEME_TAG_TYPED,
|
|
50
|
+
SCHEME_VERSION_MSG: () => SCHEME_VERSION_MSG,
|
|
51
|
+
SCHEME_VERSION_TYPED: () => SCHEME_VERSION_TYPED,
|
|
43
52
|
SocketClient: () => SocketClient,
|
|
44
53
|
UNRESTRICTED_METHODS: () => UNRESTRICTED_METHODS,
|
|
54
|
+
bytesToHex: () => bytesToHex,
|
|
55
|
+
cidFromString: () => cidFromString,
|
|
56
|
+
cidRandom: () => cidRandom,
|
|
57
|
+
cidToString: () => cidToString,
|
|
58
|
+
computeFingerprint: () => computeFingerprint,
|
|
59
|
+
computeMessageDigest: () => computeMessageDigest,
|
|
60
|
+
computeTypedDataDigest: () => computeTypedDataDigest,
|
|
61
|
+
concatBytes: () => concatBytes,
|
|
62
|
+
encodeField: () => encodeField,
|
|
63
|
+
encodeType: () => encodeType,
|
|
64
|
+
fingerprintEquals: () => fingerprintEquals,
|
|
45
65
|
generateConnectionURI: () => generateConnectionURI,
|
|
46
66
|
getAppStoreUrl: () => getAppStoreUrl,
|
|
67
|
+
hashStruct: () => hashStruct,
|
|
68
|
+
hexToBytes: () => hexToBytes,
|
|
47
69
|
isMobileBrowser: () => isMobileBrowser,
|
|
48
|
-
parseConnectionURI: () => parseConnectionURI
|
|
70
|
+
parseConnectionURI: () => parseConnectionURI,
|
|
71
|
+
typeHash: () => typeHash,
|
|
72
|
+
verifyMessage: () => verifyMessage,
|
|
73
|
+
verifyTypedData: () => verifyTypedData
|
|
49
74
|
});
|
|
50
75
|
module.exports = __toCommonJS(index_exports);
|
|
51
76
|
|
|
@@ -56,94 +81,153 @@ var import_eventemitter34 = __toESM(require("eventemitter3"));
|
|
|
56
81
|
var import_eventemitter33 = __toESM(require("eventemitter3"));
|
|
57
82
|
var import_uuid = require("uuid");
|
|
58
83
|
|
|
59
|
-
// src/
|
|
60
|
-
var
|
|
84
|
+
// src/KeyExchange.ts
|
|
85
|
+
var import_eventemitter3 = __toESM(require("eventemitter3"));
|
|
61
86
|
|
|
62
|
-
// src/
|
|
63
|
-
var
|
|
64
|
-
|
|
65
|
-
|
|
87
|
+
// src/PQCrypto.ts
|
|
88
|
+
var import_ml_kem = require("@noble/post-quantum/ml-kem.js");
|
|
89
|
+
var textEncoder = new TextEncoder();
|
|
90
|
+
var LABEL = textEncoder.encode("pq-pair/v1");
|
|
91
|
+
var LABEL_AEAD_SUFFIX = textEncoder.encode(" aead");
|
|
92
|
+
var DIR_DAPP_TX = new Uint8Array([0, 0, 0, 1]);
|
|
93
|
+
var DIR_WALLET_TX = new Uint8Array([0, 0, 0, 2]);
|
|
94
|
+
var ML_KEM_768_CT_LEN = 1088;
|
|
95
|
+
var AEAD_KEY_LEN = 32;
|
|
96
|
+
function subtle() {
|
|
97
|
+
const c = globalThis.crypto;
|
|
98
|
+
if (!c || !c.subtle) {
|
|
99
|
+
throw new Error("PQCrypto: WebCrypto SubtleCrypto is not available in this environment");
|
|
100
|
+
}
|
|
101
|
+
return c.subtle;
|
|
66
102
|
}
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
console.log(`[QRLConnect:${tag}]`, ...args);
|
|
70
|
-
}
|
|
103
|
+
function bs(u) {
|
|
104
|
+
return u;
|
|
71
105
|
}
|
|
72
|
-
function
|
|
73
|
-
|
|
106
|
+
function kemKeygen() {
|
|
107
|
+
const { publicKey, secretKey } = import_ml_kem.ml_kem768.keygen();
|
|
108
|
+
return { pk: publicKey, sk: secretKey };
|
|
74
109
|
}
|
|
75
|
-
function
|
|
76
|
-
|
|
110
|
+
function kemEncaps(pk) {
|
|
111
|
+
const { cipherText, sharedSecret } = import_ml_kem.ml_kem768.encapsulate(pk);
|
|
112
|
+
return { ct: cipherText, ss: sharedSecret };
|
|
113
|
+
}
|
|
114
|
+
function kemDecaps(sk, ct) {
|
|
115
|
+
return import_ml_kem.ml_kem768.decapsulate(ct, sk);
|
|
116
|
+
}
|
|
117
|
+
async function transcriptHash(cid, pk, ct) {
|
|
118
|
+
const buf = concat(LABEL, cid, pk, ct);
|
|
119
|
+
return new Uint8Array(await subtle().digest("SHA-256", bs(buf)));
|
|
120
|
+
}
|
|
121
|
+
async function deriveAeadKey(ss, htx) {
|
|
122
|
+
const ikm = await subtle().importKey("raw", bs(ss), "HKDF", false, ["deriveKey"]);
|
|
123
|
+
const info = concat(LABEL, LABEL_AEAD_SUFFIX, htx);
|
|
124
|
+
return subtle().deriveKey(
|
|
125
|
+
{ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(32), info: bs(info) },
|
|
126
|
+
ikm,
|
|
127
|
+
{ name: "AES-GCM", length: 256 },
|
|
128
|
+
true,
|
|
129
|
+
["encrypt", "decrypt"]
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
async function importRawAeadKey(raw) {
|
|
133
|
+
if (raw.length !== AEAD_KEY_LEN) {
|
|
134
|
+
throw new Error(`PQCrypto: raw AEAD key must be ${AEAD_KEY_LEN} bytes`);
|
|
135
|
+
}
|
|
136
|
+
return subtle().importKey("raw", bs(raw), { name: "AES-GCM", length: 256 }, true, [
|
|
137
|
+
"encrypt",
|
|
138
|
+
"decrypt"
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
async function exportRawAeadKey(key) {
|
|
142
|
+
const buf = await subtle().exportKey("raw", key);
|
|
143
|
+
return new Uint8Array(buf);
|
|
144
|
+
}
|
|
145
|
+
function nonce(dir, seq) {
|
|
146
|
+
if (dir.length !== 4) {
|
|
147
|
+
throw new Error("PQCrypto: direction tag must be 4 bytes");
|
|
148
|
+
}
|
|
149
|
+
const n = new Uint8Array(12);
|
|
150
|
+
n.set(dir, 0);
|
|
151
|
+
new DataView(n.buffer, n.byteOffset, n.byteLength).setBigUint64(4, BigInt(seq), true);
|
|
152
|
+
return n;
|
|
153
|
+
}
|
|
154
|
+
function aad(htx, seq) {
|
|
155
|
+
const out = new Uint8Array(htx.length + 8);
|
|
156
|
+
out.set(htx, 0);
|
|
157
|
+
new DataView(out.buffer, out.byteOffset, out.byteLength).setBigUint64(
|
|
158
|
+
htx.length,
|
|
159
|
+
BigInt(seq),
|
|
160
|
+
true
|
|
161
|
+
);
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
async function seal(key, dir, seq, htx, pt) {
|
|
165
|
+
const ct = await subtle().encrypt(
|
|
166
|
+
{ name: "AES-GCM", iv: bs(nonce(dir, seq)), additionalData: bs(aad(htx, seq)) },
|
|
167
|
+
key,
|
|
168
|
+
bs(pt)
|
|
169
|
+
);
|
|
170
|
+
return new Uint8Array(ct);
|
|
171
|
+
}
|
|
172
|
+
async function open(key, dir, seq, htx, ct) {
|
|
173
|
+
const pt = await subtle().decrypt(
|
|
174
|
+
{ name: "AES-GCM", iv: bs(nonce(dir, seq)), additionalData: bs(aad(htx, seq)) },
|
|
175
|
+
key,
|
|
176
|
+
bs(ct)
|
|
177
|
+
);
|
|
178
|
+
return new Uint8Array(pt);
|
|
179
|
+
}
|
|
180
|
+
function zeroize(b) {
|
|
181
|
+
b.fill(0);
|
|
182
|
+
}
|
|
183
|
+
function concat(...parts) {
|
|
184
|
+
let total = 0;
|
|
185
|
+
for (const p of parts) total += p.length;
|
|
186
|
+
const out = new Uint8Array(total);
|
|
187
|
+
let offset = 0;
|
|
188
|
+
for (const p of parts) {
|
|
189
|
+
out.set(p, offset);
|
|
190
|
+
offset += p.length;
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
77
193
|
}
|
|
78
|
-
|
|
79
|
-
// src/ECIESClient.ts
|
|
80
194
|
function toBase64(bytes) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
195
|
+
const CHUNK = 32768;
|
|
196
|
+
let bin = "";
|
|
197
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
198
|
+
const slice = bytes.subarray(i, i + CHUNK);
|
|
199
|
+
bin += String.fromCharCode.apply(null, slice);
|
|
84
200
|
}
|
|
85
|
-
return btoa(
|
|
201
|
+
return btoa(bin);
|
|
86
202
|
}
|
|
87
|
-
function fromBase64(
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
for (let i = 0; i <
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
203
|
+
function fromBase64(b64) {
|
|
204
|
+
const bin = atob(b64);
|
|
205
|
+
const out = new Uint8Array(bin.length);
|
|
206
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
function constantTimeEquals(a, b) {
|
|
210
|
+
if (a.length !== b.length) return false;
|
|
211
|
+
let d = 0;
|
|
212
|
+
for (let i = 0; i < a.length; i++) d |= a[i] ^ b[i];
|
|
213
|
+
return d === 0;
|
|
94
214
|
}
|
|
95
|
-
var ECIESClient = class {
|
|
96
|
-
constructor(existingPrivateKey) {
|
|
97
|
-
if (existingPrivateKey) {
|
|
98
|
-
this.privateKey = import_eciesjs.PrivateKey.fromHex(existingPrivateKey);
|
|
99
|
-
log("ECIES", "Restored from existing private key");
|
|
100
|
-
} else {
|
|
101
|
-
this.privateKey = new import_eciesjs.PrivateKey();
|
|
102
|
-
log("ECIES", "Generated new ECIES keypair");
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
/** Get public key as hex string */
|
|
106
|
-
getPublicKey() {
|
|
107
|
-
return this.privateKey.publicKey.toHex();
|
|
108
|
-
}
|
|
109
|
-
/** Get private key as hex string (for session persistence) */
|
|
110
|
-
getPrivateKeyHex() {
|
|
111
|
-
return this.privateKey.toHex();
|
|
112
|
-
}
|
|
113
|
-
/** Encrypt a string message for the given public key. Returns base64. */
|
|
114
|
-
encrypt(data, otherPublicKey) {
|
|
115
|
-
const encoded = new TextEncoder().encode(data);
|
|
116
|
-
const encrypted = (0, import_eciesjs.encrypt)(otherPublicKey, encoded);
|
|
117
|
-
return toBase64(encrypted);
|
|
118
|
-
}
|
|
119
|
-
/** Decrypt a base64-encoded message using our private key. */
|
|
120
|
-
decrypt(encryptedBase64) {
|
|
121
|
-
const encryptedBytes = fromBase64(encryptedBase64);
|
|
122
|
-
const decrypted = (0, import_eciesjs.decrypt)(this.privateKey.toHex(), encryptedBytes);
|
|
123
|
-
return new TextDecoder().decode(decrypted);
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// src/KeyExchange.ts
|
|
128
|
-
var import_eventemitter3 = __toESM(require("eventemitter3"));
|
|
129
215
|
|
|
130
216
|
// src/config.ts
|
|
131
217
|
var DEFAULT_RELAY_URL = "https://qrlwallet.com";
|
|
132
218
|
var RELAY_PATH = "/relay";
|
|
133
|
-
var PROTOCOL_VERSION =
|
|
219
|
+
var PROTOCOL_VERSION = 2;
|
|
134
220
|
var SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
135
221
|
var STORAGE_KEY_PREFIX = "@qrlwallet/connect";
|
|
136
222
|
var REQUEST_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
137
223
|
var WALLET_UNRESPONSIVE_MS = 30 * 1e3;
|
|
224
|
+
var RECONNECT_WALLET_PROBE_MS = 12 * 1e3;
|
|
138
225
|
var RESTRICTED_METHODS = /* @__PURE__ */ new Set([
|
|
139
226
|
"qrl_requestAccounts",
|
|
140
227
|
"qrl_sendTransaction",
|
|
141
228
|
"qrl_signTransaction",
|
|
142
|
-
"
|
|
143
|
-
"personal_sign",
|
|
229
|
+
"qrl_signMessage",
|
|
144
230
|
"qrl_signTypedData",
|
|
145
|
-
"qrl_signTypedData_v3",
|
|
146
|
-
"qrl_signTypedData_v4",
|
|
147
231
|
"wallet_addQrlChain",
|
|
148
232
|
"wallet_switchQrlChain"
|
|
149
233
|
]);
|
|
@@ -216,154 +300,300 @@ var ConnectionStatus = /* @__PURE__ */ ((ConnectionStatus2) => {
|
|
|
216
300
|
return ConnectionStatus2;
|
|
217
301
|
})(ConnectionStatus || {});
|
|
218
302
|
|
|
303
|
+
// src/utils/logger.ts
|
|
304
|
+
var debugEnabled = false;
|
|
305
|
+
function setDebug(enabled) {
|
|
306
|
+
debugEnabled = enabled;
|
|
307
|
+
}
|
|
308
|
+
function log(tag, ...args) {
|
|
309
|
+
if (debugEnabled) {
|
|
310
|
+
console.log(`[QRLConnect:${tag}]`, ...args);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function warn(tag, ...args) {
|
|
314
|
+
console.warn(`[QRLConnect:${tag}]`, ...args);
|
|
315
|
+
}
|
|
316
|
+
function error(tag, ...args) {
|
|
317
|
+
console.error(`[QRLConnect:${tag}]`, ...args);
|
|
318
|
+
}
|
|
319
|
+
|
|
219
320
|
// src/KeyExchange.ts
|
|
321
|
+
var textEncoder2 = new TextEncoder();
|
|
322
|
+
var textDecoder = new TextDecoder();
|
|
323
|
+
var HELLO_WALLET = textEncoder2.encode("hello/wallet/v1");
|
|
324
|
+
var HELLO_DAPP = textEncoder2.encode("hello/dapp/v1");
|
|
220
325
|
var KeyExchange = class extends import_eventemitter3.default {
|
|
221
|
-
constructor(
|
|
326
|
+
constructor(isOriginator, restored) {
|
|
222
327
|
super();
|
|
223
|
-
this.
|
|
224
|
-
this.
|
|
328
|
+
this.keypair = null;
|
|
329
|
+
this.session = null;
|
|
330
|
+
this.step = "key_handshake_SYN" /* SYN */;
|
|
225
331
|
this.awaitingSynAck = false;
|
|
226
332
|
this.awaitingAck = false;
|
|
227
|
-
this.
|
|
228
|
-
this.ecies = ecies;
|
|
333
|
+
this.keysExchanged = false;
|
|
229
334
|
this.isOriginator = isOriginator;
|
|
230
|
-
if (
|
|
231
|
-
this.
|
|
335
|
+
if (restored) {
|
|
336
|
+
this.session = restored;
|
|
232
337
|
this.keysExchanged = true;
|
|
233
|
-
|
|
338
|
+
this.step = "key_handshake_ACK" /* ACK */;
|
|
339
|
+
log("KeyExchange", "Hydrated from persisted session");
|
|
234
340
|
}
|
|
235
341
|
}
|
|
236
|
-
/**
|
|
237
|
-
|
|
342
|
+
/**
|
|
343
|
+
* Originator (dApp) side: begin a fresh handshake by generating an
|
|
344
|
+
* ephemeral ML-KEM-768 keypair. Returns pk to embed in the QR URI.
|
|
345
|
+
*/
|
|
346
|
+
initiate() {
|
|
347
|
+
if (!this.isOriginator) {
|
|
348
|
+
throw new Error("KeyExchange: responder cannot initiate");
|
|
349
|
+
}
|
|
350
|
+
this.resetInternal(false);
|
|
351
|
+
this.keypair = kemKeygen();
|
|
238
352
|
this.step = "key_handshake_SYN" /* SYN */;
|
|
239
353
|
this.awaitingSynAck = true;
|
|
240
354
|
this.emit("step_change", this.step);
|
|
355
|
+
return this.keypair.pk;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Originator: handle a SYNACK received over the relay.
|
|
359
|
+
* Returns an ACK wire message to send, or null if the SYNACK was a
|
|
360
|
+
* duplicate (handshake already complete).
|
|
361
|
+
*/
|
|
362
|
+
async onSynAck(cid, msg) {
|
|
363
|
+
if (!this.isOriginator) {
|
|
364
|
+
warn("KeyExchange", "Responder received SYNACK \u2014 ignoring");
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
if (!this.awaitingSynAck || !this.keypair) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
this.awaitingSynAck = false;
|
|
371
|
+
const ct = fromBase64(msg.ct);
|
|
372
|
+
const c0 = fromBase64(msg.c0);
|
|
373
|
+
if (ct.length !== ML_KEM_768_CT_LEN) {
|
|
374
|
+
throw new Error(`KeyExchange: bad ct length ${ct.length}`);
|
|
375
|
+
}
|
|
376
|
+
const ss = kemDecaps(this.keypair.sk, ct);
|
|
377
|
+
const htx = await transcriptHash(cid, this.keypair.pk, ct);
|
|
378
|
+
const key = await deriveAeadKey(ss, htx);
|
|
379
|
+
let hello;
|
|
380
|
+
try {
|
|
381
|
+
hello = await open(key, DIR_WALLET_TX, 0, htx, c0);
|
|
382
|
+
} catch {
|
|
383
|
+
throw new Error(
|
|
384
|
+
"KeyExchange: wallet hello AEAD tag failed (tampered SYNACK, wrong QR, or protocol mismatch)"
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
if (!constantTimeEquals(hello, HELLO_WALLET)) {
|
|
388
|
+
throw new Error("KeyExchange: wallet hello mismatch");
|
|
389
|
+
}
|
|
390
|
+
zeroize(ss);
|
|
391
|
+
zeroize(this.keypair.sk);
|
|
392
|
+
this.keypair = null;
|
|
393
|
+
const c1 = await seal(key, DIR_DAPP_TX, 0, htx, HELLO_DAPP);
|
|
394
|
+
this.session = {
|
|
395
|
+
cid,
|
|
396
|
+
key,
|
|
397
|
+
htx,
|
|
398
|
+
sendDir: DIR_DAPP_TX,
|
|
399
|
+
recvDir: DIR_WALLET_TX,
|
|
400
|
+
sendSeq: 1,
|
|
401
|
+
recvSeq: 1
|
|
402
|
+
};
|
|
403
|
+
this.keysExchanged = true;
|
|
404
|
+
this.step = "key_handshake_ACK" /* ACK */;
|
|
405
|
+
this.emit("keys_exchanged");
|
|
406
|
+
this.emit("step_change", this.step);
|
|
241
407
|
return {
|
|
242
|
-
type: "
|
|
243
|
-
|
|
408
|
+
type: "key_handshake_ACK" /* ACK */,
|
|
409
|
+
c1: toBase64(c1),
|
|
244
410
|
v: PROTOCOL_VERSION
|
|
245
411
|
};
|
|
246
412
|
}
|
|
247
413
|
/**
|
|
248
|
-
*
|
|
249
|
-
* Returns
|
|
414
|
+
* Responder (wallet) side: begin handshake from a scanned QR.
|
|
415
|
+
* Returns the SYNACK wire message to emit on the relay.
|
|
250
416
|
*/
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
case "key_handshake_SYN" /* SYN */: {
|
|
255
|
-
if (this.isOriginator) {
|
|
256
|
-
warn("KeyExchange", "Originator received SYN - ignoring");
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
if (message.pubkey) {
|
|
260
|
-
this.otherPublicKey = message.pubkey;
|
|
261
|
-
}
|
|
262
|
-
this.step = "key_handshake_SYNACK" /* SYNACK */;
|
|
263
|
-
this.awaitingAck = true;
|
|
264
|
-
this.emit("step_change", this.step);
|
|
265
|
-
return {
|
|
266
|
-
type: "key_handshake_SYNACK" /* SYNACK */,
|
|
267
|
-
pubkey: this.ecies.getPublicKey(),
|
|
268
|
-
v: PROTOCOL_VERSION
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
case "key_handshake_SYNACK" /* SYNACK */: {
|
|
272
|
-
if (!this.isOriginator) {
|
|
273
|
-
warn("KeyExchange", "Non-originator received SYNACK - ignoring");
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
if (message.pubkey) {
|
|
277
|
-
this.otherPublicKey = message.pubkey;
|
|
278
|
-
}
|
|
279
|
-
if (!this.awaitingSynAck) {
|
|
280
|
-
return {
|
|
281
|
-
type: "key_handshake_ACK" /* ACK */,
|
|
282
|
-
v: PROTOCOL_VERSION
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
this.awaitingSynAck = false;
|
|
286
|
-
this.step = "key_handshake_ACK" /* ACK */;
|
|
287
|
-
if (!this.keysExchanged) {
|
|
288
|
-
this.keysExchanged = true;
|
|
289
|
-
this.emit("keys_exchanged");
|
|
290
|
-
}
|
|
291
|
-
this.emit("step_change", this.step);
|
|
292
|
-
return {
|
|
293
|
-
type: "key_handshake_ACK" /* ACK */,
|
|
294
|
-
v: PROTOCOL_VERSION
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
case "key_handshake_ACK" /* ACK */: {
|
|
298
|
-
if (this.isOriginator) {
|
|
299
|
-
warn("KeyExchange", "Originator received ACK - ignoring");
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
if (!this.awaitingAck) {
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
this.awaitingAck = false;
|
|
306
|
-
if (!this.keysExchanged) {
|
|
307
|
-
this.keysExchanged = true;
|
|
308
|
-
this.emit("keys_exchanged");
|
|
309
|
-
}
|
|
310
|
-
log("KeyExchange", "Key exchange complete (wallet side)");
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
default:
|
|
314
|
-
warn("KeyExchange", `Unknown key exchange type: ${message.type}`);
|
|
315
|
-
return null;
|
|
417
|
+
async receiveQR(cid, pk) {
|
|
418
|
+
if (this.isOriginator) {
|
|
419
|
+
throw new Error("KeyExchange: originator cannot consume a QR");
|
|
316
420
|
}
|
|
421
|
+
this.resetInternal(false);
|
|
422
|
+
const { ct, ss } = kemEncaps(pk);
|
|
423
|
+
const htx = await transcriptHash(cid, pk, ct);
|
|
424
|
+
const key = await deriveAeadKey(ss, htx);
|
|
425
|
+
const c0 = await seal(key, DIR_WALLET_TX, 0, htx, HELLO_WALLET);
|
|
426
|
+
zeroize(ss);
|
|
427
|
+
this.session = {
|
|
428
|
+
cid,
|
|
429
|
+
key,
|
|
430
|
+
htx,
|
|
431
|
+
sendDir: DIR_WALLET_TX,
|
|
432
|
+
recvDir: DIR_DAPP_TX,
|
|
433
|
+
sendSeq: 1,
|
|
434
|
+
recvSeq: 1
|
|
435
|
+
};
|
|
436
|
+
this.awaitingAck = true;
|
|
437
|
+
this.step = "key_handshake_SYNACK" /* SYNACK */;
|
|
438
|
+
this.emit("step_change", this.step);
|
|
439
|
+
return {
|
|
440
|
+
type: "key_handshake_SYNACK" /* SYNACK */,
|
|
441
|
+
ct: toBase64(ct),
|
|
442
|
+
c0: toBase64(c0),
|
|
443
|
+
v: PROTOCOL_VERSION
|
|
444
|
+
};
|
|
317
445
|
}
|
|
318
|
-
/**
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
446
|
+
/**
|
|
447
|
+
* Responder: verify an incoming ACK and finalize the session.
|
|
448
|
+
* Duplicates are silently ignored (handshake is idempotent).
|
|
449
|
+
*/
|
|
450
|
+
async onAck(msg) {
|
|
451
|
+
if (this.isOriginator) {
|
|
452
|
+
warn("KeyExchange", "Originator received ACK \u2014 ignoring");
|
|
453
|
+
return;
|
|
322
454
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
455
|
+
if (!this.awaitingAck) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (!this.session) {
|
|
459
|
+
throw new Error("KeyExchange: onAck called without a session");
|
|
460
|
+
}
|
|
461
|
+
this.awaitingAck = false;
|
|
462
|
+
const c1 = fromBase64(msg.c1);
|
|
463
|
+
let hello;
|
|
464
|
+
try {
|
|
465
|
+
hello = await open(this.session.key, DIR_DAPP_TX, 0, this.session.htx, c1);
|
|
466
|
+
} catch {
|
|
467
|
+
throw new Error("KeyExchange: dApp hello AEAD tag failed");
|
|
468
|
+
}
|
|
469
|
+
if (!constantTimeEquals(hello, HELLO_DAPP)) {
|
|
470
|
+
throw new Error("KeyExchange: dApp hello mismatch");
|
|
471
|
+
}
|
|
472
|
+
this.keysExchanged = true;
|
|
473
|
+
this.step = "key_handshake_ACK" /* ACK */;
|
|
474
|
+
this.emit("keys_exchanged");
|
|
475
|
+
}
|
|
476
|
+
/** Encrypt a string for the counterparty. Returns base64. */
|
|
477
|
+
async encryptMessage(data) {
|
|
478
|
+
if (!this.session) {
|
|
479
|
+
throw new Error("KeyExchange: cannot encrypt \u2014 session not established");
|
|
480
|
+
}
|
|
481
|
+
const pt = textEncoder2.encode(data);
|
|
482
|
+
const ct = await seal(
|
|
483
|
+
this.session.key,
|
|
484
|
+
this.session.sendDir,
|
|
485
|
+
this.session.sendSeq,
|
|
486
|
+
this.session.htx,
|
|
487
|
+
pt
|
|
488
|
+
);
|
|
489
|
+
this.session.sendSeq++;
|
|
490
|
+
return toBase64(ct);
|
|
491
|
+
}
|
|
492
|
+
/** Decrypt a base64 ciphertext from the counterparty. */
|
|
493
|
+
async decryptMessage(b64) {
|
|
494
|
+
if (!this.session) {
|
|
495
|
+
throw new Error("KeyExchange: cannot decrypt \u2014 session not established");
|
|
496
|
+
}
|
|
497
|
+
const ct = fromBase64(b64);
|
|
498
|
+
const pt = await open(
|
|
499
|
+
this.session.key,
|
|
500
|
+
this.session.recvDir,
|
|
501
|
+
this.session.recvSeq,
|
|
502
|
+
this.session.htx,
|
|
503
|
+
ct
|
|
504
|
+
);
|
|
505
|
+
this.session.recvSeq++;
|
|
506
|
+
return textDecoder.decode(pt);
|
|
507
|
+
}
|
|
508
|
+
/** Reset all state for a fresh handshake. Zeroes any live secret-key buffer. */
|
|
330
509
|
reset() {
|
|
331
|
-
this.
|
|
510
|
+
this.resetInternal(true);
|
|
511
|
+
}
|
|
512
|
+
resetInternal(emit) {
|
|
513
|
+
if (this.keypair) {
|
|
514
|
+
zeroize(this.keypair.sk);
|
|
515
|
+
this.keypair = null;
|
|
516
|
+
}
|
|
517
|
+
this.session = null;
|
|
332
518
|
this.keysExchanged = false;
|
|
333
519
|
this.awaitingSynAck = false;
|
|
334
520
|
this.awaitingAck = false;
|
|
335
521
|
this.step = "key_handshake_SYN" /* SYN */;
|
|
336
|
-
|
|
522
|
+
if (emit) this.emit("step_change", this.step);
|
|
337
523
|
}
|
|
338
524
|
areKeysExchanged() {
|
|
339
525
|
return this.keysExchanged;
|
|
340
526
|
}
|
|
341
|
-
|
|
342
|
-
return this.
|
|
527
|
+
getSession() {
|
|
528
|
+
return this.session;
|
|
343
529
|
}
|
|
344
530
|
getCurrentStep() {
|
|
345
531
|
return this.step;
|
|
346
532
|
}
|
|
533
|
+
/** Export the current session as a JSON-safe persisted record. */
|
|
534
|
+
async exportPersisted() {
|
|
535
|
+
if (!this.session) return null;
|
|
536
|
+
return {
|
|
537
|
+
cid: toBase64(this.session.cid),
|
|
538
|
+
kAeadRaw: toBase64(await exportRawAeadKey(this.session.key)),
|
|
539
|
+
htx: toBase64(this.session.htx),
|
|
540
|
+
sendDir: toBase64(this.session.sendDir),
|
|
541
|
+
recvDir: toBase64(this.session.recvDir),
|
|
542
|
+
sendSeq: this.session.sendSeq,
|
|
543
|
+
recvSeq: this.session.recvSeq
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/** Rehydrate a Session from its persisted form. */
|
|
547
|
+
static async sessionFromPersisted(p) {
|
|
548
|
+
const key = await importRawAeadKey(fromBase64(p.kAeadRaw));
|
|
549
|
+
return {
|
|
550
|
+
cid: fromBase64(p.cid),
|
|
551
|
+
key,
|
|
552
|
+
htx: fromBase64(p.htx),
|
|
553
|
+
sendDir: fromBase64(p.sendDir),
|
|
554
|
+
recvDir: fromBase64(p.recvDir),
|
|
555
|
+
sendSeq: p.sendSeq,
|
|
556
|
+
recvSeq: p.recvSeq
|
|
557
|
+
};
|
|
558
|
+
}
|
|
347
559
|
};
|
|
348
560
|
|
|
349
561
|
// src/SocketClient.ts
|
|
350
562
|
var import_socket = require("socket.io-client");
|
|
351
563
|
var import_eventemitter32 = __toESM(require("eventemitter3"));
|
|
564
|
+
var PENDING_JOIN_TIMEOUT_MS = 2e4;
|
|
352
565
|
var SocketClient = class extends import_eventemitter32.default {
|
|
353
566
|
constructor(relayUrl, clientType) {
|
|
354
567
|
super();
|
|
355
568
|
this.socket = null;
|
|
356
569
|
this.channelId = null;
|
|
570
|
+
// The dApp's own PK (base64) that must be uploaded with every join
|
|
571
|
+
// attempt so the relay can bind it to the channel. Wallet-side leaves
|
|
572
|
+
// this undefined.
|
|
573
|
+
this.publicKeyBase64 = null;
|
|
357
574
|
this.seq = 0;
|
|
358
575
|
this.pendingJoin = null;
|
|
359
576
|
this.relayUrl = relayUrl;
|
|
360
577
|
this.clientType = clientType;
|
|
361
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* dApp-only: stash the PK so it gets uploaded on every join_channel
|
|
581
|
+
* attempt (including auto-rejoins after reconnect).
|
|
582
|
+
*/
|
|
583
|
+
setPublicKey(publicKeyBase64) {
|
|
584
|
+
if (this.clientType !== "dapp") {
|
|
585
|
+
throw new Error("SocketClient.setPublicKey is only valid for dApp clients");
|
|
586
|
+
}
|
|
587
|
+
this.publicKeyBase64 = publicKeyBase64;
|
|
588
|
+
}
|
|
362
589
|
/**
|
|
363
590
|
* Connect to the relay server.
|
|
364
591
|
*/
|
|
365
592
|
connect() {
|
|
366
|
-
if (this.socket
|
|
593
|
+
if (this.socket) {
|
|
594
|
+
if (!this.socket.connected) this.socket.connect();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
367
597
|
log("Socket", `Connecting to ${this.relayUrl}`);
|
|
368
598
|
this.socket = (0, import_socket.io)(this.relayUrl, {
|
|
369
599
|
path: RELAY_PATH,
|
|
@@ -380,17 +610,11 @@ var SocketClient = class extends import_eventemitter32.default {
|
|
|
380
610
|
if (this.channelId) {
|
|
381
611
|
const reconnectChannelId = this.channelId;
|
|
382
612
|
this.joinChannelNow(reconnectChannelId).then((result) => {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
this.pendingJoin = null;
|
|
386
|
-
}
|
|
387
|
-
this.emit("reconnected");
|
|
613
|
+
this.settlePendingJoin(reconnectChannelId, { result });
|
|
614
|
+
this.emit("reconnected", result);
|
|
388
615
|
}).catch((err) => {
|
|
389
616
|
const reconnectErr = err instanceof Error ? err : new Error(String(err));
|
|
390
|
-
|
|
391
|
-
this.pendingJoin.reject(reconnectErr);
|
|
392
|
-
this.pendingJoin = null;
|
|
393
|
-
}
|
|
617
|
+
this.settlePendingJoin(reconnectChannelId, { error: reconnectErr });
|
|
394
618
|
error("Socket", `Rejoin failed: ${reconnectErr.message}`);
|
|
395
619
|
this.emit("error", reconnectErr);
|
|
396
620
|
});
|
|
@@ -414,7 +638,24 @@ var SocketClient = class extends import_eventemitter32.default {
|
|
|
414
638
|
});
|
|
415
639
|
}
|
|
416
640
|
/**
|
|
417
|
-
*
|
|
641
|
+
* Resolve or reject the currently-pending `joinChannel` promise, if it
|
|
642
|
+
* matches the given channelId. Clears the watchdog timer on settle so
|
|
643
|
+
* it doesn't fire after the fact.
|
|
644
|
+
*/
|
|
645
|
+
settlePendingJoin(channelId, outcome) {
|
|
646
|
+
if (!this.pendingJoin || this.pendingJoin.channelId !== channelId) return;
|
|
647
|
+
clearTimeout(this.pendingJoin.watchdog);
|
|
648
|
+
const pending = this.pendingJoin;
|
|
649
|
+
this.pendingJoin = null;
|
|
650
|
+
if ("result" in outcome) {
|
|
651
|
+
pending.resolve(outcome.result);
|
|
652
|
+
} else {
|
|
653
|
+
pending.reject(outcome.error);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Join a relay channel. Resolves with any buffered messages plus the
|
|
658
|
+
* dApp's public key bound to the channel (for wallet joins).
|
|
418
659
|
*/
|
|
419
660
|
async joinChannel(channelId) {
|
|
420
661
|
if (!this.socket) {
|
|
@@ -428,6 +669,7 @@ var SocketClient = class extends import_eventemitter32.default {
|
|
|
428
669
|
if (this.pendingJoin.channelId === channelId) {
|
|
429
670
|
return this.pendingJoin.promise;
|
|
430
671
|
}
|
|
672
|
+
clearTimeout(this.pendingJoin.watchdog);
|
|
431
673
|
this.pendingJoin.reject(new Error("Join request superseded by a newer channel"));
|
|
432
674
|
this.pendingJoin = null;
|
|
433
675
|
}
|
|
@@ -437,11 +679,17 @@ var SocketClient = class extends import_eventemitter32.default {
|
|
|
437
679
|
resolvePending = resolve;
|
|
438
680
|
rejectPending = reject;
|
|
439
681
|
});
|
|
682
|
+
const watchdog = setTimeout(() => {
|
|
683
|
+
this.settlePendingJoin(channelId, {
|
|
684
|
+
error: new Error(`joinChannel timed out after ${PENDING_JOIN_TIMEOUT_MS}ms`)
|
|
685
|
+
});
|
|
686
|
+
}, PENDING_JOIN_TIMEOUT_MS);
|
|
440
687
|
this.pendingJoin = {
|
|
441
688
|
channelId,
|
|
442
689
|
promise,
|
|
443
690
|
resolve: resolvePending,
|
|
444
|
-
reject: rejectPending
|
|
691
|
+
reject: rejectPending,
|
|
692
|
+
watchdog
|
|
445
693
|
};
|
|
446
694
|
return promise;
|
|
447
695
|
}
|
|
@@ -451,13 +699,22 @@ var SocketClient = class extends import_eventemitter32.default {
|
|
|
451
699
|
reject(new Error("Socket not connected"));
|
|
452
700
|
return;
|
|
453
701
|
}
|
|
702
|
+
const payload = { channelId, clientType: this.clientType };
|
|
703
|
+
if (this.clientType === "dapp" && this.publicKeyBase64) {
|
|
704
|
+
payload.publicKey = this.publicKeyBase64;
|
|
705
|
+
}
|
|
454
706
|
this.socket.emit(
|
|
455
707
|
"join_channel",
|
|
456
|
-
|
|
708
|
+
payload,
|
|
457
709
|
(response) => {
|
|
458
710
|
if (response.success) {
|
|
459
711
|
log("Socket", `Joined channel ${channelId}`);
|
|
460
|
-
resolve({
|
|
712
|
+
resolve({
|
|
713
|
+
bufferedMessages: response.bufferedMessages || [],
|
|
714
|
+
channelPublicKey: response.channelPublicKey ?? null,
|
|
715
|
+
participants: response.participants || [],
|
|
716
|
+
terminated: response.terminated === true
|
|
717
|
+
});
|
|
461
718
|
} else {
|
|
462
719
|
error("Socket", `Failed to join channel: ${response.error}`);
|
|
463
720
|
reject(new Error(response.error || "Failed to join channel"));
|
|
@@ -494,6 +751,7 @@ var SocketClient = class extends import_eventemitter32.default {
|
|
|
494
751
|
*/
|
|
495
752
|
leaveChannel() {
|
|
496
753
|
if (this.pendingJoin) {
|
|
754
|
+
clearTimeout(this.pendingJoin.watchdog);
|
|
497
755
|
this.pendingJoin.reject(new Error("Channel left before join completed"));
|
|
498
756
|
this.pendingJoin = null;
|
|
499
757
|
}
|
|
@@ -507,6 +765,7 @@ var SocketClient = class extends import_eventemitter32.default {
|
|
|
507
765
|
*/
|
|
508
766
|
disconnect() {
|
|
509
767
|
if (this.pendingJoin) {
|
|
768
|
+
clearTimeout(this.pendingJoin.watchdog);
|
|
510
769
|
this.pendingJoin.reject(new Error("Socket disconnected before join completed"));
|
|
511
770
|
this.pendingJoin = null;
|
|
512
771
|
}
|
|
@@ -526,111 +785,267 @@ var SocketClient = class extends import_eventemitter32.default {
|
|
|
526
785
|
}
|
|
527
786
|
};
|
|
528
787
|
|
|
788
|
+
// src/utils/base45.ts
|
|
789
|
+
var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
|
|
790
|
+
var DECODE = (() => {
|
|
791
|
+
const table = new Int8Array(128).fill(-1);
|
|
792
|
+
for (let i = 0; i < ALPHABET.length; i++) {
|
|
793
|
+
table[ALPHABET.charCodeAt(i)] = i;
|
|
794
|
+
}
|
|
795
|
+
return table;
|
|
796
|
+
})();
|
|
797
|
+
function decodeChar(charCode) {
|
|
798
|
+
if (charCode < 0 || charCode >= 128) return -1;
|
|
799
|
+
return DECODE[charCode];
|
|
800
|
+
}
|
|
801
|
+
function base45Encode(bytes) {
|
|
802
|
+
const n = bytes.length;
|
|
803
|
+
let out = "";
|
|
804
|
+
let i = 0;
|
|
805
|
+
while (i + 2 <= n) {
|
|
806
|
+
const v = bytes[i] << 8 | bytes[i + 1];
|
|
807
|
+
const c2 = Math.floor(v / 2025);
|
|
808
|
+
const r = v - c2 * 2025;
|
|
809
|
+
const c1 = Math.floor(r / 45);
|
|
810
|
+
const c0 = r - c1 * 45;
|
|
811
|
+
out += ALPHABET[c0] + ALPHABET[c1] + ALPHABET[c2];
|
|
812
|
+
i += 2;
|
|
813
|
+
}
|
|
814
|
+
if (i < n) {
|
|
815
|
+
const v = bytes[i];
|
|
816
|
+
const c1 = Math.floor(v / 45);
|
|
817
|
+
const c0 = v - c1 * 45;
|
|
818
|
+
out += ALPHABET[c0] + ALPHABET[c1];
|
|
819
|
+
}
|
|
820
|
+
return out;
|
|
821
|
+
}
|
|
822
|
+
function base45Decode(s) {
|
|
823
|
+
const len = s.length;
|
|
824
|
+
if (len === 0) return new Uint8Array(0);
|
|
825
|
+
const full = Math.floor(len / 3);
|
|
826
|
+
const tail = len - full * 3;
|
|
827
|
+
if (tail !== 0 && tail !== 2) {
|
|
828
|
+
throw new Error("base45: invalid input length");
|
|
829
|
+
}
|
|
830
|
+
const outLen = full * 2 + (tail === 2 ? 1 : 0);
|
|
831
|
+
const out = new Uint8Array(outLen);
|
|
832
|
+
for (let g = 0; g < full; g++) {
|
|
833
|
+
const d0 = decodeChar(s.charCodeAt(g * 3));
|
|
834
|
+
const d1 = decodeChar(s.charCodeAt(g * 3 + 1));
|
|
835
|
+
const d2 = decodeChar(s.charCodeAt(g * 3 + 2));
|
|
836
|
+
if (d0 < 0 || d1 < 0 || d2 < 0) {
|
|
837
|
+
throw new Error("base45: invalid character");
|
|
838
|
+
}
|
|
839
|
+
const v = d0 + d1 * 45 + d2 * 2025;
|
|
840
|
+
if (v > 65535) {
|
|
841
|
+
throw new Error("base45: decoded group exceeds 0xFFFF");
|
|
842
|
+
}
|
|
843
|
+
out[g * 2] = v >> 8 & 255;
|
|
844
|
+
out[g * 2 + 1] = v & 255;
|
|
845
|
+
}
|
|
846
|
+
if (tail === 2) {
|
|
847
|
+
const d0 = decodeChar(s.charCodeAt(full * 3));
|
|
848
|
+
const d1 = decodeChar(s.charCodeAt(full * 3 + 1));
|
|
849
|
+
if (d0 < 0 || d1 < 0) {
|
|
850
|
+
throw new Error("base45: invalid character");
|
|
851
|
+
}
|
|
852
|
+
const v = d0 + d1 * 45;
|
|
853
|
+
if (v > 255) {
|
|
854
|
+
throw new Error("base45: decoded byte exceeds 0xFF");
|
|
855
|
+
}
|
|
856
|
+
out[outLen - 1] = v;
|
|
857
|
+
}
|
|
858
|
+
return out;
|
|
859
|
+
}
|
|
860
|
+
|
|
529
861
|
// src/utils/qrUri.ts
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
|
|
862
|
+
var MAGIC = new Uint8Array([80, 81, 80, 50]);
|
|
863
|
+
var FP_LABEL = new TextEncoder().encode("pq-fp/v2");
|
|
864
|
+
var CID_LEN = 16;
|
|
865
|
+
var FP_LEN = 32;
|
|
866
|
+
var BLOB_LEN = 4 + CID_LEN + FP_LEN;
|
|
867
|
+
function bs2(u) {
|
|
868
|
+
return u;
|
|
869
|
+
}
|
|
870
|
+
async function computeFingerprint(cid, pk) {
|
|
871
|
+
if (cid.length !== CID_LEN) {
|
|
872
|
+
throw new Error(`qrUri: cid must be ${CID_LEN} bytes`);
|
|
873
|
+
}
|
|
874
|
+
const c = globalThis.crypto;
|
|
875
|
+
if (!c || !c.subtle) {
|
|
876
|
+
throw new Error("qrUri: WebCrypto SubtleCrypto is not available");
|
|
877
|
+
}
|
|
878
|
+
const buf = new Uint8Array(FP_LABEL.length + cid.length + pk.length);
|
|
879
|
+
buf.set(FP_LABEL, 0);
|
|
880
|
+
buf.set(cid, FP_LABEL.length);
|
|
881
|
+
buf.set(pk, FP_LABEL.length + cid.length);
|
|
882
|
+
const digest = await c.subtle.digest("SHA-256", bs2(buf));
|
|
883
|
+
return new Uint8Array(digest);
|
|
884
|
+
}
|
|
885
|
+
function fingerprintEquals(a, b) {
|
|
886
|
+
if (a.length !== b.length) return false;
|
|
887
|
+
let diff = 0;
|
|
888
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
889
|
+
return diff === 0;
|
|
890
|
+
}
|
|
891
|
+
async function generateConnectionURI(cid, pk, relayUrl) {
|
|
892
|
+
if (cid.length !== CID_LEN) {
|
|
893
|
+
throw new Error(`qrUri: cid must be ${CID_LEN} bytes (got ${cid.length})`);
|
|
894
|
+
}
|
|
895
|
+
const fp = await computeFingerprint(cid, pk);
|
|
896
|
+
const blob = new Uint8Array(BLOB_LEN);
|
|
897
|
+
blob.set(MAGIC, 0);
|
|
898
|
+
blob.set(cid, 4);
|
|
899
|
+
blob.set(fp, 4 + CID_LEN);
|
|
900
|
+
const params = new URLSearchParams({ q: base45Encode(blob) });
|
|
901
|
+
if (relayUrl) params.set("r", relayUrl);
|
|
902
|
+
return `qrlconnect://?${params.toString()}`;
|
|
903
|
+
}
|
|
904
|
+
var PQP1_MAGIC = new Uint8Array([80, 81, 80, 49]);
|
|
905
|
+
function startsWith(buf, prefix) {
|
|
906
|
+
if (buf.length < prefix.length) return false;
|
|
907
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
908
|
+
if (buf[i] !== prefix[i]) return false;
|
|
909
|
+
}
|
|
910
|
+
return true;
|
|
543
911
|
}
|
|
544
|
-
function parseConnectionURI(uri) {
|
|
912
|
+
async function parseConnectionURI(uri) {
|
|
913
|
+
if (typeof uri !== "string" || uri.length === 0) {
|
|
914
|
+
throw new Error("qrUri: empty URI");
|
|
915
|
+
}
|
|
916
|
+
if (!/^qrlconnect:/i.test(uri)) {
|
|
917
|
+
throw new Error("qrUri: not a qrlconnect URI");
|
|
918
|
+
}
|
|
919
|
+
let params;
|
|
545
920
|
try {
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
const channelId = params.get("channelId");
|
|
549
|
-
const pubKey = params.get("pubKey");
|
|
550
|
-
const name = params.get("name");
|
|
551
|
-
const url = params.get("url");
|
|
552
|
-
const chainId = params.get("chainId");
|
|
553
|
-
const relayUrl = params.get("relay");
|
|
554
|
-
if (!channelId || !pubKey || !name || !url || !chainId || !relayUrl) {
|
|
555
|
-
return null;
|
|
556
|
-
}
|
|
557
|
-
return {
|
|
558
|
-
channelId,
|
|
559
|
-
pubKey,
|
|
560
|
-
name,
|
|
561
|
-
url,
|
|
562
|
-
icon: params.get("icon") || void 0,
|
|
563
|
-
chainId,
|
|
564
|
-
relayUrl
|
|
565
|
-
};
|
|
921
|
+
const swapped = new URL(uri.replace(/^qrlconnect:\/?\/?/i, "https://qrlconnect/"));
|
|
922
|
+
params = swapped.searchParams;
|
|
566
923
|
} catch {
|
|
567
|
-
|
|
924
|
+
throw new Error("qrUri: malformed URI");
|
|
925
|
+
}
|
|
926
|
+
if (params.has("channelId") || params.has("pubKey")) {
|
|
927
|
+
throw new Error(
|
|
928
|
+
"qrUri: legacy v1 URI detected \u2014 this wallet and this dApp must both run protocol v2"
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
const q = params.get("q");
|
|
932
|
+
if (!q) {
|
|
933
|
+
throw new Error("qrUri: missing q parameter");
|
|
934
|
+
}
|
|
935
|
+
let blob;
|
|
936
|
+
try {
|
|
937
|
+
blob = base45Decode(q);
|
|
938
|
+
} catch (err) {
|
|
939
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
940
|
+
throw new Error(`qrUri: base45 decode failed: ${msg}`);
|
|
941
|
+
}
|
|
942
|
+
if (blob.length !== BLOB_LEN) {
|
|
943
|
+
if (blob.length === 1208 && startsWith(blob, PQP1_MAGIC)) {
|
|
944
|
+
throw new Error("qrUri: legacy PQP1 URI detected \u2014 regenerate the QR with a v2.0+ dApp SDK");
|
|
945
|
+
}
|
|
946
|
+
throw new Error(`qrUri: expected ${BLOB_LEN}-byte blob, got ${blob.length}`);
|
|
947
|
+
}
|
|
948
|
+
if (!startsWith(blob, MAGIC)) {
|
|
949
|
+
throw new Error("qrUri: bad PQP2 magic");
|
|
950
|
+
}
|
|
951
|
+
const cid = blob.slice(4, 4 + CID_LEN);
|
|
952
|
+
const fp = blob.slice(4 + CID_LEN, 4 + CID_LEN + FP_LEN);
|
|
953
|
+
const relayUrl = params.get("r") || void 0;
|
|
954
|
+
return { cid, fp, relayUrl };
|
|
955
|
+
}
|
|
956
|
+
function cidToString(cid) {
|
|
957
|
+
if (cid.length !== CID_LEN) {
|
|
958
|
+
throw new Error(`cidToString: expected ${CID_LEN}-byte cid`);
|
|
959
|
+
}
|
|
960
|
+
let hex = "";
|
|
961
|
+
for (let i = 0; i < CID_LEN; i++) {
|
|
962
|
+
hex += cid[i].toString(16).padStart(2, "0");
|
|
963
|
+
}
|
|
964
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
965
|
+
}
|
|
966
|
+
function cidFromString(s) {
|
|
967
|
+
const hex = s.replace(/-/g, "");
|
|
968
|
+
if (!/^[0-9a-f]{32}$/i.test(hex)) {
|
|
969
|
+
throw new Error("cidFromString: not a 128-bit hex string");
|
|
970
|
+
}
|
|
971
|
+
const out = new Uint8Array(CID_LEN);
|
|
972
|
+
for (let i = 0; i < CID_LEN; i++) {
|
|
973
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
568
974
|
}
|
|
975
|
+
return out;
|
|
976
|
+
}
|
|
977
|
+
function cidRandom() {
|
|
978
|
+
const c = globalThis.crypto;
|
|
979
|
+
if (!c || typeof c.getRandomValues !== "function") {
|
|
980
|
+
throw new Error("qrUri: crypto.getRandomValues is not available");
|
|
981
|
+
}
|
|
982
|
+
return c.getRandomValues(new Uint8Array(CID_LEN));
|
|
569
983
|
}
|
|
570
984
|
|
|
571
985
|
// src/ConnectionManager.ts
|
|
572
986
|
var DAPP_PARTICIPANT_CONFLICT_ERROR_MSG = "dapp participant is already connected";
|
|
987
|
+
var TERMINATE_SEND_TIMEOUT_MS = 800;
|
|
573
988
|
var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.default {
|
|
574
989
|
constructor(options) {
|
|
575
990
|
super();
|
|
991
|
+
this.keyExchange = null;
|
|
576
992
|
this.status = "disconnected" /* DISCONNECTED */;
|
|
577
993
|
this.connectedAccounts = [];
|
|
578
994
|
this.unresponsiveTimer = null;
|
|
579
|
-
this.
|
|
995
|
+
this.reconnectProbeTimer = null;
|
|
580
996
|
this.failedReconnects = 0;
|
|
581
|
-
this.synSent = false;
|
|
582
|
-
this.synAttempts = 0;
|
|
583
997
|
this.walletPresent = false;
|
|
998
|
+
this.pendingRestore = null;
|
|
999
|
+
this.messageQueue = Promise.resolve();
|
|
584
1000
|
this.dappMetadata = options.dappMetadata;
|
|
585
1001
|
this.relayUrl = options.relayUrl || DEFAULT_RELAY_URL;
|
|
586
1002
|
this.chainId = options.chainId || "0x0";
|
|
587
1003
|
this.storageKey = options.storageKey || `${STORAGE_KEY_PREFIX}:session`;
|
|
588
|
-
const
|
|
589
|
-
if (
|
|
590
|
-
this.channelId =
|
|
591
|
-
this.
|
|
592
|
-
this.
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
session.otherPublicKey || void 0
|
|
597
|
-
);
|
|
598
|
-
this.connectedAccounts = session.connectedAccounts;
|
|
599
|
-
log("ConnectionManager", `Restored session for channel ${this.channelId}`);
|
|
1004
|
+
const stored = this.readStoredSession();
|
|
1005
|
+
if (stored) {
|
|
1006
|
+
this.channelId = stored.channelId;
|
|
1007
|
+
this.connectedAccounts = stored.connectedAccounts;
|
|
1008
|
+
this.chainId = stored.chainId;
|
|
1009
|
+
this.dappMetadata = stored.dappMetadata;
|
|
1010
|
+
this.pendingRestore = stored;
|
|
1011
|
+
log("ConnectionManager", `Found persisted session for channel ${this.channelId}`);
|
|
600
1012
|
} else {
|
|
601
1013
|
this.channelId = (0, import_uuid.v4)();
|
|
602
|
-
this.ecies = new ECIESClient();
|
|
603
|
-
this.keyExchange = new KeyExchange(this.ecies, true);
|
|
604
1014
|
}
|
|
605
1015
|
this.socketClient = new SocketClient(this.relayUrl, "dapp");
|
|
606
1016
|
this.setupSocketListeners();
|
|
607
|
-
this.setupKeyExchangeListeners();
|
|
608
1017
|
}
|
|
1018
|
+
// ── Setup ──────────────────────────────────────────────────
|
|
609
1019
|
setupSocketListeners() {
|
|
610
1020
|
this.socketClient.on("connected", () => {
|
|
611
1021
|
if (this.status === "reconnecting" /* RECONNECTING */) {
|
|
612
1022
|
this.failedReconnects = 0;
|
|
613
|
-
log("ConnectionManager", "Reconnected to relay");
|
|
614
1023
|
}
|
|
615
1024
|
});
|
|
616
|
-
this.socketClient.on("reconnected", () => {
|
|
617
|
-
if (
|
|
1025
|
+
this.socketClient.on("reconnected", (result) => {
|
|
1026
|
+
if (result?.terminated) {
|
|
1027
|
+
log("ConnectionManager", "Channel terminated, observed on auto-reconnect");
|
|
1028
|
+
this.handleSessionTerminated();
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (result) {
|
|
1032
|
+
this.walletPresent = result.participants.includes("wallet");
|
|
1033
|
+
}
|
|
1034
|
+
if (this.keyExchange?.areKeysExchanged()) {
|
|
618
1035
|
if (this.walletPresent) {
|
|
1036
|
+
this.clearReconnectProbe();
|
|
619
1037
|
this.setStatus("connected" /* CONNECTED */);
|
|
620
1038
|
this.failedReconnects = 0;
|
|
621
1039
|
} else {
|
|
622
1040
|
this.setStatus("waiting" /* WAITING */);
|
|
1041
|
+
this.armReconnectProbe();
|
|
623
1042
|
}
|
|
624
|
-
this.synAttempts = 0;
|
|
625
|
-
this.clearSynRetryTimer();
|
|
626
1043
|
} else {
|
|
627
|
-
this.
|
|
1044
|
+
this.setStatus("waiting" /* WAITING */);
|
|
628
1045
|
}
|
|
629
1046
|
});
|
|
630
|
-
this.socketClient.on("disconnected", (
|
|
1047
|
+
this.socketClient.on("disconnected", () => {
|
|
631
1048
|
this.walletPresent = false;
|
|
632
|
-
this.synSent = false;
|
|
633
|
-
this.clearSynRetryTimer();
|
|
634
1049
|
if (this.status === "connected" /* CONNECTED */) {
|
|
635
1050
|
this.setStatus("reconnecting" /* RECONNECTING */);
|
|
636
1051
|
this.failedReconnects++;
|
|
@@ -640,7 +1055,7 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
640
1055
|
}
|
|
641
1056
|
});
|
|
642
1057
|
this.socketClient.on("message", (data) => {
|
|
643
|
-
this.
|
|
1058
|
+
this.enqueueRelayMessage(data);
|
|
644
1059
|
});
|
|
645
1060
|
this.socketClient.on("error", (err) => {
|
|
646
1061
|
this.emit("error", err);
|
|
@@ -650,20 +1065,21 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
650
1065
|
this.walletPresent = true;
|
|
651
1066
|
log("ConnectionManager", "Wallet joined channel");
|
|
652
1067
|
this.clearUnresponsiveTimer();
|
|
653
|
-
|
|
1068
|
+
this.clearReconnectProbe();
|
|
1069
|
+
if (this.keyExchange?.areKeysExchanged()) {
|
|
654
1070
|
this.failedReconnects = 0;
|
|
655
1071
|
this.setStatus("connected" /* CONNECTED */);
|
|
656
|
-
} else if (!this.synSent) {
|
|
657
|
-
this.sendSYN();
|
|
658
1072
|
}
|
|
659
1073
|
}
|
|
1074
|
+
if (data.event === "close") {
|
|
1075
|
+
log("ConnectionManager", "Wallet closed the channel (explicit terminate)");
|
|
1076
|
+
this.handleSessionTerminated();
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
660
1079
|
if (data.event === "disconnect" || data.event === "leave") {
|
|
661
1080
|
if (data.clientType === "wallet" || !data.clientType) {
|
|
662
1081
|
this.walletPresent = false;
|
|
663
|
-
if (
|
|
664
|
-
this.synSent = false;
|
|
665
|
-
this.clearSynRetryTimer();
|
|
666
|
-
} else if (this.status === "connected" /* CONNECTED */ || this.status === "reconnecting" /* RECONNECTING */) {
|
|
1082
|
+
if (this.keyExchange?.areKeysExchanged() && (this.status === "connected" /* CONNECTED */ || this.status === "reconnecting" /* RECONNECTING */)) {
|
|
667
1083
|
this.setStatus("waiting" /* WAITING */);
|
|
668
1084
|
}
|
|
669
1085
|
log("ConnectionManager", "Wallet left channel");
|
|
@@ -672,15 +1088,15 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
672
1088
|
});
|
|
673
1089
|
}
|
|
674
1090
|
setupKeyExchangeListeners() {
|
|
1091
|
+
if (!this.keyExchange) return;
|
|
675
1092
|
this.keyExchange.on("keys_exchanged", () => {
|
|
676
1093
|
log("ConnectionManager", "Key exchange complete");
|
|
1094
|
+
this.clearReconnectProbe();
|
|
677
1095
|
this.setStatus("connected" /* CONNECTED */);
|
|
678
1096
|
this.walletPresent = true;
|
|
679
|
-
this.
|
|
680
|
-
this.
|
|
681
|
-
this.
|
|
682
|
-
this.saveSession();
|
|
683
|
-
this.sendEncrypted({
|
|
1097
|
+
this.failedReconnects = 0;
|
|
1098
|
+
void this.persistSession();
|
|
1099
|
+
void this.sendEncrypted({
|
|
684
1100
|
type: "originator_info" /* ORIGINATOR_INFO */,
|
|
685
1101
|
originatorInfo: {
|
|
686
1102
|
...this.dappMetadata,
|
|
@@ -689,16 +1105,25 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
689
1105
|
});
|
|
690
1106
|
});
|
|
691
1107
|
}
|
|
1108
|
+
// ── Public API ─────────────────────────────────────────────
|
|
692
1109
|
/**
|
|
693
|
-
* Generate
|
|
1110
|
+
* Generate a new v2 connection URI. Rotates channel id and keypair.
|
|
1111
|
+
* Returns a `qrlconnect://?q=…` URI safe for QR-rendering or deep-link.
|
|
694
1112
|
*/
|
|
695
1113
|
async getConnectionURI(retryOnConflict = true) {
|
|
696
1114
|
this.setStatus("connecting" /* CONNECTING */);
|
|
697
|
-
this.
|
|
1115
|
+
this.clearReconnectProbe();
|
|
1116
|
+
this.pendingRestore = null;
|
|
698
1117
|
this.walletPresent = false;
|
|
699
|
-
this.
|
|
700
|
-
|
|
701
|
-
|
|
1118
|
+
if (this.keyExchange) {
|
|
1119
|
+
this.keyExchange.reset();
|
|
1120
|
+
} else {
|
|
1121
|
+
this.keyExchange = new KeyExchange(true);
|
|
1122
|
+
this.setupKeyExchangeListeners();
|
|
1123
|
+
}
|
|
1124
|
+
const pk = this.keyExchange.initiate();
|
|
1125
|
+
this.channelId = (0, import_uuid.v4)();
|
|
1126
|
+
this.socketClient.setPublicKey(toBase64(pk));
|
|
702
1127
|
this.socketClient.connect();
|
|
703
1128
|
try {
|
|
704
1129
|
await this.socketClient.joinChannel(this.channelId);
|
|
@@ -708,48 +1133,73 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
708
1133
|
"ConnectionManager",
|
|
709
1134
|
"Channel already has an active dApp participant. Rotating to a fresh channel."
|
|
710
1135
|
);
|
|
711
|
-
this.
|
|
1136
|
+
this.channelId = (0, import_uuid.v4)();
|
|
712
1137
|
return this.getConnectionURI(false);
|
|
713
1138
|
}
|
|
714
1139
|
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
715
1140
|
throw err;
|
|
716
1141
|
}
|
|
717
1142
|
this.setStatus("waiting" /* WAITING */);
|
|
718
|
-
const uri = generateConnectionURI(
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
chainId: this.chainId,
|
|
725
|
-
relayUrl: this.relayUrl
|
|
726
|
-
});
|
|
727
|
-
log("ConnectionManager", `Generated connection URI for channel ${this.channelId}`);
|
|
1143
|
+
const uri = await generateConnectionURI(
|
|
1144
|
+
cidFromString(this.channelId),
|
|
1145
|
+
pk,
|
|
1146
|
+
this.relayUrl === DEFAULT_RELAY_URL ? void 0 : this.relayUrl
|
|
1147
|
+
);
|
|
1148
|
+
log("ConnectionManager", `Generated v2 connection URI for channel ${this.channelId}`);
|
|
728
1149
|
return uri;
|
|
729
1150
|
}
|
|
730
1151
|
/**
|
|
731
1152
|
* Reconnect to an existing session.
|
|
1153
|
+
* Returns false if there is nothing to restore.
|
|
732
1154
|
*/
|
|
733
1155
|
async reconnect() {
|
|
734
|
-
|
|
735
|
-
if (!session) return false;
|
|
1156
|
+
if (!this.pendingRestore) return false;
|
|
736
1157
|
this.setStatus("reconnecting" /* RECONNECTING */);
|
|
737
1158
|
this.walletPresent = false;
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1159
|
+
try {
|
|
1160
|
+
const session = await KeyExchange.sessionFromPersisted(this.pendingRestore.keyExchange);
|
|
1161
|
+
this.keyExchange = new KeyExchange(true, session);
|
|
1162
|
+
this.setupKeyExchangeListeners();
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
error("ConnectionManager", "Failed to hydrate persisted session:", err);
|
|
1165
|
+
this.clearSession();
|
|
1166
|
+
this.pendingRestore = null;
|
|
1167
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
return this.joinAndSettle();
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* (Re)open the socket, join the persisted channel, drain buffered relay
|
|
1174
|
+
* messages, and settle status. Shared by the cold-restore reconnect() and
|
|
1175
|
+
* the warm resume() path whose socket was torn down by the reconnect probe.
|
|
1176
|
+
* Assumes channelId and a hydrated keyExchange are already in place, and
|
|
1177
|
+
* that the caller has reset walletPresent + set RECONNECTING status.
|
|
1178
|
+
*/
|
|
1179
|
+
async joinAndSettle() {
|
|
741
1180
|
this.socketClient.connect();
|
|
742
1181
|
try {
|
|
743
|
-
const { bufferedMessages } = await this.socketClient.joinChannel(
|
|
1182
|
+
const { bufferedMessages, participants, terminated } = await this.socketClient.joinChannel(
|
|
1183
|
+
this.channelId
|
|
1184
|
+
);
|
|
1185
|
+
if (terminated) {
|
|
1186
|
+
log("ConnectionManager", "Stored session was terminated by the wallet; dropping it");
|
|
1187
|
+
this.handleSessionTerminated();
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
if (participants.includes("wallet")) {
|
|
1191
|
+
this.walletPresent = true;
|
|
1192
|
+
}
|
|
744
1193
|
for (const msg of bufferedMessages) {
|
|
745
|
-
this.
|
|
1194
|
+
this.enqueueRelayMessage(msg);
|
|
746
1195
|
}
|
|
747
|
-
|
|
1196
|
+
await this.messageQueue;
|
|
1197
|
+
if (this.keyExchange?.areKeysExchanged() && this.walletPresent) {
|
|
1198
|
+
this.clearReconnectProbe();
|
|
748
1199
|
this.setStatus("connected" /* CONNECTED */);
|
|
749
|
-
} else if (this.keyExchange.areKeysExchanged()) {
|
|
750
|
-
this.setStatus("waiting" /* WAITING */);
|
|
751
1200
|
} else {
|
|
752
|
-
this.
|
|
1201
|
+
this.setStatus("waiting" /* WAITING */);
|
|
1202
|
+
this.armReconnectProbe();
|
|
753
1203
|
}
|
|
754
1204
|
return true;
|
|
755
1205
|
} catch (err) {
|
|
@@ -759,10 +1209,69 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
759
1209
|
}
|
|
760
1210
|
}
|
|
761
1211
|
/**
|
|
762
|
-
*
|
|
1212
|
+
* Nudge the connection back to life after the dApp tab / wallet app was
|
|
1213
|
+
* backgrounded. Idempotent and safe to call from visibilitychange / online
|
|
1214
|
+
* / pageshow handlers. If a restored session has not yet been hydrated,
|
|
1215
|
+
* runs the full reconnect(); otherwise just re-opens the socket, whose
|
|
1216
|
+
* own connect handler re-joins the channel and drains the relay buffer.
|
|
1217
|
+
*/
|
|
1218
|
+
resume() {
|
|
1219
|
+
if (this.status === "connected" /* CONNECTED */) return;
|
|
1220
|
+
if (this.pendingRestore && !this.keyExchange?.areKeysExchanged()) {
|
|
1221
|
+
void this.reconnect();
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (this.channelId && this.keyExchange?.areKeysExchanged()) {
|
|
1225
|
+
if (this.socketClient.getChannelId()) {
|
|
1226
|
+
this.socketClient.connect();
|
|
1227
|
+
} else {
|
|
1228
|
+
this.setStatus("reconnecting" /* RECONNECTING */);
|
|
1229
|
+
this.walletPresent = false;
|
|
1230
|
+
void this.joinAndSettle();
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* A session was terminated for good (wallet sent a relay 'close', or the
|
|
1236
|
+
* join ack reported a tombstone). Clear local state and surface
|
|
1237
|
+
* DISCONNECTED so the consumer drops to a fresh-pairing UI.
|
|
1238
|
+
*/
|
|
1239
|
+
handleSessionTerminated() {
|
|
1240
|
+
this.clearReconnectProbe();
|
|
1241
|
+
this.clearUnresponsiveTimer();
|
|
1242
|
+
this.walletPresent = false;
|
|
1243
|
+
this.pendingRestore = null;
|
|
1244
|
+
this.keyExchange = null;
|
|
1245
|
+
this.socketClient.leaveChannel();
|
|
1246
|
+
this.socketClient.disconnect();
|
|
1247
|
+
this.clearSession();
|
|
1248
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1249
|
+
}
|
|
1250
|
+
armReconnectProbe() {
|
|
1251
|
+
this.clearReconnectProbe();
|
|
1252
|
+
this.reconnectProbeTimer = setTimeout(() => {
|
|
1253
|
+
this.reconnectProbeTimer = null;
|
|
1254
|
+
if (this.walletPresent) return;
|
|
1255
|
+
warn(
|
|
1256
|
+
"ConnectionManager",
|
|
1257
|
+
`No wallet rejoined within ${RECONNECT_WALLET_PROBE_MS}ms; treating reconnect as dead`
|
|
1258
|
+
);
|
|
1259
|
+
this.socketClient.leaveChannel();
|
|
1260
|
+
this.socketClient.disconnect();
|
|
1261
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1262
|
+
}, RECONNECT_WALLET_PROBE_MS);
|
|
1263
|
+
}
|
|
1264
|
+
clearReconnectProbe() {
|
|
1265
|
+
if (this.reconnectProbeTimer) {
|
|
1266
|
+
clearTimeout(this.reconnectProbeTimer);
|
|
1267
|
+
this.reconnectProbeTimer = null;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Send a JSON-RPC request to the wallet (async, fire-and-forget).
|
|
763
1272
|
*/
|
|
764
1273
|
sendJsonRpc(request) {
|
|
765
|
-
if (!this.keyExchange
|
|
1274
|
+
if (!this.keyExchange?.areKeysExchanged()) {
|
|
766
1275
|
throw new Error("Not connected: key exchange not complete");
|
|
767
1276
|
}
|
|
768
1277
|
this.sendEncrypted({
|
|
@@ -771,16 +1280,93 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
771
1280
|
id: request.id,
|
|
772
1281
|
method: request.method,
|
|
773
1282
|
params: request.params
|
|
1283
|
+
}).catch((err) => {
|
|
1284
|
+
error("ConnectionManager", "Failed to send JSON-RPC:", err);
|
|
774
1285
|
});
|
|
775
1286
|
this.startUnresponsiveTimer();
|
|
776
1287
|
}
|
|
1288
|
+
getStatus() {
|
|
1289
|
+
return this.status;
|
|
1290
|
+
}
|
|
1291
|
+
getAccounts() {
|
|
1292
|
+
return this.connectedAccounts;
|
|
1293
|
+
}
|
|
1294
|
+
getChainId() {
|
|
1295
|
+
return this.chainId;
|
|
1296
|
+
}
|
|
1297
|
+
getChannelId() {
|
|
1298
|
+
return this.channelId;
|
|
1299
|
+
}
|
|
1300
|
+
/** Check (sync) if a persisted session exists and has not expired. */
|
|
1301
|
+
hasStoredSession() {
|
|
1302
|
+
if (typeof localStorage === "undefined") return false;
|
|
1303
|
+
try {
|
|
1304
|
+
const raw = localStorage.getItem(this.storageKey);
|
|
1305
|
+
if (!raw) return false;
|
|
1306
|
+
const session = JSON.parse(raw);
|
|
1307
|
+
if (session.version !== 2) return false;
|
|
1308
|
+
return Date.now() - session.createdAt <= SESSION_TTL_MS;
|
|
1309
|
+
} catch {
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Best-effort TERMINATE delivery before we tear the socket down. Without
|
|
1315
|
+
* awaiting, the socket.disconnect() below would win the race against the
|
|
1316
|
+
* outbound emit, the wallet would only see `participants_changed:
|
|
1317
|
+
* disconnect` and enter its stale-session grace period instead of an
|
|
1318
|
+
* instant disconnect. Mirrors the wallet side's pattern.
|
|
1319
|
+
*/
|
|
1320
|
+
async flushTerminate() {
|
|
1321
|
+
if (!this.keyExchange?.areKeysExchanged()) return;
|
|
1322
|
+
const send = this.sendEncrypted({ type: "terminate" /* TERMINATE */ }).catch(() => {
|
|
1323
|
+
});
|
|
1324
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, TERMINATE_SEND_TIMEOUT_MS));
|
|
1325
|
+
await Promise.race([send, timeout]);
|
|
1326
|
+
}
|
|
777
1327
|
/**
|
|
778
|
-
*
|
|
1328
|
+
* Reset to a fresh channel + keypair. Sends TERMINATE to any live peer,
|
|
1329
|
+
* drops the persisted session, and prepares a clean state for
|
|
1330
|
+
* getConnectionURI() to be called again.
|
|
779
1331
|
*/
|
|
780
|
-
|
|
1332
|
+
async resetForNewChannel() {
|
|
1333
|
+
this.clearUnresponsiveTimer();
|
|
1334
|
+
this.clearReconnectProbe();
|
|
1335
|
+
this.walletPresent = false;
|
|
1336
|
+
await this.flushTerminate();
|
|
1337
|
+
this.socketClient.leaveChannel();
|
|
1338
|
+
this.socketClient.disconnect();
|
|
1339
|
+
this.clearSession();
|
|
1340
|
+
this.connectedAccounts = [];
|
|
1341
|
+
this.pendingRestore = null;
|
|
1342
|
+
this.channelId = (0, import_uuid.v4)();
|
|
1343
|
+
this.keyExchange = null;
|
|
1344
|
+
this.socketClient = new SocketClient(this.relayUrl, "dapp");
|
|
1345
|
+
this.setupSocketListeners();
|
|
1346
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1347
|
+
}
|
|
1348
|
+
async disconnect() {
|
|
1349
|
+
this.clearUnresponsiveTimer();
|
|
1350
|
+
this.clearReconnectProbe();
|
|
1351
|
+
this.walletPresent = false;
|
|
1352
|
+
await this.flushTerminate();
|
|
1353
|
+
this.socketClient.leaveChannel();
|
|
1354
|
+
this.socketClient.disconnect();
|
|
1355
|
+
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1356
|
+
this.clearSession();
|
|
1357
|
+
this.connectedAccounts = [];
|
|
1358
|
+
this.pendingRestore = null;
|
|
1359
|
+
this.keyExchange = null;
|
|
1360
|
+
}
|
|
1361
|
+
// ── Internals ──────────────────────────────────────────────
|
|
1362
|
+
enqueueRelayMessage(data) {
|
|
1363
|
+
this.messageQueue = this.messageQueue.then(() => this.handleRelayMessage(data)).catch((err) => error("ConnectionManager", "messageQueue handler error:", err));
|
|
1364
|
+
}
|
|
1365
|
+
async handleRelayMessage(data) {
|
|
781
1366
|
if (data.clientType === "wallet") {
|
|
782
1367
|
this.walletPresent = true;
|
|
783
|
-
|
|
1368
|
+
this.clearReconnectProbe();
|
|
1369
|
+
if (this.keyExchange?.areKeysExchanged() && this.status !== "connected" /* CONNECTED */) {
|
|
784
1370
|
this.failedReconnects = 0;
|
|
785
1371
|
this.setStatus("connected" /* CONNECTED */);
|
|
786
1372
|
}
|
|
@@ -788,16 +1374,18 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
788
1374
|
const message = data.message;
|
|
789
1375
|
if (typeof message === "object" && message !== null) {
|
|
790
1376
|
const msg = message;
|
|
791
|
-
if (msg.type === "
|
|
792
|
-
this.
|
|
793
|
-
|
|
794
|
-
|
|
1377
|
+
if (msg.type === "key_handshake_SYNACK" /* SYNACK */) {
|
|
1378
|
+
await this.handleSynAck(message);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
if (msg.type === "key_handshake_SYN" /* SYN */ || msg.type === "key_handshake_ACK" /* ACK */) {
|
|
1382
|
+
warn("ConnectionManager", `Unexpected ${msg.type} on dApp side \u2014 ignoring`);
|
|
795
1383
|
return;
|
|
796
1384
|
}
|
|
797
1385
|
}
|
|
798
|
-
if (typeof message === "string" && this.keyExchange
|
|
1386
|
+
if (typeof message === "string" && this.keyExchange?.areKeysExchanged()) {
|
|
799
1387
|
try {
|
|
800
|
-
const decrypted = this.keyExchange.decryptMessage(message);
|
|
1388
|
+
const decrypted = await this.keyExchange.decryptMessage(message);
|
|
801
1389
|
const parsed = JSON.parse(decrypted);
|
|
802
1390
|
this.handleDecryptedMessage(parsed);
|
|
803
1391
|
} catch (err) {
|
|
@@ -805,9 +1393,18 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
805
1393
|
}
|
|
806
1394
|
}
|
|
807
1395
|
}
|
|
808
|
-
|
|
1396
|
+
async handleSynAck(msg) {
|
|
1397
|
+
if (!this.keyExchange) return;
|
|
809
1398
|
this.setStatus("key_exchange" /* KEY_EXCHANGE */);
|
|
810
|
-
|
|
1399
|
+
let response;
|
|
1400
|
+
try {
|
|
1401
|
+
response = await this.keyExchange.onSynAck(cidFromString(this.channelId), msg);
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1404
|
+
error("ConnectionManager", "SYNACK processing failed:", e);
|
|
1405
|
+
this.emit("error", e);
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
811
1408
|
if (response) {
|
|
812
1409
|
this.sendPlaintext(response);
|
|
813
1410
|
}
|
|
@@ -824,17 +1421,13 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
824
1421
|
const chainChanged = this.chainId !== nextChainId;
|
|
825
1422
|
this.connectedAccounts = nextAccounts;
|
|
826
1423
|
this.chainId = nextChainId;
|
|
827
|
-
this.
|
|
1424
|
+
void this.persistSession();
|
|
828
1425
|
this.emit("wallet_info", {
|
|
829
1426
|
accounts: this.connectedAccounts,
|
|
830
1427
|
chainId: this.chainId
|
|
831
1428
|
});
|
|
832
|
-
if (accountsChanged)
|
|
833
|
-
|
|
834
|
-
}
|
|
835
|
-
if (chainChanged) {
|
|
836
|
-
this.emit("chain_changed", this.chainId);
|
|
837
|
-
}
|
|
1429
|
+
if (accountsChanged) this.emit("accounts_changed", this.connectedAccounts);
|
|
1430
|
+
if (chainChanged) this.emit("chain_changed", this.chainId);
|
|
838
1431
|
break;
|
|
839
1432
|
}
|
|
840
1433
|
case "jsonrpc" /* JSONRPC */: {
|
|
@@ -844,14 +1437,13 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
844
1437
|
}
|
|
845
1438
|
case "terminate" /* TERMINATE */: {
|
|
846
1439
|
log("ConnectionManager", "Received terminate from wallet");
|
|
847
|
-
this.disconnect();
|
|
1440
|
+
void this.disconnect();
|
|
848
1441
|
break;
|
|
849
1442
|
}
|
|
850
1443
|
default:
|
|
851
1444
|
log("ConnectionManager", `Unhandled message type: ${type}`);
|
|
852
1445
|
}
|
|
853
1446
|
}
|
|
854
|
-
/** Send a plaintext (unencrypted) message through the relay. */
|
|
855
1447
|
sendPlaintext(message) {
|
|
856
1448
|
this.socketClient.sendMessage({
|
|
857
1449
|
id: this.channelId,
|
|
@@ -861,20 +1453,16 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
861
1453
|
error("ConnectionManager", "Failed to send plaintext:", err);
|
|
862
1454
|
});
|
|
863
1455
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
const encrypted = this.keyExchange.encryptMessage(JSON.stringify(message));
|
|
868
|
-
this.socketClient.sendMessage({
|
|
869
|
-
id: this.channelId,
|
|
870
|
-
clientType: "dapp",
|
|
871
|
-
message: encrypted
|
|
872
|
-
}).catch((err) => {
|
|
873
|
-
error("ConnectionManager", "Failed to send encrypted:", err);
|
|
874
|
-
});
|
|
875
|
-
} catch (err) {
|
|
876
|
-
error("ConnectionManager", "Encryption failed:", err);
|
|
1456
|
+
async sendEncrypted(message) {
|
|
1457
|
+
if (!this.keyExchange?.areKeysExchanged()) {
|
|
1458
|
+
throw new Error("sendEncrypted: not connected");
|
|
877
1459
|
}
|
|
1460
|
+
const encrypted = await this.keyExchange.encryptMessage(JSON.stringify(message));
|
|
1461
|
+
await this.socketClient.sendMessage({
|
|
1462
|
+
id: this.channelId,
|
|
1463
|
+
clientType: "dapp",
|
|
1464
|
+
message: encrypted
|
|
1465
|
+
});
|
|
878
1466
|
}
|
|
879
1467
|
startUnresponsiveTimer() {
|
|
880
1468
|
this.clearUnresponsiveTimer();
|
|
@@ -888,46 +1476,6 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
888
1476
|
this.unresponsiveTimer = null;
|
|
889
1477
|
}
|
|
890
1478
|
}
|
|
891
|
-
sendSYN() {
|
|
892
|
-
if (!this.socketClient.isConnected()) {
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
if (this.synAttempts >= _ConnectionManager.MAX_SYN_ATTEMPTS) {
|
|
896
|
-
this.synSent = false;
|
|
897
|
-
this.emit("error", new Error("Key exchange timed out waiting for wallet response"));
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
try {
|
|
901
|
-
const syn = this.keyExchange.createSYN();
|
|
902
|
-
this.sendPlaintext(syn);
|
|
903
|
-
this.synSent = true;
|
|
904
|
-
this.synAttempts++;
|
|
905
|
-
this.scheduleSynRetry();
|
|
906
|
-
log("ConnectionManager", "Sent SYN key exchange message");
|
|
907
|
-
} catch (err) {
|
|
908
|
-
error("ConnectionManager", "Failed to send SYN:", err);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
scheduleSynRetry() {
|
|
912
|
-
this.clearSynRetryTimer();
|
|
913
|
-
this.synRetryTimer = setTimeout(() => {
|
|
914
|
-
this.synRetryTimer = null;
|
|
915
|
-
if (this.keyExchange.areKeysExchanged()) {
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
this.synSent = false;
|
|
919
|
-
if (!this.walletPresent || !this.socketClient.isConnected()) {
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
this.sendSYN();
|
|
923
|
-
}, _ConnectionManager.SYN_RETRY_MS);
|
|
924
|
-
}
|
|
925
|
-
clearSynRetryTimer() {
|
|
926
|
-
if (this.synRetryTimer) {
|
|
927
|
-
clearTimeout(this.synRetryTimer);
|
|
928
|
-
this.synRetryTimer = null;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
1479
|
setStatus(status) {
|
|
932
1480
|
if (this.status !== status) {
|
|
933
1481
|
this.status = status;
|
|
@@ -940,105 +1488,44 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
940
1488
|
}
|
|
941
1489
|
areArraysEqual(a, b) {
|
|
942
1490
|
if (a.length !== b.length) return false;
|
|
943
|
-
for (let i = 0; i < a.length; i++)
|
|
944
|
-
if (a[i] !== b[i]) return false;
|
|
945
|
-
}
|
|
1491
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
946
1492
|
return true;
|
|
947
1493
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
}
|
|
951
|
-
getAccounts() {
|
|
952
|
-
return this.connectedAccounts;
|
|
953
|
-
}
|
|
954
|
-
getChainId() {
|
|
955
|
-
return this.chainId;
|
|
956
|
-
}
|
|
957
|
-
getChannelId() {
|
|
958
|
-
return this.channelId;
|
|
959
|
-
}
|
|
960
|
-
/** Check if a stored session exists (without loading/restoring it). */
|
|
961
|
-
hasStoredSession() {
|
|
962
|
-
if (typeof localStorage === "undefined") return false;
|
|
963
|
-
try {
|
|
964
|
-
const raw = localStorage.getItem(this.storageKey);
|
|
965
|
-
if (!raw) return false;
|
|
966
|
-
const session = JSON.parse(raw);
|
|
967
|
-
return Date.now() - session.createdAt <= SESSION_TTL_MS;
|
|
968
|
-
} catch {
|
|
969
|
-
return false;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
/**
|
|
973
|
-
* Reset the connection and create a new channel for a fresh pairing.
|
|
974
|
-
* Disconnects any existing session, generates a new channel ID and keys.
|
|
975
|
-
*/
|
|
976
|
-
resetForNewChannel() {
|
|
977
|
-
this.clearUnresponsiveTimer();
|
|
978
|
-
this.clearSynRetryTimer();
|
|
979
|
-
this.synSent = false;
|
|
980
|
-
this.synAttempts = 0;
|
|
981
|
-
this.walletPresent = false;
|
|
982
|
-
if (this.keyExchange.areKeysExchanged()) {
|
|
983
|
-
try {
|
|
984
|
-
this.sendEncrypted({ type: "terminate" /* TERMINATE */ });
|
|
985
|
-
} catch {
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
this.socketClient.leaveChannel();
|
|
989
|
-
this.socketClient.disconnect();
|
|
990
|
-
this.clearSession();
|
|
991
|
-
this.connectedAccounts = [];
|
|
992
|
-
this.channelId = (0, import_uuid.v4)();
|
|
993
|
-
this.ecies = new ECIESClient();
|
|
994
|
-
this.keyExchange = new KeyExchange(this.ecies, true);
|
|
995
|
-
this.socketClient = new SocketClient(this.relayUrl, "dapp");
|
|
996
|
-
this.setupSocketListeners();
|
|
997
|
-
this.setupKeyExchangeListeners();
|
|
998
|
-
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
999
|
-
}
|
|
1000
|
-
/** Disconnect and clean up. */
|
|
1001
|
-
disconnect() {
|
|
1002
|
-
this.clearUnresponsiveTimer();
|
|
1003
|
-
this.clearSynRetryTimer();
|
|
1004
|
-
this.synSent = false;
|
|
1005
|
-
this.synAttempts = 0;
|
|
1006
|
-
this.walletPresent = false;
|
|
1007
|
-
if (this.keyExchange.areKeysExchanged()) {
|
|
1008
|
-
try {
|
|
1009
|
-
this.sendEncrypted({ type: "terminate" /* TERMINATE */ });
|
|
1010
|
-
} catch {
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
this.socketClient.leaveChannel();
|
|
1014
|
-
this.socketClient.disconnect();
|
|
1015
|
-
this.setStatus("disconnected" /* DISCONNECTED */);
|
|
1016
|
-
this.clearSession();
|
|
1017
|
-
this.connectedAccounts = [];
|
|
1018
|
-
}
|
|
1019
|
-
// --- Session persistence ---
|
|
1020
|
-
saveSession() {
|
|
1494
|
+
// ── Persistence ────────────────────────────────────────────
|
|
1495
|
+
async persistSession() {
|
|
1021
1496
|
if (typeof localStorage === "undefined") return;
|
|
1497
|
+
if (!this.keyExchange) return;
|
|
1498
|
+
const persistedKex = await this.keyExchange.exportPersisted();
|
|
1499
|
+
if (!persistedKex) return;
|
|
1022
1500
|
const session = {
|
|
1501
|
+
version: 2,
|
|
1023
1502
|
channelId: this.channelId,
|
|
1024
|
-
|
|
1025
|
-
otherPublicKey: this.keyExchange.getOtherPublicKey(),
|
|
1503
|
+
keyExchange: persistedKex,
|
|
1026
1504
|
dappMetadata: this.dappMetadata,
|
|
1027
1505
|
connectedAccounts: this.connectedAccounts,
|
|
1028
1506
|
chainId: this.chainId,
|
|
1029
|
-
createdAt: Date.now(),
|
|
1507
|
+
createdAt: this.pendingRestore?.createdAt ?? Date.now(),
|
|
1030
1508
|
lastActivity: Date.now()
|
|
1031
1509
|
};
|
|
1032
|
-
|
|
1510
|
+
try {
|
|
1511
|
+
localStorage.setItem(this.storageKey, JSON.stringify(session));
|
|
1512
|
+
} catch (err) {
|
|
1513
|
+
warn("ConnectionManager", "localStorage.setItem failed:", err);
|
|
1514
|
+
}
|
|
1033
1515
|
}
|
|
1034
|
-
|
|
1516
|
+
readStoredSession() {
|
|
1035
1517
|
if (typeof localStorage === "undefined") return null;
|
|
1036
1518
|
try {
|
|
1037
1519
|
const raw = localStorage.getItem(this.storageKey);
|
|
1038
1520
|
if (!raw) return null;
|
|
1039
1521
|
const session = JSON.parse(raw);
|
|
1040
|
-
if (
|
|
1041
|
-
|
|
1522
|
+
if (session.version !== 2) {
|
|
1523
|
+
log("ConnectionManager", "Dropping legacy (pre-v2) session from storage");
|
|
1524
|
+
localStorage.removeItem(this.storageKey);
|
|
1525
|
+
return null;
|
|
1526
|
+
}
|
|
1527
|
+
if (!session.createdAt || Date.now() - session.createdAt > SESSION_TTL_MS) {
|
|
1528
|
+
localStorage.removeItem(this.storageKey);
|
|
1042
1529
|
return null;
|
|
1043
1530
|
}
|
|
1044
1531
|
return session;
|
|
@@ -1052,8 +1539,6 @@ var _ConnectionManager = class _ConnectionManager extends import_eventemitter33.
|
|
|
1052
1539
|
}
|
|
1053
1540
|
};
|
|
1054
1541
|
_ConnectionManager.MAX_RECONNECT_FAILURES = 5;
|
|
1055
|
-
_ConnectionManager.SYN_RETRY_MS = 5e3;
|
|
1056
|
-
_ConnectionManager.MAX_SYN_ATTEMPTS = 6;
|
|
1057
1542
|
var ConnectionManager = _ConnectionManager;
|
|
1058
1543
|
|
|
1059
1544
|
// src/utils/platform.ts
|
|
@@ -1070,10 +1555,29 @@ function getAppStoreUrl() {
|
|
|
1070
1555
|
|
|
1071
1556
|
// src/QRLConnectProvider.ts
|
|
1072
1557
|
var requestCounter = 0;
|
|
1558
|
+
var QRL_CONNECT_PROVIDER_INFO = {
|
|
1559
|
+
name: "MyQRLWallet (Mobile)",
|
|
1560
|
+
rdns: "com.qrlwallet.connect",
|
|
1561
|
+
icon: "data:image/png;base64,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"
|
|
1562
|
+
};
|
|
1563
|
+
var EIP6963_ANNOUNCE_EVENT = "eip6963:announceProvider";
|
|
1564
|
+
var EIP6963_REQUEST_EVENT = "eip6963:requestProvider";
|
|
1565
|
+
function generateUuid() {
|
|
1566
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto?.randomUUID) {
|
|
1567
|
+
return globalThis.crypto.randomUUID();
|
|
1568
|
+
}
|
|
1569
|
+
const r = (n) => Math.floor(Math.random() * n);
|
|
1570
|
+
const hex = (n, len) => n.toString(16).padStart(len, "0");
|
|
1571
|
+
return `${hex(r(4294967296), 8)}-${hex(r(65536), 4)}-${hex(16384 | r(4096), 4)}-${hex(32768 | r(16384), 4)}-${hex(r(4294967296), 8)}${hex(r(65536), 4)}`;
|
|
1572
|
+
}
|
|
1073
1573
|
var QRLConnectProvider = class extends import_eventemitter34.default {
|
|
1074
1574
|
constructor(options) {
|
|
1075
1575
|
super();
|
|
1076
1576
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
1577
|
+
this.eip6963Detail = null;
|
|
1578
|
+
this.eip6963RequestListener = null;
|
|
1579
|
+
this.resumeListener = null;
|
|
1580
|
+
this.resumeDebounce = null;
|
|
1077
1581
|
this.isQRLConnect = true;
|
|
1078
1582
|
this.options = options;
|
|
1079
1583
|
if (options.debug) {
|
|
@@ -1089,6 +1593,81 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
|
|
|
1089
1593
|
if (options.autoReconnect !== false) {
|
|
1090
1594
|
this.connectionManager.reconnect();
|
|
1091
1595
|
}
|
|
1596
|
+
this.setupResumeListeners();
|
|
1597
|
+
if (options.announceProvider !== false) {
|
|
1598
|
+
this.startEip6963Announce();
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
startEip6963Announce() {
|
|
1602
|
+
if (typeof window === "undefined" || typeof CustomEvent === "undefined") {
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
const overrides = this.options.providerInfo ?? {};
|
|
1606
|
+
this.eip6963Detail = Object.freeze({
|
|
1607
|
+
info: Object.freeze({
|
|
1608
|
+
uuid: generateUuid(),
|
|
1609
|
+
name: overrides.name ?? QRL_CONNECT_PROVIDER_INFO.name,
|
|
1610
|
+
icon: overrides.icon ?? QRL_CONNECT_PROVIDER_INFO.icon,
|
|
1611
|
+
rdns: overrides.rdns ?? QRL_CONNECT_PROVIDER_INFO.rdns
|
|
1612
|
+
}),
|
|
1613
|
+
provider: this
|
|
1614
|
+
});
|
|
1615
|
+
const announce = () => {
|
|
1616
|
+
if (!this.eip6963Detail) return;
|
|
1617
|
+
window.dispatchEvent(new CustomEvent(EIP6963_ANNOUNCE_EVENT, { detail: this.eip6963Detail }));
|
|
1618
|
+
};
|
|
1619
|
+
this.eip6963RequestListener = announce;
|
|
1620
|
+
window.addEventListener(EIP6963_REQUEST_EVENT, announce);
|
|
1621
|
+
announce();
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Re-open the relay socket when the dApp tab regains focus or the network
|
|
1625
|
+
* comes back. Debounced because visibilitychange + online can fire together.
|
|
1626
|
+
* No-op when already connected (ConnectionManager.resume guards that).
|
|
1627
|
+
*/
|
|
1628
|
+
setupResumeListeners() {
|
|
1629
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
1630
|
+
if (this.resumeListener) return;
|
|
1631
|
+
const resume = () => {
|
|
1632
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
1633
|
+
if (this.resumeDebounce) clearTimeout(this.resumeDebounce);
|
|
1634
|
+
this.resumeDebounce = setTimeout(() => {
|
|
1635
|
+
this.resumeDebounce = null;
|
|
1636
|
+
this.connectionManager.resume();
|
|
1637
|
+
}, 300);
|
|
1638
|
+
};
|
|
1639
|
+
this.resumeListener = resume;
|
|
1640
|
+
document.addEventListener("visibilitychange", resume);
|
|
1641
|
+
window.addEventListener("online", resume);
|
|
1642
|
+
window.addEventListener("pageshow", resume);
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Remove the foreground/online resume listeners. Called on disconnect so a
|
|
1646
|
+
* torn-down session can't be silently revived by a later tab focus; the
|
|
1647
|
+
* listeners are re-armed by getConnectionURI()/newConnection() if the same
|
|
1648
|
+
* provider is re-paired. Safe in any env.
|
|
1649
|
+
*/
|
|
1650
|
+
teardownResumeListeners() {
|
|
1651
|
+
if (typeof window !== "undefined" && typeof document !== "undefined" && this.resumeListener) {
|
|
1652
|
+
document.removeEventListener("visibilitychange", this.resumeListener);
|
|
1653
|
+
window.removeEventListener("online", this.resumeListener);
|
|
1654
|
+
window.removeEventListener("pageshow", this.resumeListener);
|
|
1655
|
+
}
|
|
1656
|
+
this.resumeListener = null;
|
|
1657
|
+
if (this.resumeDebounce) {
|
|
1658
|
+
clearTimeout(this.resumeDebounce);
|
|
1659
|
+
this.resumeDebounce = null;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Stop announcing this provider over EIP-6963. Safe to call from any env.
|
|
1664
|
+
*/
|
|
1665
|
+
stopEip6963Announce() {
|
|
1666
|
+
if (typeof window !== "undefined" && this.eip6963RequestListener) {
|
|
1667
|
+
window.removeEventListener(EIP6963_REQUEST_EVENT, this.eip6963RequestListener);
|
|
1668
|
+
}
|
|
1669
|
+
this.eip6963RequestListener = null;
|
|
1670
|
+
this.eip6963Detail = null;
|
|
1092
1671
|
}
|
|
1093
1672
|
setupConnectionListeners() {
|
|
1094
1673
|
this.connectionManager.on("status_changed", (status) => {
|
|
@@ -1146,6 +1725,7 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
|
|
|
1146
1725
|
* Generate a connection URI for QR code display or deep link redirect.
|
|
1147
1726
|
*/
|
|
1148
1727
|
async getConnectionURI() {
|
|
1728
|
+
this.setupResumeListeners();
|
|
1149
1729
|
return this.connectionManager.getConnectionURI();
|
|
1150
1730
|
}
|
|
1151
1731
|
/**
|
|
@@ -1253,36 +1833,421 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
|
|
|
1253
1833
|
pending.reject(new Error("Connection reset"));
|
|
1254
1834
|
}
|
|
1255
1835
|
this.pendingRequests.clear();
|
|
1256
|
-
this.connectionManager.resetForNewChannel();
|
|
1836
|
+
await this.connectionManager.resetForNewChannel();
|
|
1257
1837
|
return this.getConnectionURI();
|
|
1258
1838
|
}
|
|
1259
1839
|
/**
|
|
1260
|
-
* Disconnect from wallet and clean up.
|
|
1840
|
+
* Disconnect from wallet and clean up. Returns once the TERMINATE has
|
|
1841
|
+
* either been flushed to the relay or the 800ms best-effort window has
|
|
1842
|
+
* elapsed — the wallet gets an instant disconnect instead of landing in
|
|
1843
|
+
* its stale-session grace period.
|
|
1261
1844
|
*/
|
|
1262
|
-
disconnect() {
|
|
1845
|
+
async disconnect() {
|
|
1263
1846
|
for (const [, pending] of this.pendingRequests) {
|
|
1264
1847
|
pending.reject(new Error("Disconnected"));
|
|
1265
1848
|
}
|
|
1266
1849
|
this.pendingRequests.clear();
|
|
1267
|
-
this.
|
|
1850
|
+
this.teardownResumeListeners();
|
|
1851
|
+
await this.connectionManager.disconnect();
|
|
1268
1852
|
}
|
|
1269
1853
|
};
|
|
1854
|
+
|
|
1855
|
+
// src/signing/ctx.ts
|
|
1856
|
+
var SCHEME_VERSION_MSG = "QRL-SIGN-MSG-v1";
|
|
1857
|
+
var SCHEME_VERSION_TYPED = "QRL-SIGN-TYPED-v1";
|
|
1858
|
+
var SCHEME_TAG_MSG = new TextEncoder().encode(SCHEME_VERSION_MSG);
|
|
1859
|
+
var SCHEME_TAG_TYPED = new TextEncoder().encode(SCHEME_VERSION_TYPED);
|
|
1860
|
+
var DIGEST_LEN = 64;
|
|
1861
|
+
|
|
1862
|
+
// src/signing/messageDigest.ts
|
|
1863
|
+
var import_sha3 = require("@noble/hashes/sha3.js");
|
|
1864
|
+
|
|
1865
|
+
// src/signing/bytes.ts
|
|
1866
|
+
function hexToBytes(hex) {
|
|
1867
|
+
if (typeof hex !== "string") throw new Error("hex must be a string");
|
|
1868
|
+
if (!/^0x([0-9a-fA-F]{2})*$/.test(hex)) {
|
|
1869
|
+
throw new Error("hex must be 0x-prefixed even-length");
|
|
1870
|
+
}
|
|
1871
|
+
const n = (hex.length - 2) / 2;
|
|
1872
|
+
const out = new Uint8Array(n);
|
|
1873
|
+
for (let i = 0; i < n; i++) {
|
|
1874
|
+
out[i] = parseInt(hex.slice(2 + i * 2, 4 + i * 2), 16);
|
|
1875
|
+
}
|
|
1876
|
+
return out;
|
|
1877
|
+
}
|
|
1878
|
+
function bytesToHex(bytes) {
|
|
1879
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
1880
|
+
throw new Error("bytes must be a Uint8Array");
|
|
1881
|
+
}
|
|
1882
|
+
let s = "0x";
|
|
1883
|
+
for (const v of bytes) s += v.toString(16).padStart(2, "0");
|
|
1884
|
+
return s;
|
|
1885
|
+
}
|
|
1886
|
+
function concatBytes(...parts) {
|
|
1887
|
+
return concatBytesArr(parts);
|
|
1888
|
+
}
|
|
1889
|
+
function concatBytesArr(parts) {
|
|
1890
|
+
let total = 0;
|
|
1891
|
+
for (const p of parts) total += p.length;
|
|
1892
|
+
const out = new Uint8Array(total);
|
|
1893
|
+
let off = 0;
|
|
1894
|
+
for (const p of parts) {
|
|
1895
|
+
out.set(p, off);
|
|
1896
|
+
off += p.length;
|
|
1897
|
+
}
|
|
1898
|
+
return out;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// src/signing/messageDigest.ts
|
|
1902
|
+
function computeMessageDigest(messageBytes) {
|
|
1903
|
+
if (!(messageBytes instanceof Uint8Array)) {
|
|
1904
|
+
throw new Error("messageBytes must be a Uint8Array");
|
|
1905
|
+
}
|
|
1906
|
+
return (0, import_sha3.shake256)(concatBytes(SCHEME_TAG_MSG, messageBytes), { dkLen: DIGEST_LEN });
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// src/signing/typedData.ts
|
|
1910
|
+
var import_sha32 = require("@noble/hashes/sha3.js");
|
|
1911
|
+
var SLOT = 32;
|
|
1912
|
+
var ATOMIC_RE = /^(?:(address|bool|string|bytes)|(u?int)(\d+)|bytes(\d+)|(.+?)\[(\d*)\])$/;
|
|
1913
|
+
var MAX_TYPE_DEPTH = 12;
|
|
1914
|
+
function parseFieldType(type, types, depth = 0) {
|
|
1915
|
+
if (depth > MAX_TYPE_DEPTH) throw new Error(`type nesting too deep: ${type}`);
|
|
1916
|
+
if (Object.prototype.hasOwnProperty.call(types, type)) {
|
|
1917
|
+
return { kind: "ref", name: type };
|
|
1918
|
+
}
|
|
1919
|
+
const m = ATOMIC_RE.exec(type);
|
|
1920
|
+
if (!m) throw new Error(`unknown type: ${type}`);
|
|
1921
|
+
const [, atomic, intKind, intWidthStr, bytesWidthStr, innerType, sizeStr] = m;
|
|
1922
|
+
if (atomic) return { kind: atomic };
|
|
1923
|
+
if (intKind) {
|
|
1924
|
+
const width = Number(intWidthStr);
|
|
1925
|
+
if (!Number.isInteger(width) || width < 8 || width > 256 || width % 8 !== 0) {
|
|
1926
|
+
throw new Error(`invalid int width: ${type}`);
|
|
1927
|
+
}
|
|
1928
|
+
return { kind: intKind === "uint" ? "uintN" : "intN", width };
|
|
1929
|
+
}
|
|
1930
|
+
if (bytesWidthStr) {
|
|
1931
|
+
const width = Number(bytesWidthStr);
|
|
1932
|
+
if (!Number.isInteger(width) || width < 1 || width > 32) {
|
|
1933
|
+
throw new Error(`invalid bytesN width: ${type}`);
|
|
1934
|
+
}
|
|
1935
|
+
return { kind: "bytesN", width };
|
|
1936
|
+
}
|
|
1937
|
+
if (innerType !== void 0) {
|
|
1938
|
+
parseFieldType(innerType, types, depth + 1);
|
|
1939
|
+
const size = sizeStr ? Number(sizeStr) : void 0;
|
|
1940
|
+
if (sizeStr && (!Number.isInteger(size) || size <= 0)) {
|
|
1941
|
+
throw new Error(`invalid array size: ${type}`);
|
|
1942
|
+
}
|
|
1943
|
+
return { kind: "array", inner: innerType, size };
|
|
1944
|
+
}
|
|
1945
|
+
throw new Error(`unhandled type: ${type}`);
|
|
1946
|
+
}
|
|
1947
|
+
function baseTypeName(type) {
|
|
1948
|
+
return type.replace(/(\[\d*\])+$/, "");
|
|
1949
|
+
}
|
|
1950
|
+
function collectDependencies(primary, types) {
|
|
1951
|
+
if (!Object.prototype.hasOwnProperty.call(types, primary)) {
|
|
1952
|
+
throw new Error(`unknown primary type: ${primary}`);
|
|
1953
|
+
}
|
|
1954
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1955
|
+
const visit = (name, path) => {
|
|
1956
|
+
if (path.includes(name)) {
|
|
1957
|
+
throw new Error(`cyclic type reference: ${[...path, name].join(" -> ")}`);
|
|
1958
|
+
}
|
|
1959
|
+
if (visited.has(name)) return;
|
|
1960
|
+
visited.add(name);
|
|
1961
|
+
for (const f of types[name]) {
|
|
1962
|
+
const base = baseTypeName(f.type);
|
|
1963
|
+
if (Object.prototype.hasOwnProperty.call(types, base)) {
|
|
1964
|
+
visit(base, [...path, name]);
|
|
1965
|
+
}
|
|
1966
|
+
parseFieldType(f.type, types);
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
visit(primary, []);
|
|
1970
|
+
return visited;
|
|
1971
|
+
}
|
|
1972
|
+
function validateTypeMap(types) {
|
|
1973
|
+
for (const [name, def] of Object.entries(types)) {
|
|
1974
|
+
if (!Array.isArray(def) || def.length === 0) {
|
|
1975
|
+
throw new Error(`empty or invalid struct: ${name}`);
|
|
1976
|
+
}
|
|
1977
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1978
|
+
for (const f of def) {
|
|
1979
|
+
if (!f || typeof f.name !== "string" || !f.name) {
|
|
1980
|
+
throw new Error(`bad field in ${name}`);
|
|
1981
|
+
}
|
|
1982
|
+
if (typeof f.type !== "string" || !f.type) {
|
|
1983
|
+
throw new Error(`bad field type in ${name}.${f.name}`);
|
|
1984
|
+
}
|
|
1985
|
+
if (seen.has(f.name)) {
|
|
1986
|
+
throw new Error(`duplicate field "${f.name}" in ${name}`);
|
|
1987
|
+
}
|
|
1988
|
+
seen.add(f.name);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
function encodeType(primary, types) {
|
|
1993
|
+
const deps = collectDependencies(primary, types);
|
|
1994
|
+
const others = [...deps].filter((n) => n !== primary).sort();
|
|
1995
|
+
return [primary, ...others].map((name) => {
|
|
1996
|
+
const fields = types[name];
|
|
1997
|
+
const inner = fields.map((f) => `${f.type} ${f.name}`).join(",");
|
|
1998
|
+
return `${name}(${inner})`;
|
|
1999
|
+
}).join("");
|
|
2000
|
+
}
|
|
2001
|
+
function typeHash(primary, types) {
|
|
2002
|
+
return (0, import_sha32.shake256)(new TextEncoder().encode(encodeType(primary, types)), { dkLen: DIGEST_LEN });
|
|
2003
|
+
}
|
|
2004
|
+
function parseQAddress(addr) {
|
|
2005
|
+
if (typeof addr !== "string" || !/^Q[0-9a-fA-F]{40}$/.test(addr)) {
|
|
2006
|
+
throw new Error(`invalid Q-address: ${String(addr)}`);
|
|
2007
|
+
}
|
|
2008
|
+
return hexToBytes("0x" + addr.slice(1).toLowerCase());
|
|
2009
|
+
}
|
|
2010
|
+
function padLeft32(bytes) {
|
|
2011
|
+
if (bytes.length > SLOT) throw new Error("cannot pad: bytes > 32");
|
|
2012
|
+
const out = new Uint8Array(SLOT);
|
|
2013
|
+
out.set(bytes, SLOT - bytes.length);
|
|
2014
|
+
return out;
|
|
2015
|
+
}
|
|
2016
|
+
function padRight32(bytes) {
|
|
2017
|
+
if (bytes.length > SLOT) throw new Error("cannot pad: bytes > 32");
|
|
2018
|
+
const out = new Uint8Array(SLOT);
|
|
2019
|
+
out.set(bytes, 0);
|
|
2020
|
+
return out;
|
|
2021
|
+
}
|
|
2022
|
+
function bigIntToSlot(value, width, signed) {
|
|
2023
|
+
if (signed) {
|
|
2024
|
+
const limit = 1n << BigInt(width - 1);
|
|
2025
|
+
if (value >= limit || value < -limit) {
|
|
2026
|
+
throw new Error(`int${width} out of range: ${value}`);
|
|
2027
|
+
}
|
|
2028
|
+
} else {
|
|
2029
|
+
if (value < 0n) throw new Error(`uint${width} negative: ${value}`);
|
|
2030
|
+
const limit = 1n << BigInt(width);
|
|
2031
|
+
if (value >= limit) throw new Error(`uint${width} out of range: ${value}`);
|
|
2032
|
+
}
|
|
2033
|
+
const slotMask = (1n << 256n) - 1n;
|
|
2034
|
+
const repr = value < 0n ? value + (1n << 256n) & slotMask : value;
|
|
2035
|
+
const out = new Uint8Array(SLOT);
|
|
2036
|
+
let v = repr;
|
|
2037
|
+
for (let i = SLOT - 1; i >= 0; i--) {
|
|
2038
|
+
out[i] = Number(v & 0xffn);
|
|
2039
|
+
v >>= 8n;
|
|
2040
|
+
}
|
|
2041
|
+
return out;
|
|
2042
|
+
}
|
|
2043
|
+
function parseIntValue(v, typeLabel) {
|
|
2044
|
+
if (typeof v === "bigint") return v;
|
|
2045
|
+
if (typeof v === "string") {
|
|
2046
|
+
if (/^-?0x[0-9a-fA-F]+$/i.test(v)) {
|
|
2047
|
+
const isNegative = v.startsWith("-");
|
|
2048
|
+
const abs = BigInt(isNegative ? v.slice(1) : v);
|
|
2049
|
+
return isNegative ? -abs : abs;
|
|
2050
|
+
}
|
|
2051
|
+
if (!/^-?(0|[1-9]\d*)$/.test(v)) {
|
|
2052
|
+
throw new Error(`invalid ${typeLabel} string: ${v}`);
|
|
2053
|
+
}
|
|
2054
|
+
return BigInt(v);
|
|
2055
|
+
}
|
|
2056
|
+
if (typeof v === "number") {
|
|
2057
|
+
if (!Number.isInteger(v) || !Number.isSafeInteger(v)) {
|
|
2058
|
+
throw new Error(`unsafe ${typeLabel} number: ${v}, pass as string`);
|
|
2059
|
+
}
|
|
2060
|
+
return BigInt(v);
|
|
2061
|
+
}
|
|
2062
|
+
throw new Error(`unsupported ${typeLabel} value: ${typeof v}`);
|
|
2063
|
+
}
|
|
2064
|
+
function encodeField(type, value, types) {
|
|
2065
|
+
const parsed = parseFieldType(type, types);
|
|
2066
|
+
switch (parsed.kind) {
|
|
2067
|
+
case "ref":
|
|
2068
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
2069
|
+
throw new Error(`struct field expects object: ${type}`);
|
|
2070
|
+
}
|
|
2071
|
+
return hashStruct(parsed.name, value, types);
|
|
2072
|
+
case "address":
|
|
2073
|
+
if (typeof value !== "string") {
|
|
2074
|
+
throw new Error(`address field expects string: ${typeof value}`);
|
|
2075
|
+
}
|
|
2076
|
+
return padLeft32(parseQAddress(value));
|
|
2077
|
+
case "bool":
|
|
2078
|
+
if (typeof value !== "boolean") {
|
|
2079
|
+
throw new Error(`bool field expects boolean: ${typeof value}`);
|
|
2080
|
+
}
|
|
2081
|
+
return padLeft32(new Uint8Array([value ? 1 : 0]));
|
|
2082
|
+
case "string":
|
|
2083
|
+
if (typeof value !== "string") {
|
|
2084
|
+
throw new Error(`string field expects string: ${typeof value}`);
|
|
2085
|
+
}
|
|
2086
|
+
return (0, import_sha32.shake256)(new TextEncoder().encode(value), { dkLen: DIGEST_LEN });
|
|
2087
|
+
case "bytes":
|
|
2088
|
+
if (typeof value !== "string") {
|
|
2089
|
+
throw new Error(`bytes field expects 0x-hex string: ${typeof value}`);
|
|
2090
|
+
}
|
|
2091
|
+
return (0, import_sha32.shake256)(hexToBytes(value), { dkLen: DIGEST_LEN });
|
|
2092
|
+
case "uintN":
|
|
2093
|
+
return bigIntToSlot(parseIntValue(value, `uint${parsed.width}`), parsed.width, false);
|
|
2094
|
+
case "intN":
|
|
2095
|
+
return bigIntToSlot(parseIntValue(value, `int${parsed.width}`), parsed.width, true);
|
|
2096
|
+
case "bytesN": {
|
|
2097
|
+
if (typeof value !== "string") {
|
|
2098
|
+
throw new Error(`bytes${parsed.width} expects 0x-hex string: ${typeof value}`);
|
|
2099
|
+
}
|
|
2100
|
+
const raw = hexToBytes(value);
|
|
2101
|
+
if (raw.length !== parsed.width) {
|
|
2102
|
+
throw new Error(`bytes${parsed.width} requires ${parsed.width} bytes, got ${raw.length}`);
|
|
2103
|
+
}
|
|
2104
|
+
return padRight32(raw);
|
|
2105
|
+
}
|
|
2106
|
+
case "array": {
|
|
2107
|
+
if (!Array.isArray(value)) {
|
|
2108
|
+
throw new Error(`array field expects array: ${typeof value}`);
|
|
2109
|
+
}
|
|
2110
|
+
if (parsed.size !== void 0 && value.length !== parsed.size) {
|
|
2111
|
+
throw new Error(`fixed array ${type} requires length ${parsed.size}, got ${value.length}`);
|
|
2112
|
+
}
|
|
2113
|
+
const chunks = value.map((v) => encodeField(parsed.inner, v, types));
|
|
2114
|
+
return (0, import_sha32.shake256)(concatBytesArr(chunks), { dkLen: DIGEST_LEN });
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
function hashStruct(primary, data, types) {
|
|
2119
|
+
const fields = types[primary];
|
|
2120
|
+
if (!fields) throw new Error(`unknown struct: ${primary}`);
|
|
2121
|
+
const expected = new Set(fields.map((f) => f.name));
|
|
2122
|
+
for (const k of Object.keys(data)) {
|
|
2123
|
+
if (!expected.has(k)) throw new Error(`unknown field in ${primary}: ${k}`);
|
|
2124
|
+
}
|
|
2125
|
+
const parts = [typeHash(primary, types)];
|
|
2126
|
+
for (const f of fields) {
|
|
2127
|
+
if (!(f.name in data)) throw new Error(`missing field ${primary}.${f.name}`);
|
|
2128
|
+
parts.push(encodeField(f.type, data[f.name], types));
|
|
2129
|
+
}
|
|
2130
|
+
return (0, import_sha32.shake256)(concatBytesArr(parts), { dkLen: DIGEST_LEN });
|
|
2131
|
+
}
|
|
2132
|
+
var RESERVED_DOMAIN_FIELDS = {
|
|
2133
|
+
name: "string",
|
|
2134
|
+
version: "string",
|
|
2135
|
+
chainId: "uint256",
|
|
2136
|
+
verifyingContract: "address",
|
|
2137
|
+
salt: "bytes32"
|
|
2138
|
+
};
|
|
2139
|
+
function validateDomainTypes(types) {
|
|
2140
|
+
const def = types.QRLDomain;
|
|
2141
|
+
if (!def) throw new Error("QRLDomain type is required");
|
|
2142
|
+
let hasName = false;
|
|
2143
|
+
for (const f of def) {
|
|
2144
|
+
const expected = RESERVED_DOMAIN_FIELDS[f.name];
|
|
2145
|
+
if (!expected) {
|
|
2146
|
+
throw new Error(
|
|
2147
|
+
`QRLDomain field "${f.name}" not in reserved set (name, version, chainId, verifyingContract, salt)`
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
if (f.type !== expected) {
|
|
2151
|
+
throw new Error(`QRLDomain field "${f.name}" must be type "${expected}", got "${f.type}"`);
|
|
2152
|
+
}
|
|
2153
|
+
if (f.name === "name") hasName = true;
|
|
2154
|
+
}
|
|
2155
|
+
if (!hasName) throw new Error("QRLDomain.name is required");
|
|
2156
|
+
}
|
|
2157
|
+
function validatePayloadReachability(primary, types) {
|
|
2158
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
2159
|
+
for (const root of ["QRLDomain", primary]) {
|
|
2160
|
+
for (const t of collectDependencies(root, types)) reachable.add(t);
|
|
2161
|
+
}
|
|
2162
|
+
for (const k of Object.keys(types)) {
|
|
2163
|
+
if (!reachable.has(k)) throw new Error(`unused referenced type: ${k}`);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
function computeTypedDataDigest(payload) {
|
|
2167
|
+
if (!payload || typeof payload !== "object") {
|
|
2168
|
+
throw new Error("invalid typed data payload");
|
|
2169
|
+
}
|
|
2170
|
+
validateTypeMap(payload.types);
|
|
2171
|
+
validateDomainTypes(payload.types);
|
|
2172
|
+
validatePayloadReachability(payload.primaryType, payload.types);
|
|
2173
|
+
const domainHash = hashStruct("QRLDomain", payload.domain, payload.types);
|
|
2174
|
+
const messageHash = hashStruct(payload.primaryType, payload.message, payload.types);
|
|
2175
|
+
return (0, import_sha32.shake256)(concatBytes(SCHEME_TAG_TYPED, domainHash, messageHash), { dkLen: DIGEST_LEN });
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// src/signing/verify.ts
|
|
2179
|
+
var mldsa = __toESM(require("@theqrl/mldsa87"));
|
|
2180
|
+
function bytesOrHex(v) {
|
|
2181
|
+
if (v instanceof Uint8Array) return v;
|
|
2182
|
+
if (typeof v === "string") return hexToBytes(v);
|
|
2183
|
+
throw new Error("expected Uint8Array or 0x-hex string");
|
|
2184
|
+
}
|
|
2185
|
+
function verifyMessage({
|
|
2186
|
+
signature,
|
|
2187
|
+
publicKey,
|
|
2188
|
+
messageBytes
|
|
2189
|
+
}) {
|
|
2190
|
+
try {
|
|
2191
|
+
const sig = bytesOrHex(signature);
|
|
2192
|
+
const pk = bytesOrHex(publicKey);
|
|
2193
|
+
const msg = bytesOrHex(messageBytes);
|
|
2194
|
+
const digest = computeMessageDigest(msg);
|
|
2195
|
+
return mldsa.cryptoSignVerify(sig, digest, pk, SCHEME_TAG_MSG);
|
|
2196
|
+
} catch {
|
|
2197
|
+
return false;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
function verifyTypedData({ signature, publicKey, payload }) {
|
|
2201
|
+
try {
|
|
2202
|
+
const sig = bytesOrHex(signature);
|
|
2203
|
+
const pk = bytesOrHex(publicKey);
|
|
2204
|
+
const digest = computeTypedDataDigest(payload);
|
|
2205
|
+
return mldsa.cryptoSignVerify(sig, digest, pk, SCHEME_TAG_TYPED);
|
|
2206
|
+
} catch {
|
|
2207
|
+
return false;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
1270
2210
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1271
2211
|
0 && (module.exports = {
|
|
2212
|
+
BLOB_LEN,
|
|
2213
|
+
CID_LEN,
|
|
1272
2214
|
ConnectionManager,
|
|
1273
2215
|
ConnectionStatus,
|
|
1274
2216
|
DEFAULT_RELAY_URL,
|
|
1275
|
-
|
|
2217
|
+
DIGEST_LEN,
|
|
2218
|
+
FP_LEN,
|
|
1276
2219
|
KeyExchange,
|
|
1277
2220
|
KeyExchangeMessageType,
|
|
1278
2221
|
MessageType,
|
|
2222
|
+
PROTOCOL_VERSION,
|
|
1279
2223
|
QRLConnect,
|
|
1280
2224
|
QRLConnectProvider,
|
|
2225
|
+
QRL_CONNECT_PROVIDER_INFO,
|
|
1281
2226
|
RESTRICTED_METHODS,
|
|
2227
|
+
SCHEME_TAG_MSG,
|
|
2228
|
+
SCHEME_TAG_TYPED,
|
|
2229
|
+
SCHEME_VERSION_MSG,
|
|
2230
|
+
SCHEME_VERSION_TYPED,
|
|
1282
2231
|
SocketClient,
|
|
1283
2232
|
UNRESTRICTED_METHODS,
|
|
2233
|
+
bytesToHex,
|
|
2234
|
+
cidFromString,
|
|
2235
|
+
cidRandom,
|
|
2236
|
+
cidToString,
|
|
2237
|
+
computeFingerprint,
|
|
2238
|
+
computeMessageDigest,
|
|
2239
|
+
computeTypedDataDigest,
|
|
2240
|
+
concatBytes,
|
|
2241
|
+
encodeField,
|
|
2242
|
+
encodeType,
|
|
2243
|
+
fingerprintEquals,
|
|
1284
2244
|
generateConnectionURI,
|
|
1285
2245
|
getAppStoreUrl,
|
|
2246
|
+
hashStruct,
|
|
2247
|
+
hexToBytes,
|
|
1286
2248
|
isMobileBrowser,
|
|
1287
|
-
parseConnectionURI
|
|
2249
|
+
parseConnectionURI,
|
|
2250
|
+
typeHash,
|
|
2251
|
+
verifyMessage,
|
|
2252
|
+
verifyTypedData
|
|
1288
2253
|
});
|