@unicitylabs/sphere-sdk 0.12.0-dev.1 → 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.
@@ -30,7 +30,7 @@ declare const HOST_READY_TIMEOUT = 30000;
30
30
  * JSON-RPC-like message types for wallet ↔ dApp communication.
31
31
  */
32
32
  declare const SPHERE_CONNECT_NAMESPACE = "sphere-connect";
33
- declare const SPHERE_CONNECT_VERSION = "2.0";
33
+ declare const SPHERE_CONNECT_VERSION = "2.1";
34
34
 
35
35
  declare const RPC_METHODS: {
36
36
  readonly GET_IDENTITY: "sphere_getIdentity";
@@ -83,12 +83,40 @@ declare const ERROR_CODES: {
83
83
  readonly RATE_LIMITED: 4006;
84
84
  readonly UNSUPPORTED_PROTOCOL_VERSION: 4007;
85
85
  readonly INCOMPATIBLE_NETWORK: 4008;
86
+ readonly WALLET_LOCKED: 4009;
86
87
  readonly INSUFFICIENT_BALANCE: 4100;
87
88
  readonly INVALID_RECIPIENT: 4101;
88
89
  readonly TRANSFER_FAILED: 4102;
89
90
  readonly INTENT_CANCELLED: 4200;
91
+ /**
92
+ * The intent was DELEGATED to the wallet and the host lost track of the answer — a host
93
+ * deadline fired, or the wallet locked / logged out mid-flight. **The outcome is UNKNOWN:
94
+ * the money may or may not have moved.**
95
+ *
96
+ * A dApp MUST NOT retry on this code. Reconcile out of band (poll the recipient, the
97
+ * aggregator, or your own backend) and only then decide.
98
+ *
99
+ * This code exists because every other answer would be a lie. `INTENT_CANCELLED` (4200)
100
+ * asserts the user declined and nothing happened; `WALLET_LOCKED` (4009) invites a retry
101
+ * after the unlock. Sending either for an intent the wallet had already submitted is how a
102
+ * paid-but-not-credited order — and then a double spend on retry — happens.
103
+ */
104
+ readonly INTENT_OUTCOME_UNKNOWN: 4201;
90
105
  };
91
106
  type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
107
+ /** `data` carried by every WALLET_LOCKED (4009) refusal. */
108
+ interface WalletLockedData {
109
+ /** Discriminator. Always the literal 'locked' — reserved for future refusal reasons. */
110
+ readonly reason: 'locked';
111
+ /**
112
+ * Whether the wallet's own unlock surface is currently on screen.
113
+ * DECLARED in the fail-fast release and always absent there; POPULATED in the resume
114
+ * release from ConnectHostConfig.unlockSurface(), evaluated at refusal time (visibility
115
+ * changes over time, so a static value would lie). Feeds
116
+ * ConnectClientConfig.onWalletAttention.
117
+ */
118
+ readonly unlockSurface?: 'visible' | 'background';
119
+ }
92
120
  interface SphereMessageBase {
93
121
  readonly ns: typeof SPHERE_CONNECT_NAMESPACE;
94
122
  readonly v: typeof SPHERE_CONNECT_VERSION;
@@ -145,6 +173,11 @@ interface SphereHandshake extends SphereMessageBase {
145
173
  readonly error?: SphereRpcError;
146
174
  /** Response: non-fatal deprecation notice (does not block the connection). */
147
175
  readonly warning?: SphereRpcError;
176
+ /** Response only: the wallet is LOCKED and the session is ALIVE. The dApp is connected
177
+ * and must not re-handshake; it will receive `wallet:unlocked` on the same session.
178
+ * Additive and safe for old clients: handleHandshakeResponse reads only sessionId,
179
+ * permissions, identity, network, warning and error and ignores unknown fields. */
180
+ readonly locked?: boolean;
148
181
  }
149
182
  interface SphereRpcError {
150
183
  readonly code: number;
@@ -169,14 +202,54 @@ interface PublicIdentity {
169
202
  * No sphere_subscribe call needed — host sends these unconditionally.
170
203
  */
171
204
  declare const WALLET_EVENTS: {
172
- /** Wallet locked or user logged out. dApp shows locked state and waits for unlock.
173
- * Pushed automatically by ConnectHost — no sphere_subscribe needed. */
205
+ /** Wallet is LOCKED — the session is STILL ALIVE. Requests are answered
206
+ * WALLET_LOCKED (4009) until `wallet:unlocked`. The dApp must NOT disconnect,
207
+ * must NOT clear its sessionId, and must NOT re-handshake.
208
+ * Payload: {@link WalletLockedPayload}. Pushed by ConnectHost.setLocked() and
209
+ * immediately after a handshake response carrying `locked: true`. */
174
210
  readonly LOCKED: "wallet:locked";
211
+ /** Wallet was unlocked — the SAME session continues: no re-handshake, no re-approval,
212
+ * no re-subscribe (the host re-arms the dApp's subscriptions before pushing this).
213
+ * Payload: {@link WalletUnlockedPayload} — carries the CURRENT identity, which may
214
+ * differ from the one the dApp connected with. Pushed by ConnectHost.updateSphere()
215
+ * on the locked -> live edge only. */
216
+ readonly UNLOCKED: "wallet:unlocked";
217
+ /** The session is GONE (logout, wallet deleted, dApp sphere_disconnect, expiry seen at
218
+ * unlock, a different seed behind the lock screen, host destroy).
219
+ * The dApp must clear its session and re-handshake to continue. Unlocking does not cure it.
220
+ * Payload: {@link WalletDisconnectedPayload}. Pushed by ConnectHost.revokeSession(). */
221
+ readonly DISCONNECTED: "wallet:disconnected";
175
222
  /** Active wallet address changed. dApp should update displayed identity.
176
223
  * Pushed automatically by ConnectHost — no sphere_subscribe needed. */
177
224
  readonly IDENTITY_CHANGED: "identity:changed";
178
225
  };
179
226
  type WalletEvent = (typeof WALLET_EVENTS)[keyof typeof WALLET_EVENTS];
227
+ /** Payload of {@link WALLET_EVENTS.LOCKED}. Intentionally empty — matches today's push,
228
+ * so an old dApp sees no shape change. */
229
+ type WalletLockedPayload = Record<string, never>;
230
+ /** Payload of {@link WALLET_EVENTS.DISCONNECTED}. Intentionally empty. */
231
+ type WalletDisconnectedPayload = Record<string, never>;
232
+ /** Payload of {@link WALLET_EVENTS.UNLOCKED}. */
233
+ interface WalletUnlockedPayload {
234
+ /** The wallet's public identity at unlock time. Absent when the rebound Sphere has no
235
+ * identity yet (JSON transports drop undefined keys).
236
+ * Unlock is NOT implicitly the same wallet: the lock screen's "Forgot password ->
237
+ * restore from recovery phrase" installs a DIFFERENT seed while origin approvals are
238
+ * keyed by origin alone. The host's own lock-edge guard is authoritative (it revokes
239
+ * instead of pushing this event on a mismatch); this field is what lets a dApp render
240
+ * honestly and what the client-side retry queue checks before draining. */
241
+ readonly identity?: PublicIdentity;
242
+ }
243
+ /** Payload of {@link WALLET_EVENTS.IDENTITY_CHANGED} — unchanged: the host forwards
244
+ * Sphere's own event data verbatim and pushes a PublicIdentity from updateSphere().
245
+ * Typed as the union for honesty. */
246
+ type WalletIdentityChangedPayload = PublicIdentity | unknown;
247
+ /** Events the host pushes unconditionally. They must NEVER be routed through
248
+ * `sphere_subscribe`: Sphere.on() accepts any string and would silently never emit,
249
+ * so the subscribe would succeed and deliver nothing forever. */
250
+ declare const AUTO_PUSHED_EVENTS: readonly WalletEvent[];
251
+ /** True for an event the host pushes unconditionally (see {@link AUTO_PUSHED_EVENTS}). */
252
+ declare function isAutoPushedEvent(event: string): event is WalletEvent;
180
253
  /** Check if a message belongs to the Sphere Connect protocol.
181
254
  * Handshakes are accepted at ANY version (the version decision happens in the
182
255
  * handshake handler so an incompatible peer gets a clean typed error instead of a
@@ -242,11 +315,56 @@ interface ConnectSession {
242
315
  readonly expiresAt: number;
243
316
  active: boolean;
244
317
  }
318
+ /**
319
+ * State of the host's binding to a Sphere instance. ORTHOGONAL to `session`:
320
+ * a locked wallet keeps its session, and a live wallet may have none.
321
+ *
322
+ * - 'live' — Sphere bound and usable.
323
+ * - 'locked' — the wallet is locked: the Sphere reference is DROPPED and the
324
+ * session is PRESERVED. Requests outside the allow-list answer
325
+ * WALLET_LOCKED (4009). Curable by updateSphere().
326
+ * - 'unavailable' — Sphere is gone for a NON-lock reason (a generic init failure leaves
327
+ * `sphere === null, isLocked === false`). Entering it revokes the
328
+ * session and pushes wallet:disconnected. NOT curable by unlocking.
329
+ */
330
+ type WalletState = 'live' | 'locked' | 'unavailable';
331
+ /** Context handed to {@link ConnectHostConfig.onLockedRequest}. Notify-only. */
332
+ interface LockedRequestContext {
333
+ /** ConnectHostConfig.origin, when the wallet supplied one. NEVER the dApp-claimed
334
+ * `session.dapp.url`. Absent = "a connected app"; make no claim you cannot verify. */
335
+ readonly origin?: string;
336
+ readonly kind: 'query' | 'intent' | 'handshake';
337
+ /** An RPC_METHODS value for 'query', an INTENT_ACTIONS value for 'intent',
338
+ * the literal 'handshake' for 'handshake'. */
339
+ readonly name: string;
340
+ }
341
+ /** 4th argument of {@link ConnectHostConfig.onIntent} (Connect 2.1). */
342
+ interface IntentContext {
343
+ readonly origin?: string;
344
+ /** Epoch ms after which the host answers on its own and aborts `signal`. */
345
+ readonly expiresAt: number;
346
+ /** Aborted when the host settles the intent for ANY reason (deadline, lock, revoke,
347
+ * unavailable, destroy). The wallet MUST dismiss its modal on abort. */
348
+ readonly signal: AbortSignal;
349
+ }
245
350
  interface ConnectHostConfig {
246
- /** Sphere SDK instance to bridge */
351
+ /** Sphere SDK instance to bridge. MAY be null (or omitted-as-null) when
352
+ * `initialWalletState` is 'locked' — the host never serves anything from a destroyed
353
+ * Sphere. A null sphere with initialWalletState 'live' is coerced to 'unavailable' with
354
+ * a logger.warn rather than throwing, because a throw here breaks the wallet's React
355
+ * mount. */
247
356
  sphere: unknown;
248
357
  /** Transport layer for communication */
249
358
  transport: ConnectTransport;
359
+ /**
360
+ * The origin this host serves, when the wallet knows it (e.g. 'https://app.example.com').
361
+ * OPTIONAL and it stays optional: with the credential prompt gone, NO security decision
362
+ * depends on it — it only labels a passive badge and log lines. Requiring it would break
363
+ * the nodejs/ mock wallet host and any WebSocket host, neither of which has a browser
364
+ * origin. When absent, the wallet's badge says "a connected app" and claims no origin.
365
+ * Never confuse it with `session.dapp.url`, which is dApp-CLAIMED metadata.
366
+ */
367
+ origin?: string;
250
368
  /** Called when dApp requests connection. Wallet shows approval UI.
251
369
  * When `silent` is true, the wallet must NOT open any UI — return rejected immediately if origin is unknown. */
252
370
  onConnectionRequest: (dapp: DAppMetadata, requestedPermissions: PermissionScope[], silent?: boolean, clientInfo?: {
@@ -257,8 +375,11 @@ interface ConnectHostConfig {
257
375
  approved: boolean;
258
376
  grantedPermissions: PermissionScope[];
259
377
  }>;
260
- /** Called when dApp sends an intent. Wallet opens corresponding UI. */
261
- onIntent: (action: string, params: Record<string, unknown>, session: ConnectSession) => Promise<{
378
+ /** Called when dApp sends an intent. Wallet opens corresponding UI.
379
+ * `ctx` (added in Connect 2.1) carries the host-side deadline and an AbortSignal:
380
+ * the wallet MUST dismiss its modal on abort, otherwise the host's own deadline
381
+ * manufactures the double-submit it was added to prevent. */
382
+ onIntent: (action: string, params: Record<string, unknown>, session: ConnectSession, ctx?: IntentContext) => Promise<{
262
383
  result?: unknown;
263
384
  error?: {
264
385
  code: number;
@@ -271,14 +392,47 @@ interface ConnectHostConfig {
271
392
  * in its UI. Does NOT affect the decision (the host already decided). `silent` is true for
272
393
  * auto-connect attempts — the wallet should not show UI for those. */
273
394
  onConnectionRejected?: (dapp: DAppMetadata | undefined, error: SphereRpcError, silent?: boolean) => void;
395
+ /**
396
+ * Notify-only: the host has just answered WALLET_LOCKED (4009) — or refused a handshake
397
+ * while locked. The host has ALREADY answered and never waits for this callback;
398
+ * throwing from it must not break the host (the host wraps the call in try/catch).
399
+ *
400
+ * THIS MUST NOT RAISE A CREDENTIAL SURFACE. A dApp request may trigger a CONSENT
401
+ * prompt; it may never trigger a password field. The wallet's only permitted reaction
402
+ * is a PASSIVE badge in its PERMANENT chrome ("N requests waiting — Unlock"); the
403
+ * password field appears only after a human clicks it. Volume is already bounded by
404
+ * checkRateLimit(), which now guards all three entry points — there is no second
405
+ * anti-spam mechanism, no coalescing, no cooldown and no cap by design.
406
+ *
407
+ * Also the natural telemetry seam.
408
+ */
409
+ onLockedRequest?: (ctx: LockedRequestContext) => void;
274
410
  /** Session time-to-live in ms. Default: 86400000 (24h). 0 = no expiry. */
275
411
  sessionTtlMs?: number;
412
+ /**
413
+ * The wallet-binding state the host starts in. Default: 'live'.
414
+ * Pass 'locked' when constructing a host while the wallet is already locked (cold start
415
+ * with an encrypted wallet: initialize() takes the classifyInitFailure === 'locked'
416
+ * branch and returns without setting Sphere) — otherwise the host starts 'live' with
417
+ * `sphere: null` and dereferences null on the first request.
418
+ * 'unavailable' is not constructible: use setUnavailable() after construction.
419
+ */
420
+ initialWalletState?: 'live' | 'locked';
276
421
  /** Optional secondary npm-SDK floor (rarely needed — the Connect protocol version is the era gate). */
277
422
  minSdkVersion?: string;
278
423
  /** Optional MINOR floor within the current Connect MAJOR. */
279
424
  minMinorVersion?: number;
280
425
  /** Max requests per second per session. Default: 20. */
281
426
  maxRequestsPerSecond?: number;
427
+ /** Host-side deadline for a query, in ms. Default: 25000. The host answers within it
428
+ * no matter what the router does. */
429
+ requestDeadlineMs?: number;
430
+ /** Host-side deadline for an intent, in ms. Default: 90000. Fires INTENT_CANCELLED
431
+ * (4200) AND aborts ctx.signal — it must cancel, not merely answer. */
432
+ intentDeadlineMs?: number;
433
+ /** Host-side deadline for onConnectionRequest, in ms. Default: 120000. A handshake
434
+ * carries no id, so expiry sends the empty refusal. */
435
+ handshakeDeadlineMs?: number;
282
436
  }
283
437
  interface ConnectClientConfig {
284
438
  /** Transport layer for communication */
@@ -305,6 +459,9 @@ interface ConnectResult {
305
459
  readonly sessionId: string;
306
460
  readonly permissions: PermissionScope[];
307
461
  readonly identity: PublicIdentity;
462
+ /** True when the wallet is locked but the session is alive — a resume during a lock,
463
+ * the most common entry into this feature. Requests answer 4009 until wallet:unlocked. */
464
+ readonly locked?: boolean;
308
465
  }
309
466
  type ConnectEventHandler = (data: unknown) => void;
310
467
 
@@ -317,7 +474,27 @@ type ConnectEventHandler = (data: unknown) => void;
317
474
  */
318
475
 
319
476
  declare class ConnectHost {
477
+ /** Null whenever _walletState is 'locked' or 'unavailable' (invariant B). */
320
478
  private sphere;
479
+ /** The wallet-binding axis. Underscored because `walletState` is the public getter.
480
+ * ORTHOGONAL to `session` — a locked wallet keeps its session, a live wallet may have
481
+ * none. Written only by the WALLET (setLocked / setUnavailable / updateSphere / destroy);
482
+ * `session` is written by the dApp handshake, sphere_disconnect and expiry. */
483
+ private _walletState;
484
+ /** Immutable public facts about the current binding. Refreshed on every bind
485
+ * (constructor, updateSphere); FROZEN by setLocked(); EMPTY after setUnavailable() and
486
+ * destroy(). Never read from Sphere while locked — that is a property of the types
487
+ * here, not of code review. */
488
+ private snapshot;
489
+ /** Subscription KEYS captured by setLocked() BEFORE the unsub closures are detached.
490
+ * Sphere.destroy() kills those closures, so the keys are the only recoverable
491
+ * information. Excludes 'identity:changed' (autoSubscribeIdentityChanged re-arms it).
492
+ * A Set, not an array: handleSubscribe may be called twice for the same key while
493
+ * locked. */
494
+ private suspendedSubscriptions;
495
+ /** Every accepted id, with its own host-side timer. The single convergence point for
496
+ * lock / revoke / unavailable / destroy / deadline. */
497
+ private readonly inFlight;
321
498
  private readonly transport;
322
499
  private readonly config;
323
500
  private session;
@@ -328,6 +505,16 @@ declare class ConnectHost {
328
505
  private rateLimitResetAt;
329
506
  private unsubscribeTransport;
330
507
  constructor(config: ConnectHostConfig);
508
+ /** The wallet-binding axis. Orthogonal to {@link getSession}. Read-only —
509
+ * transitions go through setLocked() / setUnavailable() / updateSphere(). */
510
+ get walletState(): WalletState;
511
+ /** Both axes in one read, for UI that must render "connected AND locked".
512
+ * Required by the wallet's ConnectPage, which today renders a green pulsing
513
+ * "Connected to {dapp}" with no regard for lock state. */
514
+ getState(): {
515
+ readonly walletState: WalletState;
516
+ readonly session: ConnectSession | null;
517
+ };
331
518
  /** Get current active session */
332
519
  getSession(): ConnectSession | null;
333
520
  /** Register an auto-approve handler for an intent action (session-scoped). */
@@ -341,19 +528,64 @@ declare class ConnectHost {
341
528
  /** Remove auto-approve for an intent action. */
342
529
  clearIntentAutoApprove(action: string): void;
343
530
  /**
344
- * Update the Sphere instance (e.g. user switched address — new Sphere created).
345
- * Re-subscribes auto-push events and notifies connected dApp of the new identity.
531
+ * Bind a (new) Sphere instance. This is BOTH the re-arm path after setLocked() /
532
+ * setUnavailable() AND the existing address-switch path in a live wallet.
533
+ *
534
+ * From 'live' (address switch): today's behaviour, unchanged — re-arm identity:changed,
535
+ * push identity:changed. NO identity comparison: an address switch is legal.
536
+ *
537
+ * On the 'locked' -> 'live' edge, in this order:
538
+ * 1. compare snapshot.identity?.chainPubkey with the new Sphere's chainPubkey.
539
+ * MISMATCH => revokeSession() (which pushes wallet:disconnected) and RETURN.
540
+ * Never wallet:unlocked. This is the "Forgot password -> restore recovery phrase
541
+ * installed a different seed behind an origin-keyed approval" guard.
542
+ * 2. session.expiresAt passed => revokeSession() and RETURN. A wallet:unlocked into a
543
+ * dead session would make the dApp's next request answer SESSION_EXPIRED 4004.
544
+ * 3. rebind, refresh the snapshot, go live, re-arm identity:changed, replay every
545
+ * suspended sphere_subscribe key, and ONLY THEN push wallet:unlocked with the
546
+ * CURRENT identity. Re-arm BEFORE push, so a dApp reacting synchronously cannot
547
+ * race its own event streams.
548
+ *
549
+ * From 'unavailable' -> 'live': rebind + refresh the snapshot, no identity check
550
+ * (nothing was bound to compare against) and no event (the session is already null).
346
551
  */
347
552
  updateSphere(newSphere: unknown): void;
348
- /** Revoke the current session */
349
- revokeSession(): void;
350
553
  /**
351
- * Notify connected dApp that wallet is locked/logged out, then revoke session.
352
- * Call this BEFORE destroy() when the wallet locks so the dApp gets a clean signal
353
- * instead of receiving NOT_CONNECTED errors on the next request.
554
+ * The wallet locked (manual lock, idle auto-lock, cross-tab broadcast, cold start).
555
+ * The session is PRESERVED — a lock is a state, not a teardown. Every request outside
556
+ * the locked allow-list is answered WALLET_LOCKED (4009) until updateSphere().
557
+ *
558
+ * Idempotent: a second call is a no-op and pushes nothing. Required, because
559
+ * SphereProvider.lock(), ConnectPage's `sphere → null` effect and broadcastLock()'s
560
+ * same-tab loopback can all fire it for one user action.
561
+ *
562
+ * ORDERING CONTRACT: call this BEFORE sphere.destroy(). The host drops its Sphere
563
+ * reference here; destroying first leaves in-flight requests reading a dead instance
564
+ * (-32603, or `undefined` returned AS SUCCESS from sphere_getIdentity).
565
+ */
566
+ setLocked(): void;
567
+ /**
568
+ * The Sphere instance is gone for a NON-LOCK reason (a generic init failure leaves
569
+ * `sphere === null, isLocked === false` in the wallet).
570
+ * This is a DEAD END: unlocking does not cure it, so it revokes the session and pushes
571
+ * wallet:disconnected rather than promising an unlock that cannot help.
572
+ * Subsequent requests answer NOT_CONNECTED (4001); handshakes get the empty refusal
573
+ * WITHOUT dereferencing a null Sphere.
574
+ *
575
+ * Idempotent. Pushes no 'wallet:unavailable' — there is no such event.
354
576
  */
355
- notifyWalletLocked(): void;
356
- /** Destroy the host, clean up all resources */
577
+ setUnavailable(): void;
578
+ /**
579
+ * Destroy the SESSION (logout, wallet deleted, dApp sphere_disconnect, popup
580
+ * beforeunload, expiry, identity mismatch at unlock). Pushes wallet:disconnected BEFORE
581
+ * tearing down, so the dApp stops believing it is connected instead of finding out at
582
+ * its next 4001.
583
+ *
584
+ * This is the TEARDOWN verb. For a lock use setLocked() — a lock never destroys the
585
+ * session. revokeSession() does NOT touch walletState: the two axes are orthogonal.
586
+ */
587
+ revokeSession(): void;
588
+ /** Destroy the host, clean up all resources. Idempotent. */
357
589
  destroy(): void;
358
590
  private handleMessage;
359
591
  private handleHandshake;
@@ -367,6 +599,39 @@ declare class ConnectHost {
367
599
  private cleanupEventSubscriptions;
368
600
  /** Push an event to the dApp without requiring a sphere_subscribe call. */
369
601
  private pushClientEvent;
602
+ /** The bound Sphere, or a typed refusal. The ONLY way the router may reach Sphere.
603
+ * Unreachable in practice — the gate guarantees 'live' before the router is entered —
604
+ * so this is defence in depth, not the primary mechanism. */
605
+ private requireSphere;
606
+ /** SNAPSHOT read. `undefined` means "we never saw an identity": callers MUST refuse,
607
+ * never answer undefined-as-success — a dApp reads that as "the wallet has no
608
+ * identity". Two explicit methods instead of one dual-mode method, so nobody can serve
609
+ * a snapshot believing it is live. */
610
+ private snapshotIdentity;
611
+ /** InFlightRegistry.onExpire sink. Filled in a later task; declared here so the
612
+ * constructor can wire it. */
613
+ private settleExpired;
614
+ /** Answer every request already in flight with one coded frame each. A request in flight
615
+ * when the Sphere goes away otherwise answers -32603 with a raw JS message, returns
616
+ * `undefined` AS SUCCESS (sphere_getIdentity), or — for a delegated intent — never
617
+ * answers at all until the client's own 120 s timeout. */
618
+ private settleInFlight;
619
+ /**
620
+ * Notify-only. The host has ALREADY answered and never waits for the wallet.
621
+ *
622
+ * The wallet's only permitted reaction is a PASSIVE badge in its PERMANENT chrome; a
623
+ * dApp request may trigger a CONSENT prompt but never a credential prompt. Volume is
624
+ * bounded by checkRateLimit(), which guards all three entry points — there is no
625
+ * coalescing, no cooldown and no cap by design.
626
+ *
627
+ * A throwing handler must not break the host.
628
+ */
629
+ private notifyLockedRequest;
630
+ /** Last-resort answer for a handler that threw before its own catch could run.
631
+ * Id-bearing frames get a coded error (InFlightRegistry guarantees exactly one answer
632
+ * per id); a handshake gets today's empty refusal, because a failed handshake must
633
+ * reveal nothing. */
634
+ private sendUnhandledError;
370
635
  private getPublicIdentity;
371
636
  private stripTokenSdkData;
372
637
  private sendResult;
@@ -406,6 +671,8 @@ declare class ConnectClient {
406
671
  private grantedPermissions;
407
672
  private identity;
408
673
  private walletNet;
674
+ private walletProto;
675
+ private locked;
409
676
  private connected;
410
677
  private pendingRequests;
411
678
  private eventHandlers;
@@ -426,6 +693,27 @@ declare class ConnectClient {
426
693
  get walletIdentity(): PublicIdentity | null;
427
694
  /** Wallet's active network, received during handshake. */
428
695
  get walletNetwork(): NetworkInfo | null;
696
+ /**
697
+ * The wallet's Connect protocol version, captured from the handshake response `v`.
698
+ * Null before the first handshake response and after a disconnect.
699
+ *
700
+ * Feature-detect with it: compare against '2.1' to decide whether the wallet can be
701
+ * trusted to send wallet:unlocked / wallet:disconnected. A Connect 2.0 wallet destroys
702
+ * the session on lock and never emits either, so a dApp waiting for them against one
703
+ * waits forever.
704
+ *
705
+ * CAVEAT: on an ERROR response the host echoes the dApp's own `v` back
706
+ * (ConnectHost.sendHandshakeResponse), so after a refused connection this may be the
707
+ * dApp's version rather than the wallet's. Only trust it after a successful handshake.
708
+ */
709
+ get walletProtocol(): string | null;
710
+ /**
711
+ * Whether the wallet was locked at the last handshake or lifecycle event.
712
+ * A locked client is still CONNECTED: `isConnected` stays true and `session` stays valid.
713
+ * Requests answer WALLET_LOCKED (4009) until `wallet:unlocked` arrives on the SAME
714
+ * session — do not disconnect, do not clear the session, do not re-handshake.
715
+ */
716
+ get walletLocked(): boolean;
429
717
  /** Send a query request and return the result */
430
718
  query<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
431
719
  /** Send an intent request. The wallet will open its UI for user confirmation. */
@@ -433,9 +721,10 @@ declare class ConnectClient {
433
721
  /** Subscribe to a wallet event. Returns unsubscribe function. */
434
722
  on(event: string, handler: ConnectEventHandler): () => void;
435
723
  private handleMessage;
724
+ private dispatchEvent;
436
725
  private handleHandshakeResponse;
437
726
  private handlePendingResponse;
438
727
  private cleanup;
439
728
  }
440
729
 
441
- export { ALL_PERMISSIONS, ConnectClient, type ConnectClientConfig, ConnectError, type ConnectEventHandler, ConnectHost, type ConnectHostConfig, type ConnectResult, type ConnectSession, type ConnectTransport, type DAppMetadata, DEFAULT_PERMISSIONS, ERROR_CODES, type ErrorCode, HOST_READY_TIMEOUT, HOST_READY_TYPE, INTENT_ACTIONS, INTENT_PERMISSIONS, type IntentAction, METHOD_PERMISSIONS, type NetworkInfo, PERMISSION_SCOPES, type PermissionScope, type PublicIdentity, RPC_METHODS, type RpcMethod, SPHERE_CONNECT_NAMESPACE, SPHERE_CONNECT_VERSION, SPHERE_NETWORKS, type SphereConnectMessage, type SphereEventMessage, type SphereHandshake, type SphereIntentRequest, type SphereIntentResult, type SphereRpcError, type SphereRpcRequest, type SphereRpcResponse, WALLET_EVENTS, type WalletEvent, createRequestId, hasIntentPermission, hasMethodPermission, isSphereConnectMessage, validatePermissions };
730
+ export { ALL_PERMISSIONS, AUTO_PUSHED_EVENTS, ConnectClient, type ConnectClientConfig, ConnectError, type ConnectEventHandler, ConnectHost, type ConnectHostConfig, type ConnectResult, type ConnectSession, type ConnectTransport, type DAppMetadata, DEFAULT_PERMISSIONS, ERROR_CODES, type ErrorCode, HOST_READY_TIMEOUT, HOST_READY_TYPE, INTENT_ACTIONS, INTENT_PERMISSIONS, type IntentAction, type IntentContext, type LockedRequestContext, METHOD_PERMISSIONS, type NetworkInfo, PERMISSION_SCOPES, type PermissionScope, type PublicIdentity, RPC_METHODS, type RpcMethod, SPHERE_CONNECT_NAMESPACE, SPHERE_CONNECT_VERSION, SPHERE_NETWORKS, type SphereConnectMessage, type SphereEventMessage, type SphereHandshake, type SphereIntentRequest, type SphereIntentResult, type SphereRpcError, type SphereRpcRequest, type SphereRpcResponse, WALLET_EVENTS, type WalletDisconnectedPayload, type WalletEvent, type WalletIdentityChangedPayload, type WalletLockedData, type WalletLockedPayload, type WalletState, type WalletUnlockedPayload, createRequestId, hasIntentPermission, hasMethodPermission, isAutoPushedEvent, isSphereConnectMessage, validatePermissions };