@unicitylabs/sphere-sdk 0.12.0 → 0.13.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.
@@ -14,7 +14,7 @@ interface NetworkInfo {
14
14
  * JSON-RPC-like message types for wallet ↔ dApp communication.
15
15
  */
16
16
  declare const SPHERE_CONNECT_NAMESPACE = "sphere-connect";
17
- declare const SPHERE_CONNECT_VERSION = "2.0";
17
+ declare const SPHERE_CONNECT_VERSION = "2.1";
18
18
 
19
19
  interface SphereMessageBase {
20
20
  readonly ns: typeof SPHERE_CONNECT_NAMESPACE;
@@ -72,6 +72,11 @@ interface SphereHandshake extends SphereMessageBase {
72
72
  readonly error?: SphereRpcError;
73
73
  /** Response: non-fatal deprecation notice (does not block the connection). */
74
74
  readonly warning?: SphereRpcError;
75
+ /** Response only: the wallet is LOCKED and the session is ALIVE. The dApp is connected
76
+ * and must not re-handshake; it will receive `wallet:unlocked` on the same session.
77
+ * Additive and safe for old clients: handleHandshakeResponse reads only sessionId,
78
+ * permissions, identity, network, warning and error and ignores unknown fields. */
79
+ readonly locked?: boolean;
75
80
  }
76
81
  interface SphereRpcError {
77
82
  readonly code: number;
@@ -152,6 +157,9 @@ interface ConnectResult {
152
157
  readonly sessionId: string;
153
158
  readonly permissions: PermissionScope[];
154
159
  readonly identity: PublicIdentity;
160
+ /** True when the wallet is locked but the session is alive — a resume during a lock,
161
+ * the most common entry into this feature. Requests answer 4009 until wallet:unlocked. */
162
+ readonly locked?: boolean;
155
163
  }
156
164
  type ConnectEventHandler = (data: unknown) => void;
157
165
 
@@ -178,6 +186,8 @@ declare class ConnectClient {
178
186
  private grantedPermissions;
179
187
  private identity;
180
188
  private walletNet;
189
+ private walletProto;
190
+ private locked;
181
191
  private connected;
182
192
  private pendingRequests;
183
193
  private eventHandlers;
@@ -198,6 +208,27 @@ declare class ConnectClient {
198
208
  get walletIdentity(): PublicIdentity | null;
199
209
  /** Wallet's active network, received during handshake. */
200
210
  get walletNetwork(): NetworkInfo | null;
211
+ /**
212
+ * The wallet's Connect protocol version, captured from the handshake response `v`.
213
+ * Null before the first handshake response and after a disconnect.
214
+ *
215
+ * Feature-detect with it: compare against '2.1' to decide whether the wallet can be
216
+ * trusted to send wallet:unlocked / wallet:disconnected. A Connect 2.0 wallet destroys
217
+ * the session on lock and never emits either, so a dApp waiting for them against one
218
+ * waits forever.
219
+ *
220
+ * CAVEAT: on an ERROR response the host echoes the dApp's own `v` back
221
+ * (ConnectHost.sendHandshakeResponse), so after a refused connection this may be the
222
+ * dApp's version rather than the wallet's. Only trust it after a successful handshake.
223
+ */
224
+ get walletProtocol(): string | null;
225
+ /**
226
+ * Whether the wallet was locked at the last handshake or lifecycle event.
227
+ * A locked client is still CONNECTED: `isConnected` stays true and `session` stays valid.
228
+ * Requests answer WALLET_LOCKED (4009) until `wallet:unlocked` arrives on the SAME
229
+ * session — do not disconnect, do not clear the session, do not re-handshake.
230
+ */
231
+ get walletLocked(): boolean;
201
232
  /** Send a query request and return the result */
202
233
  query<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
203
234
  /** Send an intent request. The wallet will open its UI for user confirmation. */
@@ -205,6 +236,7 @@ declare class ConnectClient {
205
236
  /** Subscribe to a wallet event. Returns unsubscribe function. */
206
237
  on(event: string, handler: ConnectEventHandler): () => void;
207
238
  private handleMessage;
239
+ private dispatchEvent;
208
240
  private handleHandshakeResponse;
209
241
  private handlePendingResponse;
210
242
  private cleanup;
@@ -14,7 +14,7 @@ interface NetworkInfo {
14
14
  * JSON-RPC-like message types for wallet ↔ dApp communication.
15
15
  */
16
16
  declare const SPHERE_CONNECT_NAMESPACE = "sphere-connect";
17
- declare const SPHERE_CONNECT_VERSION = "2.0";
17
+ declare const SPHERE_CONNECT_VERSION = "2.1";
18
18
 
19
19
  interface SphereMessageBase {
20
20
  readonly ns: typeof SPHERE_CONNECT_NAMESPACE;
@@ -72,6 +72,11 @@ interface SphereHandshake extends SphereMessageBase {
72
72
  readonly error?: SphereRpcError;
73
73
  /** Response: non-fatal deprecation notice (does not block the connection). */
74
74
  readonly warning?: SphereRpcError;
75
+ /** Response only: the wallet is LOCKED and the session is ALIVE. The dApp is connected
76
+ * and must not re-handshake; it will receive `wallet:unlocked` on the same session.
77
+ * Additive and safe for old clients: handleHandshakeResponse reads only sessionId,
78
+ * permissions, identity, network, warning and error and ignores unknown fields. */
79
+ readonly locked?: boolean;
75
80
  }
76
81
  interface SphereRpcError {
77
82
  readonly code: number;
@@ -152,6 +157,9 @@ interface ConnectResult {
152
157
  readonly sessionId: string;
153
158
  readonly permissions: PermissionScope[];
154
159
  readonly identity: PublicIdentity;
160
+ /** True when the wallet is locked but the session is alive — a resume during a lock,
161
+ * the most common entry into this feature. Requests answer 4009 until wallet:unlocked. */
162
+ readonly locked?: boolean;
155
163
  }
156
164
  type ConnectEventHandler = (data: unknown) => void;
157
165
 
@@ -178,6 +186,8 @@ declare class ConnectClient {
178
186
  private grantedPermissions;
179
187
  private identity;
180
188
  private walletNet;
189
+ private walletProto;
190
+ private locked;
181
191
  private connected;
182
192
  private pendingRequests;
183
193
  private eventHandlers;
@@ -198,6 +208,27 @@ declare class ConnectClient {
198
208
  get walletIdentity(): PublicIdentity | null;
199
209
  /** Wallet's active network, received during handshake. */
200
210
  get walletNetwork(): NetworkInfo | null;
211
+ /**
212
+ * The wallet's Connect protocol version, captured from the handshake response `v`.
213
+ * Null before the first handshake response and after a disconnect.
214
+ *
215
+ * Feature-detect with it: compare against '2.1' to decide whether the wallet can be
216
+ * trusted to send wallet:unlocked / wallet:disconnected. A Connect 2.0 wallet destroys
217
+ * the session on lock and never emits either, so a dApp waiting for them against one
218
+ * waits forever.
219
+ *
220
+ * CAVEAT: on an ERROR response the host echoes the dApp's own `v` back
221
+ * (ConnectHost.sendHandshakeResponse), so after a refused connection this may be the
222
+ * dApp's version rather than the wallet's. Only trust it after a successful handshake.
223
+ */
224
+ get walletProtocol(): string | null;
225
+ /**
226
+ * Whether the wallet was locked at the last handshake or lifecycle event.
227
+ * A locked client is still CONNECTED: `isConnected` stays true and `session` stays valid.
228
+ * Requests answer WALLET_LOCKED (4009) until `wallet:unlocked` arrives on the SAME
229
+ * session — do not disconnect, do not clear the session, do not re-handshake.
230
+ */
231
+ get walletLocked(): boolean;
201
232
  /** Send a query request and return the result */
202
233
  query<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
203
234
  /** Send an intent request. The wallet will open its UI for user confirmation. */
@@ -205,6 +236,7 @@ declare class ConnectClient {
205
236
  /** Subscribe to a wallet event. Returns unsubscribe function. */
206
237
  on(event: string, handler: ConnectEventHandler): () => void;
207
238
  private handleMessage;
239
+ private dispatchEvent;
208
240
  private handleHandshakeResponse;
209
241
  private handlePendingResponse;
210
242
  private cleanup;
@@ -90,26 +90,6 @@ var logger = {
90
90
  }
91
91
  };
92
92
 
93
- // core/errors.ts
94
- var SphereError = class extends Error {
95
- code;
96
- cause;
97
- /**
98
- * #441 deferred-paid linkage: the transferId of the possibly-committed send,
99
- * stamped by `sendOnce` before the error leaves so a payment-request consumer
100
- * can durably journal request→transfer and resolve 'paid' only when that
101
- * transfer actually completes (never optimistically). Present ONLY on
102
- * possibly-committed outcomes ({@link isPossiblyCommittedSendOutcome}).
103
- */
104
- transferId;
105
- constructor(message, code, cause) {
106
- super(message);
107
- this.name = "SphereError";
108
- this.code = code;
109
- this.cause = cause;
110
- }
111
- };
112
-
113
93
  // connect/semver.ts
114
94
  function majorOf(v) {
115
95
  return parseInt(String(v).split(".")[0], 10);
@@ -308,7 +288,7 @@ var HOST_READY_TIMEOUT = 3e4;
308
288
 
309
289
  // connect/protocol.ts
310
290
  var SPHERE_CONNECT_NAMESPACE = "sphere-connect";
311
- var SPHERE_CONNECT_VERSION = "2.0";
291
+ var SPHERE_CONNECT_VERSION = "2.1";
312
292
  var RPC_METHODS = {
313
293
  GET_IDENTITY: "sphere_getIdentity",
314
294
  GET_BALANCE: "sphere_getBalance",
@@ -344,6 +324,78 @@ var INTENT_ACTIONS = {
344
324
  SET_AUTO_RETURN: "set_auto_return",
345
325
  MINT: "mint"
346
326
  };
327
+ var ERROR_CODES = {
328
+ // Standard JSON-RPC
329
+ PARSE_ERROR: -32700,
330
+ INVALID_REQUEST: -32600,
331
+ METHOD_NOT_FOUND: -32601,
332
+ INVALID_PARAMS: -32602,
333
+ INTERNAL_ERROR: -32603,
334
+ // Sphere Connect (4xxx)
335
+ NOT_CONNECTED: 4001,
336
+ PERMISSION_DENIED: 4002,
337
+ USER_REJECTED: 4003,
338
+ SESSION_EXPIRED: 4004,
339
+ ORIGIN_BLOCKED: 4005,
340
+ RATE_LIMITED: 4006,
341
+ UNSUPPORTED_PROTOCOL_VERSION: 4007,
342
+ // Connect MAJOR mismatch (incompatible era)
343
+ INCOMPATIBLE_NETWORK: 4008,
344
+ // dApp targets a different network than the wallet
345
+ // Wallet locked; THE SESSION IS STILL ALIVE. A QUERY may be retried after wallet:unlocked.
346
+ // An INTENT already delegated to the wallet is NEVER answered with this code — it gets
347
+ // INTENT_OUTCOME_UNKNOWN (4201) instead, because a retry could double-spend.
348
+ WALLET_LOCKED: 4009,
349
+ INSUFFICIENT_BALANCE: 4100,
350
+ INVALID_RECIPIENT: 4101,
351
+ TRANSFER_FAILED: 4102,
352
+ INTENT_CANCELLED: 4200,
353
+ /**
354
+ * The intent was DELEGATED to the wallet and the host lost track of the answer — a host
355
+ * deadline fired, or the wallet locked / logged out mid-flight. **The outcome is UNKNOWN:
356
+ * the money may or may not have moved.**
357
+ *
358
+ * A dApp MUST NOT retry on this code. Reconcile out of band (poll the recipient, the
359
+ * aggregator, or your own backend) and only then decide.
360
+ *
361
+ * This code exists because every other answer would be a lie. `INTENT_CANCELLED` (4200)
362
+ * asserts the user declined and nothing happened; `WALLET_LOCKED` (4009) invites a retry
363
+ * after the unlock. Sending either for an intent the wallet had already submitted is how a
364
+ * paid-but-not-credited order — and then a double spend on retry — happens.
365
+ */
366
+ INTENT_OUTCOME_UNKNOWN: 4201
367
+ };
368
+ var WALLET_EVENTS = {
369
+ /** Wallet is LOCKED — the session is STILL ALIVE. Requests are answered
370
+ * WALLET_LOCKED (4009) until `wallet:unlocked`. The dApp must NOT disconnect,
371
+ * must NOT clear its sessionId, and must NOT re-handshake.
372
+ * Payload: {@link WalletLockedPayload}. Pushed by ConnectHost.setLocked() and
373
+ * immediately after a handshake response carrying `locked: true`. */
374
+ LOCKED: "wallet:locked",
375
+ /** Wallet was unlocked — the SAME session continues: no re-handshake, no re-approval,
376
+ * no re-subscribe (the host re-arms the dApp's subscriptions before pushing this).
377
+ * Payload: {@link WalletUnlockedPayload} — carries the CURRENT identity, which may
378
+ * differ from the one the dApp connected with. Pushed by ConnectHost.updateSphere()
379
+ * on the locked -> live edge only. */
380
+ UNLOCKED: "wallet:unlocked",
381
+ /** The session is GONE (logout, wallet deleted, dApp sphere_disconnect, expiry seen at
382
+ * unlock, a different seed behind the lock screen, host destroy).
383
+ * The dApp must clear its session and re-handshake to continue. Unlocking does not cure it.
384
+ * Payload: {@link WalletDisconnectedPayload}. Pushed by ConnectHost.revokeSession(). */
385
+ DISCONNECTED: "wallet:disconnected",
386
+ /** Active wallet address changed. dApp should update displayed identity.
387
+ * Pushed automatically by ConnectHost — no sphere_subscribe needed. */
388
+ IDENTITY_CHANGED: "identity:changed"
389
+ };
390
+ var AUTO_PUSHED_EVENTS = [
391
+ WALLET_EVENTS.LOCKED,
392
+ WALLET_EVENTS.UNLOCKED,
393
+ WALLET_EVENTS.DISCONNECTED,
394
+ WALLET_EVENTS.IDENTITY_CHANGED
395
+ ];
396
+ function isAutoPushedEvent(event) {
397
+ return AUTO_PUSHED_EVENTS.includes(event);
398
+ }
347
399
  function isSphereConnectMessage(msg) {
348
400
  if (!msg || typeof msg !== "object") return false;
349
401
  const m = msg;
@@ -360,7 +412,7 @@ function createRequestId() {
360
412
  }
361
413
 
362
414
  // connect/version.ts
363
- var SDK_VERSION = "0.12.0";
415
+ var SDK_VERSION = "0.13.0";
364
416
 
365
417
  // connect/permissions.ts
366
418
  var PERMISSION_SCOPES = {
@@ -419,6 +471,37 @@ var INTENT_PERMISSIONS = {
419
471
  [INTENT_ACTIONS.MINT]: PERMISSION_SCOPES.MINT_REQUEST
420
472
  };
421
473
 
474
+ // connect/host/host-state.ts
475
+ var WALLET_LOCKED_MESSAGE = "Wallet is locked";
476
+ var NOT_CONNECTED_MESSAGE = "Not connected";
477
+ var LOCKED_ALLOWLIST = /* @__PURE__ */ new Set([
478
+ RPC_METHODS.GET_IDENTITY,
479
+ RPC_METHODS.SUBSCRIBE,
480
+ RPC_METHODS.UNSUBSCRIBE,
481
+ RPC_METHODS.DISCONNECT
482
+ ]);
483
+ var REFUSE_NOT_CONNECTED = {
484
+ kind: "refuse",
485
+ error: { code: ERROR_CODES.NOT_CONNECTED, message: NOT_CONNECTED_MESSAGE }
486
+ };
487
+ var REFUSE_LOCKED = {
488
+ kind: "refuse",
489
+ error: {
490
+ code: ERROR_CODES.WALLET_LOCKED,
491
+ message: WALLET_LOCKED_MESSAGE,
492
+ data: { reason: "locked" }
493
+ }
494
+ };
495
+
496
+ // connect/host/WalletSnapshot.ts
497
+ var EMPTY_WALLET_SNAPSHOT = Object.freeze({ capturedAt: 0 });
498
+
499
+ // connect/host/ConnectHost.ts
500
+ var CHANNEL_ONLY_CODES = /* @__PURE__ */ new Set([
501
+ ERROR_CODES.WALLET_LOCKED,
502
+ ERROR_CODES.NOT_CONNECTED
503
+ ]);
504
+
422
505
  // connect/client/ConnectClient.ts
423
506
  var ConnectError = class extends Error {
424
507
  constructor(message, code, data) {
@@ -443,6 +526,8 @@ var ConnectClient = class {
443
526
  grantedPermissions = [];
444
527
  identity = null;
445
528
  walletNet = null;
529
+ walletProto = null;
530
+ locked = false;
446
531
  connected = false;
447
532
  pendingRequests = /* @__PURE__ */ new Map();
448
533
  eventHandlers = /* @__PURE__ */ new Map();
@@ -515,12 +600,37 @@ var ConnectClient = class {
515
600
  get walletNetwork() {
516
601
  return this.walletNet;
517
602
  }
603
+ /**
604
+ * The wallet's Connect protocol version, captured from the handshake response `v`.
605
+ * Null before the first handshake response and after a disconnect.
606
+ *
607
+ * Feature-detect with it: compare against '2.1' to decide whether the wallet can be
608
+ * trusted to send wallet:unlocked / wallet:disconnected. A Connect 2.0 wallet destroys
609
+ * the session on lock and never emits either, so a dApp waiting for them against one
610
+ * waits forever.
611
+ *
612
+ * CAVEAT: on an ERROR response the host echoes the dApp's own `v` back
613
+ * (ConnectHost.sendHandshakeResponse), so after a refused connection this may be the
614
+ * dApp's version rather than the wallet's. Only trust it after a successful handshake.
615
+ */
616
+ get walletProtocol() {
617
+ return this.walletProto;
618
+ }
619
+ /**
620
+ * Whether the wallet was locked at the last handshake or lifecycle event.
621
+ * A locked client is still CONNECTED: `isConnected` stays true and `session` stays valid.
622
+ * Requests answer WALLET_LOCKED (4009) until `wallet:unlocked` arrives on the SAME
623
+ * session — do not disconnect, do not clear the session, do not re-handshake.
624
+ */
625
+ get walletLocked() {
626
+ return this.locked;
627
+ }
518
628
  // ===========================================================================
519
629
  // Query (read data)
520
630
  // ===========================================================================
521
631
  /** Send a query request and return the result */
522
632
  async query(method, params) {
523
- if (!this.connected) throw new SphereError("Not connected", "NOT_INITIALIZED");
633
+ if (!this.connected) throw new ConnectError("Not connected", ERROR_CODES.NOT_CONNECTED);
524
634
  const id = createRequestId();
525
635
  return new Promise((resolve, reject) => {
526
636
  const timer = setTimeout(() => {
@@ -530,7 +640,8 @@ var ConnectClient = class {
530
640
  this.pendingRequests.set(id, {
531
641
  resolve,
532
642
  reject,
533
- timer
643
+ timer,
644
+ kind: "query"
534
645
  });
535
646
  this.transport.send({
536
647
  ns: SPHERE_CONNECT_NAMESPACE,
@@ -547,17 +658,23 @@ var ConnectClient = class {
547
658
  // ===========================================================================
548
659
  /** Send an intent request. The wallet will open its UI for user confirmation. */
549
660
  async intent(action, params) {
550
- if (!this.connected) throw new SphereError("Not connected", "NOT_INITIALIZED");
661
+ if (!this.connected) throw new ConnectError("Not connected", ERROR_CODES.NOT_CONNECTED);
551
662
  const id = createRequestId();
552
663
  return new Promise((resolve, reject) => {
553
664
  const timer = setTimeout(() => {
554
665
  this.pendingRequests.delete(id);
555
- reject(new Error(`Intent timeout: ${action}`));
666
+ reject(
667
+ new ConnectError(
668
+ `Intent outcome unknown \u2014 do not retry; reconcile before acting: ${action}`,
669
+ ERROR_CODES.INTENT_OUTCOME_UNKNOWN
670
+ )
671
+ );
556
672
  }, this.intentTimeout);
557
673
  this.pendingRequests.set(id, {
558
674
  resolve,
559
675
  reject,
560
- timer
676
+ timer,
677
+ kind: "intent"
561
678
  });
562
679
  this.transport.send({
563
680
  ns: SPHERE_CONNECT_NAMESPACE,
@@ -576,7 +693,7 @@ var ConnectClient = class {
576
693
  on(event, handler) {
577
694
  if (!this.eventHandlers.has(event)) {
578
695
  this.eventHandlers.set(event, /* @__PURE__ */ new Set());
579
- if (this.connected) {
696
+ if (this.connected && !isAutoPushedEvent(event)) {
580
697
  this.query(RPC_METHODS.SUBSCRIBE, { event }).catch((err) => logger.debug("Connect", "Event subscription failed", err));
581
698
  }
582
699
  }
@@ -587,7 +704,7 @@ var ConnectClient = class {
587
704
  handlers.delete(handler);
588
705
  if (handlers.size === 0) {
589
706
  this.eventHandlers.delete(event);
590
- if (this.connected) {
707
+ if (this.connected && !isAutoPushedEvent(event)) {
591
708
  this.query(RPC_METHODS.UNSUBSCRIBE, { event }).catch((err) => logger.debug("Connect", "Event unsubscription failed", err));
592
709
  }
593
710
  }
@@ -611,15 +728,36 @@ var ConnectClient = class {
611
728
  return;
612
729
  }
613
730
  if (msg.type === "event") {
614
- const handlers = this.eventHandlers.get(msg.event);
615
- if (handlers) {
616
- for (const handler of handlers) {
617
- try {
618
- handler(msg.data);
619
- } catch (err) {
620
- logger.debug("Connect", "Event handler error", err);
621
- }
622
- }
731
+ if (!this.connected || !this.sessionId) {
732
+ logger.warn("Connect", `Ignoring wallet event before a session exists: ${msg.event}`);
733
+ return;
734
+ }
735
+ if (msg.event === WALLET_EVENTS.LOCKED) {
736
+ this.locked = true;
737
+ } else if (msg.event === WALLET_EVENTS.UNLOCKED) {
738
+ this.locked = false;
739
+ const identity = msg.data?.identity;
740
+ if (identity) this.identity = identity;
741
+ } else if (msg.event === WALLET_EVENTS.DISCONNECTED) {
742
+ this.connected = false;
743
+ } else if (msg.event === WALLET_EVENTS.IDENTITY_CHANGED) {
744
+ const data = msg.data;
745
+ if (data && typeof data.chainPubkey === "string") this.identity = data;
746
+ }
747
+ this.dispatchEvent(msg.event, msg.data);
748
+ if (msg.event === WALLET_EVENTS.DISCONNECTED) {
749
+ this.cleanup();
750
+ }
751
+ }
752
+ }
753
+ dispatchEvent(event, data) {
754
+ const handlers = this.eventHandlers.get(event);
755
+ if (!handlers) return;
756
+ for (const handler of handlers) {
757
+ try {
758
+ handler(data);
759
+ } catch (err) {
760
+ logger.debug("Connect", "Event handler error", err);
623
761
  }
624
762
  }
625
763
  }
@@ -627,6 +765,7 @@ var ConnectClient = class {
627
765
  if (!this.handshakeResolver) return;
628
766
  clearTimeout(this.handshakeResolver.timer);
629
767
  const m = msg;
768
+ this.walletProto = msg.v ?? null;
630
769
  if (m.error) {
631
770
  this.handshakeResolver.reject(new ConnectError(m.error.message, m.error.code, m.error.data));
632
771
  this.handshakeResolver = null;
@@ -637,12 +776,16 @@ var ConnectClient = class {
637
776
  this.grantedPermissions = msg.permissions;
638
777
  this.identity = msg.identity;
639
778
  this.walletNet = m.network ?? null;
779
+ this.locked = m.locked === true;
640
780
  this.connected = true;
641
781
  if (m.warning) logger.warn("Connect", "Wallet deprecation notice", m.warning.message);
642
782
  this.handshakeResolver.resolve({
643
783
  sessionId: msg.sessionId,
644
784
  permissions: this.grantedPermissions,
645
- identity: msg.identity
785
+ identity: msg.identity,
786
+ // A resume DURING a lock succeeds: the dApp is connected on the same session and
787
+ // must not re-handshake. It will get wallet:unlocked when the user unlocks.
788
+ ...this.locked ? { locked: true } : {}
646
789
  });
647
790
  } else {
648
791
  this.handshakeResolver.reject(new Error("Connection rejected by wallet"));
@@ -668,9 +811,19 @@ var ConnectClient = class {
668
811
  this.unsubscribeTransport();
669
812
  this.unsubscribeTransport = null;
670
813
  }
814
+ if (this.handshakeResolver) {
815
+ clearTimeout(this.handshakeResolver.timer);
816
+ this.handshakeResolver.reject(new ConnectError("Disconnected", ERROR_CODES.NOT_CONNECTED));
817
+ this.handshakeResolver = null;
818
+ }
671
819
  for (const [, pending] of this.pendingRequests) {
672
820
  clearTimeout(pending.timer);
673
- pending.reject(new Error("Disconnected"));
821
+ pending.reject(
822
+ pending.kind === "intent" ? new ConnectError(
823
+ "Intent outcome unknown \u2014 do not retry; reconcile before acting",
824
+ ERROR_CODES.INTENT_OUTCOME_UNKNOWN
825
+ ) : new ConnectError("Disconnected", ERROR_CODES.NOT_CONNECTED)
826
+ );
674
827
  }
675
828
  this.pendingRequests.clear();
676
829
  this.eventHandlers.clear();
@@ -679,11 +832,21 @@ var ConnectClient = class {
679
832
  this.grantedPermissions = [];
680
833
  this.identity = null;
681
834
  this.walletNet = null;
835
+ this.walletProto = null;
836
+ this.locked = false;
682
837
  }
683
838
  };
684
839
 
685
840
  // impl/browser/connect/PostMessageTransport.ts
686
841
  var POPUP_CLOSE_CHECK_INTERVAL = 1e3;
842
+ function originOf(targetOrigin) {
843
+ if (targetOrigin === "*") return null;
844
+ try {
845
+ return [new URL(targetOrigin).origin];
846
+ } catch {
847
+ return null;
848
+ }
849
+ }
687
850
  var PostMessageTransport = class _PostMessageTransport {
688
851
  targetWindow;
689
852
  targetOrigin;
@@ -697,6 +860,9 @@ var PostMessageTransport = class _PostMessageTransport {
697
860
  this.targetOrigin = targetOrigin;
698
861
  this.allowedOrigins = allowedOrigins ? new Set(allowedOrigins) : null;
699
862
  this.listener = (event) => {
863
+ if (event.source && event.source !== this.targetWindow) {
864
+ return;
865
+ }
700
866
  if (this.allowedOrigins && !this.allowedOrigins.has("*") && !this.allowedOrigins.has(event.origin)) {
701
867
  return;
702
868
  }
@@ -735,7 +901,8 @@ var PostMessageTransport = class _PostMessageTransport {
735
901
  static forClient(options) {
736
902
  const target = options?.target ?? window.parent;
737
903
  const targetOrigin = options?.targetOrigin ?? "*";
738
- const transport = new _PostMessageTransport(target, targetOrigin, null);
904
+ const allowedOrigins = originOf(targetOrigin);
905
+ const transport = new _PostMessageTransport(target, targetOrigin, allowedOrigins);
739
906
  if (options?.target && options.target !== window.parent) {
740
907
  transport.startPopupCloseDetection(options.target);
741
908
  }