@tezos-x/octez.connect-dapp 5.0.4 → 5.0.5

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.
@@ -88,6 +88,18 @@ export declare class DAppClient extends Client {
88
88
  private prepareRequest;
89
89
  private createStateSnapshot;
90
90
  private initUserID;
91
+ /**
92
+ * Disconnect the postMessage and P2P transports and drop all three instances.
93
+ *
94
+ * A dropped transport is not garbage. Its communication client keeps its
95
+ * `window` handlers (postMessage) or its matrix sync (P2P) until it is told to
96
+ * stop, so every path that used to just forget the instances left one more
97
+ * live client behind per connection. Every such path goes through here.
98
+ *
99
+ * WalletConnect is left to each call site: its session is shared across tabs
100
+ * and only the leader may close it, which the callers already handle.
101
+ */
102
+ private dropTransports;
91
103
  initInternalTransports(): Promise<void>;
92
104
  private initEvents;
93
105
  private onRelayerError;
@@ -302,5 +314,18 @@ export declare class DAppClient extends Client {
302
314
  private addOpenRequest;
303
315
  private sendNotificationWithAccessToken;
304
316
  private buildAccountInfosFromV4Fanout;
317
+ /**
318
+ * Is this message from the wallet behind the active account? Over P2P the
319
+ * senderId is already bound to the decrypting key (see handleResponse).
320
+ */
321
+ private isActiveWallet;
322
+ /** Is this message from a wallet we are paired with on this connection? */
323
+ private isPairedWallet;
324
+ /**
325
+ * A wallet-initiated disconnect. Strangers are ignored; a saved but inactive
326
+ * wallet only removes its own records; only the active wallet (or any paired
327
+ * wallet when no account is active) ends the session.
328
+ */
329
+ private onUnsolicitedDisconnect;
305
330
  private onNewAccount;
306
331
  }
@@ -157,11 +157,25 @@ class DAppClient extends octez_connect_core_1.Client {
157
157
  // normalized back to those same flat shapes — the wire dialect stays
158
158
  // invisible to integrators. Non-Tezos wrapped payloads keep the
159
159
  // pass-through of the generic permissionRequest/request API.
160
- const isWrapped = (0, octez_connect_core_1.usesWrappedMessages)(wireMessage.version);
160
+ // The wrapped dialect is identified by its payload, not by the absence of
161
+ // a flat marker: `wrapBeaconMessage` only ever emits
162
+ // { id, version, senderId, message }. Routing on the version alone dropped
163
+ // every flat message stamped with the wrapped dialect's number -- which is
164
+ // how a wallet built outside this SDK (or on <= 4.8.6, echoing the peer's
165
+ // version) sends its `disconnect` -- so handleDisconnect never ran and the
166
+ // dApp kept the account connected until the user disconnected a second
167
+ // time (#52). Testing for `.message` positively routes that message down
168
+ // the flat path, and still reads a non-conformant wallet's wrapped
169
+ // envelope as wrapped even when it carries a redundant top-level `type`.
170
+ const hasWrappedPayload = Boolean(wireMessage.message);
171
+ const isWrapped = (0, octez_connect_core_1.usesWrappedMessages)(wireMessage.version) && hasWrappedPayload;
161
172
  // Issue #33: a V3-versioned message can arrive without its wrapped payload.
162
- // Drop it safely instead of dereferencing an undefined payload, which would
163
- // throw an unhandled rejection inside the transport subscription callback.
164
- if (isWrapped && !wireMessage.message) {
173
+ // With no top-level `type` either it carries nothing to route on, so drop
174
+ // it instead of dereferencing an undefined payload, which would throw an
175
+ // unhandled rejection inside the transport subscription callback.
176
+ if ((0, octez_connect_core_1.usesWrappedMessages)(wireMessage.version) &&
177
+ !hasWrappedPayload &&
178
+ !('type' in wireMessage)) {
165
179
  logger.log('handleResponse', 'Received wrapped message with undefined payload; dropping', wireMessage);
166
180
  return;
167
181
  }
@@ -170,6 +184,13 @@ class DAppClient extends octez_connect_core_1.Client {
170
184
  : wireMessage;
171
185
  const message = normalized !== null && normalized !== void 0 ? normalized : wireMessage;
172
186
  const typedMessage = normalized !== null && normalized !== void 0 ? normalized : wireMessage.message;
187
+ if ((connectionInfo.origin === octez_connect_types_1.Origin.P2P &&
188
+ message.senderId !== (yield (0, octez_connect_core_1.getSenderId)(connectionInfo.id))) ||
189
+ (connectionInfo.origin === octez_connect_types_1.Origin.EXTENSION &&
190
+ !(yield this.isPairedWallet(message.senderId, connectionInfo)))) {
191
+ logger.warn('handleResponse', 'dropping message: senderId does not match its sender');
192
+ return;
193
+ }
173
194
  let appMetadata = normalized
174
195
  ? typedMessage.appMetadata
175
196
  : (_a = typedMessage.blockchainData) === null || _a === void 0 ? void 0 : _a.appMetadata;
@@ -217,7 +238,8 @@ class DAppClient extends octez_connect_core_1.Client {
217
238
  logger.log('handleResponse', 'Received message', message, connectionInfo);
218
239
  logger.log('### message ###', JSON.stringify(message));
219
240
  logger.log('### connectionInfo ###', connectionInfo);
220
- const handleDisconnect = () => __awaiter(this, void 0, void 0, function* () {
241
+ // endSession=false: a saved but inactive wallet removes only its own records.
242
+ const handleDisconnect = (...args_1) => __awaiter(this, [...args_1], void 0, function* (endSession = true) {
221
243
  var _a;
222
244
  this.analytics.track('event', 'DAppClient', 'Disconnect received from Wallet');
223
245
  const relevantTransport = connectionInfo.origin === octez_connect_types_1.Origin.P2P
@@ -233,11 +255,12 @@ class DAppClient extends octez_connect_core_1.Client {
233
255
  }
234
256
  }
235
257
  yield this.removeAccountsForPeerIds([message.senderId]);
258
+ if (!endSession) {
259
+ return;
260
+ }
236
261
  yield this.events.emit(events_1.BeaconEvent.CHANNEL_CLOSED);
237
262
  // Reset transport state so next requestPermissions() shows pairing modal
238
- this.postMessageTransport = undefined;
239
- this.p2pTransport = undefined;
240
- this.walletConnectTransport = undefined;
263
+ yield this.dropTransports();
241
264
  yield this.setTransport(undefined);
242
265
  yield this.setActivePeer(undefined);
243
266
  });
@@ -284,10 +307,15 @@ class DAppClient extends octez_connect_core_1.Client {
284
307
  }
285
308
  else {
286
309
  if (typedMessage.type === octez_connect_types_1.BeaconMessageType.Disconnect) {
287
- yield handleDisconnect();
310
+ yield this.onUnsolicitedDisconnect(message.senderId, connectionInfo, handleDisconnect);
288
311
  }
289
312
  else if (typedMessage.type === octez_connect_types_1.BeaconMessageType.ChangeAccountRequest) {
290
- yield this.onNewAccount(typedMessage, connectionInfo);
313
+ if (yield this.isActiveWallet(message.senderId, connectionInfo)) {
314
+ yield this.onNewAccount(typedMessage, connectionInfo);
315
+ }
316
+ else {
317
+ logger.warn('handleResponse', 'ignoring change account from a non-active wallet');
318
+ }
291
319
  }
292
320
  }
293
321
  if (this._transport.isResolved()) {
@@ -433,6 +461,30 @@ class DAppClient extends octez_connect_core_1.Client {
433
461
  this.storage.set(octez_connect_types_1.StorageKey.USER_ID, this.userId);
434
462
  });
435
463
  }
464
+ /**
465
+ * Disconnect the postMessage and P2P transports and drop all three instances.
466
+ *
467
+ * A dropped transport is not garbage. Its communication client keeps its
468
+ * `window` handlers (postMessage) or its matrix sync (P2P) until it is told to
469
+ * stop, so every path that used to just forget the instances left one more
470
+ * live client behind per connection. Every such path goes through here.
471
+ *
472
+ * WalletConnect is left to each call site: its session is shared across tabs
473
+ * and only the leader may close it, which the callers already handle.
474
+ */
475
+ dropTransports() {
476
+ return __awaiter(this, void 0, void 0, function* () {
477
+ const dropped = [this.postMessageTransport, this.p2pTransport].filter((transport) => transport !== undefined);
478
+ this.postMessageTransport = undefined;
479
+ this.p2pTransport = undefined;
480
+ this.walletConnectTransport = undefined;
481
+ // Best effort: a transport that fails to disconnect is still dropped, and
482
+ // the others are still disconnected.
483
+ yield Promise.all(dropped.map((transport) => transport.disconnect().catch((error) => {
484
+ logger.warn('dropTransports', error instanceof Error ? error.message : String(error));
485
+ })));
486
+ });
487
+ }
436
488
  initInternalTransports() {
437
489
  return __awaiter(this, void 0, void 0, function* () {
438
490
  var _a, _b;
@@ -546,6 +598,7 @@ class DAppClient extends octez_connect_core_1.Client {
546
598
  });
547
599
  return __awaiter(this, void 0, void 0, function* () {
548
600
  yield this.createStateSnapshot();
601
+ yield this.dropTransports();
549
602
  yield _super.destroy.call(this);
550
603
  });
551
604
  }
@@ -676,13 +729,19 @@ class DAppClient extends octez_connect_core_1.Client {
676
729
  });
677
730
  const abortHandler = () => __awaiter(this, void 0, void 0, function* () {
678
731
  logger.log('init', 'ABORTED');
732
+ // Popup closed: replies to the QR code / request it showed no longer count.
733
+ stopListening();
679
734
  this.sendMetrics('performance-metrics/save', yield this.buildPayload('connect', 'abort'));
680
735
  yield Promise.all([
681
- postMessageTransport.disconnect(),
682
- // p2pTransport.disconnect(), do not abort connection manually
683
- walletConnectTransport === null || walletConnectTransport === void 0 ? void 0 : walletConnectTransport.disconnect()
736
+ walletConnectTransport === null || walletConnectTransport === void 0 ? void 0 : walletConnectTransport.disconnect(),
737
+ // Disconnects postMessage and P2P -- `postMessageTransport` above
738
+ // is this.postMessageTransport, so dropTransports() already owns
739
+ // it -- and forgets all three. It also stops the P2P client
740
+ // started for the pairing QR: if its start is still pending,
741
+ // P2PCommunicationClient.start() notices the stop once the login
742
+ // completes and shuts it down.
743
+ this.dropTransports()
684
744
  ]);
685
- this.postMessageTransport = this.walletConnectTransport = this.p2pTransport = undefined;
686
745
  this._activeAccount.isResolved() && this.clearActiveAccount();
687
746
  this.events.emit(events_1.BeaconEvent.PAIR_ABORTED).catch((emitError) => console.warn(emitError));
688
747
  if (this._initPromiseReject) {
@@ -761,17 +820,13 @@ class DAppClient extends octez_connect_core_1.Client {
761
820
  }
762
821
  resetInvalidState() {
763
822
  return __awaiter(this, arguments, void 0, function* (emit = true) {
764
- var _a, _b;
765
823
  this.accountManager.removeAllAccounts();
766
824
  this._activeAccount = octez_connect_utils_1.ExposedPromise.resolve(undefined);
767
825
  this.storage.set(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT, undefined);
768
826
  emit && this.events.emit(events_1.BeaconEvent.INVALID_ACTIVE_ACCOUNT_STATE);
769
827
  !emit && this.hideUI(['alert']);
770
- yield Promise.all([
771
- (_a = this.postMessageTransport) === null || _a === void 0 ? void 0 : _a.disconnect(),
772
- (_b = this.walletConnectTransport) === null || _b === void 0 ? void 0 : _b.disconnect()
773
- ]);
774
- this.postMessageTransport = this.p2pTransport = this.walletConnectTransport = undefined;
828
+ const walletConnectTransport = this.walletConnectTransport;
829
+ yield Promise.all([walletConnectTransport === null || walletConnectTransport === void 0 ? void 0 : walletConnectTransport.disconnect(), this.dropTransports()]);
775
830
  yield this.setActivePeer(undefined);
776
831
  yield this.setTransport(undefined);
777
832
  this._initPromise = undefined;
@@ -805,7 +860,7 @@ class DAppClient extends octez_connect_core_1.Client {
805
860
  if (!this.debounceSetActiveAccount && transport instanceof octez_connect_transport_walletconnect_1.WalletConnectTransport) {
806
861
  this.debounceSetActiveAccount = true;
807
862
  this._initPromise = undefined;
808
- this.postMessageTransport = this.p2pTransport = this.walletConnectTransport = undefined;
863
+ yield this.dropTransports();
809
864
  if (this.multiTabChannel.isLeader() || (0, octez_connect_ui_1.isMobileOS)(window)) {
810
865
  yield transport.disconnect();
811
866
  this.openRequestsOtherTabs.clear();
@@ -1825,9 +1880,7 @@ class DAppClient extends octez_connect_core_1.Client {
1825
1880
  if (request.type === octez_connect_types_1.BeaconMessageType.PermissionRequest &&
1826
1881
  (yield this.getActiveAccount()) === undefined) {
1827
1882
  this._initPromise = undefined;
1828
- this.postMessageTransport = undefined;
1829
- this.p2pTransport = undefined;
1830
- this.walletConnectTransport = undefined;
1883
+ yield this.dropTransports();
1831
1884
  yield this.setTransport();
1832
1885
  yield this.setActivePeer();
1833
1886
  }
@@ -2414,22 +2467,11 @@ class DAppClient extends octez_connect_core_1.Client {
2414
2467
  }
2415
2468
  yield this.clearActiveAccount();
2416
2469
  if (!(transport instanceof octez_connect_transport_walletconnect_1.WalletConnectTransport)) {
2417
- try {
2418
- yield transport.disconnect();
2419
- }
2420
- catch (disconnectError) {
2421
- const message = disconnectError instanceof Error ? disconnectError.message : String(disconnectError);
2422
- if (typeof message === 'string' && message.includes('Syncing stopped manually')) {
2423
- logger.log('disconnect', 'Matrix sync stopped manually');
2424
- }
2425
- else {
2426
- throw disconnectError;
2427
- }
2428
- }
2470
+ yield transport.disconnect();
2429
2471
  }
2430
- this.postMessageTransport = undefined;
2431
- this.p2pTransport = undefined;
2432
- this.walletConnectTransport = undefined;
2472
+ // The active transport is already disconnected above; this stops the
2473
+ // other one (a second disconnect is a no-op) and drops the instances.
2474
+ yield this.dropTransports();
2433
2475
  yield this.setTransport();
2434
2476
  this._initPromise = undefined;
2435
2477
  this._initPromiseReject = undefined;
@@ -2545,6 +2587,61 @@ class DAppClient extends octez_connect_core_1.Client {
2545
2587
  })));
2546
2588
  });
2547
2589
  }
2590
+ /**
2591
+ * Is this message from the wallet behind the active account? Over P2P the
2592
+ * senderId is already bound to the decrypting key (see handleResponse).
2593
+ */
2594
+ isActiveWallet(senderId, connectionInfo) {
2595
+ return __awaiter(this, void 0, void 0, function* () {
2596
+ if (connectionInfo.origin === octez_connect_types_1.Origin.WALLETCONNECT) {
2597
+ return true;
2598
+ }
2599
+ yield this.activeAccountLoaded;
2600
+ const active = yield this.getActiveAccount();
2601
+ return Boolean(active &&
2602
+ active.senderId === senderId &&
2603
+ active.origin.type === connectionInfo.origin &&
2604
+ active.origin.id === connectionInfo.id);
2605
+ });
2606
+ }
2607
+ /** Is this message from a wallet we are paired with on this connection? */
2608
+ isPairedWallet(senderId, connectionInfo) {
2609
+ return __awaiter(this, void 0, void 0, function* () {
2610
+ var _a, _b;
2611
+ if (connectionInfo.origin === octez_connect_types_1.Origin.P2P) {
2612
+ const peers = (_a = (yield this.storage.get(octez_connect_types_1.StorageKey.TRANSPORT_P2P_PEERS_DAPP))) !== null && _a !== void 0 ? _a : [];
2613
+ return peers.some((peer) => peer.publicKey === connectionInfo.id);
2614
+ }
2615
+ if (connectionInfo.origin === octez_connect_types_1.Origin.EXTENSION) {
2616
+ const peers = (_b = (yield this.storage.get(octez_connect_types_1.StorageKey.TRANSPORT_POSTMESSAGE_PEERS_DAPP))) !== null && _b !== void 0 ? _b : [];
2617
+ for (const peer of peers) {
2618
+ if (peer.extensionId === connectionInfo.id &&
2619
+ (yield (0, octez_connect_core_1.getSenderId)(peer.publicKey)) === senderId) {
2620
+ return true;
2621
+ }
2622
+ }
2623
+ return false;
2624
+ }
2625
+ return true; // WalletConnect: unchanged, see isActiveWallet
2626
+ });
2627
+ }
2628
+ /**
2629
+ * A wallet-initiated disconnect. Strangers are ignored; a saved but inactive
2630
+ * wallet only removes its own records; only the active wallet (or any paired
2631
+ * wallet when no account is active) ends the session.
2632
+ */
2633
+ onUnsolicitedDisconnect(senderId, connectionInfo, handleDisconnect) {
2634
+ return __awaiter(this, void 0, void 0, function* () {
2635
+ if (!(yield this.isPairedWallet(senderId, connectionInfo))) {
2636
+ logger.warn('handleResponse', 'ignoring disconnect from an unknown wallet');
2637
+ return;
2638
+ }
2639
+ yield this.activeAccountLoaded;
2640
+ const active = yield this.getActiveAccount();
2641
+ const endSession = !active || (yield this.isActiveWallet(senderId, connectionInfo));
2642
+ yield handleDisconnect(endSession);
2643
+ });
2644
+ }
2548
2645
  onNewAccount(message, connectionInfo) {
2549
2646
  return __awaiter(this, void 0, void 0, function* () {
2550
2647
  var _a, _b, _c;
@@ -2555,6 +2652,15 @@ class DAppClient extends octez_connect_core_1.Client {
2555
2652
  throw new Error('PublicKey or Address must be defined');
2556
2653
  }
2557
2654
  const address = (_a = message.address) !== null && _a !== void 0 ? _a : (yield (0, octez_connect_utils_1.getAddressFromPublicKey)(publicKey));
2655
+ if (connectionInfo.origin !== octez_connect_types_1.Origin.WALLETCONNECT &&
2656
+ publicKey &&
2657
+ message.address &&
2658
+ message.walletType !== 'abstracted_account') {
2659
+ const derived = yield (0, octez_connect_utils_1.getAddressFromPublicKey)(publicKey).catch(() => undefined);
2660
+ if (derived && derived !== message.address) {
2661
+ throw new Error(`Address "${message.address}" does not match its public key`);
2662
+ }
2663
+ }
2558
2664
  if (!(0, octez_connect_utils_1.isValidAddress)(address)) {
2559
2665
  throw new Error(`Invalid address: "${address}"`);
2560
2666
  }