@cef-ai/wallet 1.0.0 → 1.1.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/dist/index.d.ts CHANGED
@@ -41,6 +41,8 @@ type HostToPopup = {
41
41
  appContext: AppContext;
42
42
  intent: 'register' | 'login';
43
43
  label?: string;
44
+ name?: string;
45
+ email?: string;
44
46
  } | {
45
47
  type: 'wallet:requestDelegation';
46
48
  id: string;
@@ -90,6 +92,12 @@ type PopupToHost = {
90
92
  code: WalletErrorCode;
91
93
  message: string;
92
94
  traceId?: string;
95
+ } | {
96
+ type: 'wallet:ui:show';
97
+ id: string;
98
+ } | {
99
+ type: 'wallet:ui:hide';
100
+ id: string;
93
101
  };
94
102
  /** Spec §5.5. Channel name: `scp-wallet-v2`. Wallet-origin tabs only. */
95
103
  type BroadcastChannelMessage = {
@@ -104,6 +112,8 @@ type BroadcastChannelMessage = {
104
112
  /** absolute Unix epoch ms when session keys expire */ expMs: number;
105
113
  } | {
106
114
  type: 'logout';
115
+ } | {
116
+ type: 'session-available';
107
117
  };
108
118
  declare const BROADCAST_CHANNEL_NAME = "scp-wallet-v2";
109
119
 
@@ -136,54 +146,20 @@ type PopupMessageHandler<T extends HostToPopup = HostToPopup> = (message: T, met
136
146
  origin: string;
137
147
  }) => Promise<void> | void;
138
148
 
139
- interface PopupTransportOptions {
140
- /** Expected origin of the wallet popup, e.g. 'https://wallet.example.com'. */
141
- walletOrigin: string;
142
- /** Defaults to `(url, features) => window.open(url, '_blank', features)`. */
143
- openPopup?: PopupOpener;
144
- /** Window object hosting the event listener. Defaults to `globalThis.window`. */
145
- hostWindow?: Window;
146
- /** Default per-request timeout. Defaults to 60_000 ms. */
147
- defaultTimeoutMs?: number;
148
- }
149
149
  /**
150
- * Host-side popup transport. Spec §5.
151
- *
152
- * Use one `PopupTransport` instance per `EmbedWallet`. The instance owns a
153
- * single popup window at a time, opens it on demand, auto-reopens on the next
154
- * `request()` after it's been closed, and correlates each in-flight request
155
- * by its message `id`.
156
- *
157
- * Wire handshake: every freshly opened popup must send `wallet:ready` before
158
- * this transport will post any `HostToPopup` envelope. Without the handshake,
159
- * `postMessage` may fire before the popup document has executed its `message`
160
- * listener and the message is dropped. Outbound messages are queued in
161
- * `outbox` while `popupReady === false`; `wallet:ready` flushes them.
150
+ * Host-side transport contract used by `EmbedWallet`. Both `PopupTransport`
151
+ * (separate window) and `IframeTransport` (in-page cross-origin iframe)
152
+ * implement it `EmbedWallet` depends only on this, never on a concrete
153
+ * transport's popup/iframe mechanics.
162
154
  */
163
- declare class PopupTransport {
164
- private readonly opts;
165
- private popup;
166
- private pending;
167
- private listener;
168
- private popupReady;
169
- private outbox;
170
- constructor(options: PopupTransportOptions);
155
+ interface WalletTransport {
171
156
  request<T extends PopupToHost>(message: HostToPopup, expectedTypes: T['type'][], init?: {
172
157
  timeoutMs?: number;
173
158
  }): Promise<T>;
174
- /** Close the popup if open. Pending requests will time out normally. */
159
+ /** Tear down the visible surface (popup window / iframe element); keep the instance reusable. */
175
160
  close(): void;
176
- /**
177
- * Tear down the transport entirely. Closes the popup AND removes the host-
178
- * window message listener. After `destroy()`, the transport instance cannot
179
- * be reused — callers should drop the reference.
180
- *
181
- * Use `close()` if you only want to close the popup but keep the transport
182
- * alive for a later re-open.
183
- */
161
+ /** Tear down entirely (surface + host-window listener); instance not reusable after. */
184
162
  destroy(): void;
185
- private ensureListening;
186
- private onMessage;
187
163
  }
188
164
 
189
165
  interface EmbedWalletOptions {
@@ -202,12 +178,21 @@ interface EmbedWalletOptions {
202
178
  };
203
179
  /** Optional name for the appContext sent via wallet:hello. Defaults to ''. */
204
180
  appName?: string;
181
+ /** Transport surface. 'iframe' (default) embeds the wallet inline; 'popup' opens a window. */
182
+ transport?: 'iframe' | 'popup';
205
183
  /**
206
184
  * Test seam. Injects an alternative `PopupTransport` instance. Production
207
185
  * callers leave this undefined and the SDK constructs its own transport.
208
186
  */
209
187
  __internal__?: {
210
- transport?: PopupTransport;
188
+ transport?: WalletTransport;
189
+ /**
190
+ * Test seam. Injects the `openPopup` used by the transient `PopupTransport`
191
+ * that `register()` opens when the main transport is not already a popup
192
+ * (see `EmbedWallet.register`). Production callers leave this undefined and
193
+ * the transient popup uses the real `window.open` default.
194
+ */
195
+ registerPopupOpener?: PopupOpener;
211
196
  };
212
197
  }
213
198
 
@@ -285,12 +270,23 @@ declare class EmbedWallet {
285
270
  private _credentialId;
286
271
  private _listeners;
287
272
  private readonly transport;
273
+ private readonly regPopupOpener?;
288
274
  constructor(opts: EmbedWalletOptions);
289
275
  get isLoggedIn(): boolean;
290
276
  get addresses(): Addresses | null;
291
277
  get credentialId(): string | null;
278
+ /**
279
+ * Safari blocks WebAuthn `create()` in cross-origin iframes, so `register()`
280
+ * always runs its `wallet:hello` ceremony over a `PopupTransport` — even
281
+ * when the configured transport is the iframe. When the main transport is
282
+ * already a `PopupTransport` it's reused; otherwise a transient one is
283
+ * opened synchronously (within the caller's click) and closed afterward.
284
+ * `login()` and all other ops use the configured transport unchanged.
285
+ */
292
286
  register(opts?: {
293
287
  label?: string;
288
+ name?: string;
289
+ email?: string;
294
290
  }): Promise<LoginResult>;
295
291
  login(): Promise<LoginResult>;
296
292
  logout(): Promise<void>;
@@ -348,11 +344,106 @@ declare const ALLOWED_ORIGIN_SCHEMES: readonly string[];
348
344
  */
349
345
  declare function isMatchingOrigin(a: string | undefined, b: string | undefined): boolean;
350
346
 
347
+ interface PopupTransportOptions {
348
+ /** Expected origin of the wallet popup, e.g. 'https://wallet.example.com'. */
349
+ walletOrigin: string;
350
+ /** Defaults to `(url, features) => window.open(url, '_blank', features)`. */
351
+ openPopup?: PopupOpener;
352
+ /** Window object hosting the event listener. Defaults to `globalThis.window`. */
353
+ hostWindow?: Window;
354
+ /** Default per-request timeout. Defaults to 60_000 ms. */
355
+ defaultTimeoutMs?: number;
356
+ }
357
+ /**
358
+ * Host-side popup transport. Spec §5.
359
+ *
360
+ * Use one `PopupTransport` instance per `EmbedWallet`. The instance owns a
361
+ * single popup window at a time, opens it on demand, auto-reopens on the next
362
+ * `request()` after it's been closed, and correlates each in-flight request
363
+ * by its message `id`.
364
+ *
365
+ * Wire handshake: every freshly opened popup must send `wallet:ready` before
366
+ * this transport will post any `HostToPopup` envelope. Without the handshake,
367
+ * `postMessage` may fire before the popup document has executed its `message`
368
+ * listener and the message is dropped. Outbound messages are queued in
369
+ * `outbox` while `popupReady === false`; `wallet:ready` flushes them.
370
+ */
371
+ declare class PopupTransport implements WalletTransport {
372
+ private readonly opts;
373
+ private popup;
374
+ private pending;
375
+ private listener;
376
+ private popupReady;
377
+ private outbox;
378
+ constructor(options: PopupTransportOptions);
379
+ request<T extends PopupToHost>(message: HostToPopup, expectedTypes: T['type'][], init?: {
380
+ timeoutMs?: number;
381
+ }): Promise<T>;
382
+ /**
383
+ * Close the popup if open. Immediately rejects any in-flight `request()`
384
+ * calls (mirrors `IframeTransport.close()`) rather than leaving them to
385
+ * time out — a caller that closes the popup mid-request (e.g.
386
+ * `EmbedWallet.register()`'s `finally` block) gets a prompt rejection
387
+ * instead of waiting out the full default 60s timeout.
388
+ */
389
+ close(): void;
390
+ /**
391
+ * Tear down the transport entirely. Closes the popup AND removes the host-
392
+ * window message listener. After `destroy()`, the transport instance cannot
393
+ * be reused — callers should drop the reference.
394
+ *
395
+ * Use `close()` if you only want to close the popup but keep the transport
396
+ * alive for a later re-open.
397
+ */
398
+ destroy(): void;
399
+ /** Reject every in-flight request with a `WalletError('internal', reason)`. Idempotent. */
400
+ private rejectAllPending;
401
+ private ensureListening;
402
+ private onMessage;
403
+ }
404
+
405
+ interface IframeTransportOptions {
406
+ walletOrigin: string;
407
+ hostWindow?: Window;
408
+ document?: Document;
409
+ defaultTimeoutMs?: number;
410
+ /** Test seam: build the iframe element (happy-dom's contentWindow is null). */
411
+ mountIframe?: (url: string, allow: string) => HTMLIFrameElement;
412
+ /** Test seam: observe overlay visibility transitions without relying on real DOM layout. */
413
+ onOverlay?: (visible: boolean) => void;
414
+ }
415
+ declare class IframeTransport implements WalletTransport {
416
+ private readonly opts;
417
+ private iframe;
418
+ private pending;
419
+ private listener;
420
+ private ready;
421
+ private outbox;
422
+ private backdrop;
423
+ private hiddenCssText;
424
+ private overlayVisible;
425
+ constructor(options: IframeTransportOptions);
426
+ private ensureIframe;
427
+ private showOverlay;
428
+ private hideOverlay;
429
+ private ensureListening;
430
+ request<T extends PopupToHost>(message: HostToPopup, expectedTypes: T['type'][], init?: {
431
+ timeoutMs?: number;
432
+ }): Promise<T>;
433
+ private postToIframe;
434
+ close(): void;
435
+ destroy(): void;
436
+ private rejectAllPending;
437
+ private onMessage;
438
+ }
439
+
351
440
  interface PopupHostBridgeOptions {
352
441
  /** Expected origin of the host page (the page that opened this popup). */
353
442
  hostOrigin: string;
354
443
  /** The window this bridge runs in. Defaults to `globalThis`. */
355
444
  popupWindow?: Window;
445
+ /** Which window to post PopupToHost messages to. 'opener' (popup, default) or 'parent' (iframe). */
446
+ replyTo?: 'opener' | 'parent';
356
447
  }
357
448
  /**
358
449
  * Popup-side bridge. Spec §5.
@@ -375,9 +466,9 @@ declare class PopupHostBridge {
375
466
  type: T;
376
467
  }>>): this;
377
468
  /**
378
- * Send a `PopupToHost` message back to the opener. The bridge does not
379
- * remember the host-window reference itself — it reads `globalThis.opener`
380
- * each call, so it survives popup re-opens.
469
+ * Send a `PopupToHost` message back to the opener or parent, depending on
470
+ * `replyTo`. The bridge does not remember the host-window reference itself —
471
+ * it reads the target window each call, so it survives popup re-opens.
381
472
  */
382
473
  send(message: PopupToHost): void;
383
474
  /**
@@ -410,18 +501,36 @@ interface BroadcastSessionShareOptions {
410
501
  */
411
502
  getSession: () => BroadcastSession | null;
412
503
  /**
413
- * Called when another tab broadcasts a `logout` message. The local handler
414
- * should wipe its own session.
504
+ * Called when another tab broadcasts `session-available` (a keyless,
505
+ * fire-and-forget "a session just got established" push — see
506
+ * `announce()`). Session-less listeners should react by issuing a fresh
507
+ * `request()`; this callback never receives key material itself.
415
508
  */
416
- onLogout?: () => void;
509
+ onAvailable?: () => void;
417
510
  }
418
511
  /**
419
512
  * Popup-side cross-tab session sharing. Spec §5.5.
420
513
  *
421
514
  * - `request()`: broadcast `request-session`, wait up to 200ms for an offer.
422
515
  * - `start()`: listen for incoming requests; reply with `offer-session` if we
423
- * have a non-expired session and `getSession()` returns it.
424
- * - `broadcastLogout()`: notify peers to wipe their sessions too.
516
+ * have a non-expired session and `getSession()` returns it. Also listens
517
+ * for `session-available` pushes (see `announce()`) and invokes
518
+ * `onAvailable` so a session-less peer can react without polling.
519
+ * - `broadcastLogout()`: notify peers to wipe their sessions too. NOTE: no
520
+ * `BroadcastSessionShare` call site wires a receive-side handler for this
521
+ * today — cross-tab logout is owned exclusively by `IdentityImpl` via its
522
+ * own `CrossTabSync` (`packages/identity/src/CrossTabSync.ts`), which
523
+ * shares this same channel name/message shape by convention. This method
524
+ * (and the `'logout'` message type) stays for public-API compatibility
525
+ * (`@cef-ai/wallet` is published) but intentionally has no paired
526
+ * `onLogout` option — wiring one would double-handle logout alongside
527
+ * `CrossTabSync`. See `ARCHITECTURE.md` Key invariants.
528
+ * - `announce()`: notify peers that THIS tab just established a session, so a
529
+ * session-less peer can immediately issue its own `request()` rather than
530
+ * waiting on a one-shot timer that may have already elapsed. Carries no key
531
+ * material — only `request()`/`offer-session` ever move keys, and only in
532
+ * direct response to an explicit request. This keeps the push trigger and
533
+ * the key transfer on two separate, independently-gated messages.
425
534
  *
426
535
  * Channel name is the spec-locked `scp-wallet-v2`. Only wallet-origin popups
427
536
  * join. Host pages do not participate.
@@ -439,14 +548,32 @@ declare class BroadcastSessionShare {
439
548
  request(opts?: {
440
549
  timeoutMs?: number;
441
550
  }): Promise<BroadcastSession | null>;
442
- /** Begin offering this tab's session in response to requests, and handle logouts. */
551
+ /**
552
+ * Begin offering this tab's session in response to requests, and invoke
553
+ * `onAvailable` on incoming `session-available` pushes. Callers that only
554
+ * care about `onAvailable` (no session to offer) still call `start()` —
555
+ * `getSession()` returning `null` simply means `request-session` replies
556
+ * are skipped.
557
+ */
443
558
  start(): void;
444
559
  stop(): void;
445
- /** Notify peers that the user logged out. Peers wipe their sessions. */
560
+ /**
561
+ * Notify peers that the user logged out. Kept for public-API compatibility;
562
+ * no `BroadcastSessionShare` instance currently handles the receive side
563
+ * (see class doc) — cross-tab logout is `CrossTabSync`'s responsibility.
564
+ */
446
565
  broadcastLogout(): void;
566
+ /**
567
+ * Notify peers that this tab just established a session (register/login
568
+ * completed). Carries NO key material — it is purely a push trigger that
569
+ * tells session-less peers to issue their own `request()`. Key material
570
+ * still only ever moves as an `offer-session` reply to an explicit
571
+ * `request-session`; `announce()` does not bypass that gate.
572
+ */
573
+ announce(): void;
447
574
  /** Free the underlying channel handle. Call from unload handlers. */
448
575
  close(): void;
449
576
  private onMessage;
450
577
  }
451
578
 
452
- export { ALLOWED_ORIGIN_SCHEMES, type AppContext, type ApplicationBody, type ApplicationFilter, type ApplicationRecord, BROADCAST_CHANNEL_NAME, type BroadcastChannelMessage, type BroadcastSession, BroadcastSessionShare, type BroadcastSessionShareOptions, type CefSigner, type DelegationRequest, type DelegationResult, type DelegationScope, EmbedWallet, type EmbedWalletOptions, type HostMessageListener, type HostToPopup, type LoginResult, type PopupHandle, PopupHostBridge, type PopupHostBridgeOptions, type PopupMessageHandler, type PopupOpener, type PopupToHost, PopupTransport, type PopupTransportOptions, type SignIntent, type SignerSpec, type SignerType, type WalletEvent, type WalletEventListener, isMatchingOrigin };
579
+ export { ALLOWED_ORIGIN_SCHEMES, type AppContext, type ApplicationBody, type ApplicationFilter, type ApplicationRecord, BROADCAST_CHANNEL_NAME, type BroadcastChannelMessage, type BroadcastSession, BroadcastSessionShare, type BroadcastSessionShareOptions, type CefSigner, type DelegationRequest, type DelegationResult, type DelegationScope, EmbedWallet, type EmbedWalletOptions, type HostMessageListener, type HostToPopup, IframeTransport, type IframeTransportOptions, type LoginResult, type PopupHandle, PopupHostBridge, type PopupHostBridgeOptions, type PopupMessageHandler, type PopupOpener, type PopupToHost, PopupTransport, type PopupTransportOptions, type SignIntent, type SignerSpec, type SignerType, type WalletEvent, type WalletEventListener, type WalletTransport, isMatchingOrigin };