@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.js CHANGED
@@ -105,8 +105,15 @@ var PopupTransport = class {
105
105
  });
106
106
  });
107
107
  }
108
- /** Close the popup if open. Pending requests will time out normally. */
108
+ /**
109
+ * Close the popup if open. Immediately rejects any in-flight `request()`
110
+ * calls (mirrors `IframeTransport.close()`) rather than leaving them to
111
+ * time out — a caller that closes the popup mid-request (e.g.
112
+ * `EmbedWallet.register()`'s `finally` block) gets a prompt rejection
113
+ * instead of waiting out the full default 60s timeout.
114
+ */
109
115
  close() {
116
+ this.rejectAllPending("transport closed");
110
117
  if (this.popup && !this.popup.closed) {
111
118
  this.popup.close();
112
119
  }
@@ -130,6 +137,14 @@ var PopupTransport = class {
130
137
  }
131
138
  }
132
139
  // ---- internal -------------------------------------------------------------
140
+ /** Reject every in-flight request with a `WalletError('internal', reason)`. Idempotent. */
141
+ rejectAllPending(reason) {
142
+ for (const [id, pending] of this.pending) {
143
+ clearTimeout(pending.timer);
144
+ this.pending.delete(id);
145
+ pending.reject(new WalletError("internal", reason));
146
+ }
147
+ }
133
148
  ensureListening() {
134
149
  if (this.listener) return;
135
150
  this.listener = (event) => this.onMessage(event);
@@ -187,6 +202,173 @@ function defaultOpener(url, features) {
187
202
  const opened = w == null ? void 0 : w.open(url, "_blank", features);
188
203
  return opened;
189
204
  }
205
+ var IframeTransport = class {
206
+ constructor(options) {
207
+ this.iframe = null;
208
+ this.pending = /* @__PURE__ */ new Map();
209
+ this.listener = null;
210
+ this.ready = false;
211
+ this.outbox = [];
212
+ this.backdrop = null;
213
+ this.hiddenCssText = "";
214
+ this.overlayVisible = false;
215
+ this.opts = __spreadValues({
216
+ hostWindow: globalThis.window,
217
+ document: globalThis.document,
218
+ defaultTimeoutMs: 6e4,
219
+ mountIframe: options.mountIframe,
220
+ onOverlay: options.onOverlay
221
+ }, options);
222
+ }
223
+ ensureIframe() {
224
+ var _a, _b, _c;
225
+ if (this.iframe) return;
226
+ const hostOrigin = (_b = (_a = globalThis.location) == null ? void 0 : _a.origin) != null ? _b : "";
227
+ const url = `${this.opts.walletOrigin}/embed/wallet?origin=${encodeURIComponent(hostOrigin)}`;
228
+ const allow = `publickey-credentials-get ${this.opts.walletOrigin}`;
229
+ if (this.opts.mountIframe) {
230
+ this.iframe = this.opts.mountIframe(url, allow);
231
+ } else {
232
+ const el = this.opts.document.createElement("iframe");
233
+ el.src = url;
234
+ el.setAttribute("allow", allow);
235
+ el.style.cssText = "position:fixed;border:0;width:0;height:0;left:-9999px;";
236
+ this.opts.document.body.appendChild(el);
237
+ this.iframe = el;
238
+ }
239
+ this.hiddenCssText = (_c = this.iframe.style.cssText) != null ? _c : "";
240
+ this.ready = false;
241
+ this.outbox = [];
242
+ this.overlayVisible = false;
243
+ this.ensureListening();
244
+ }
245
+ showOverlay() {
246
+ var _a, _b;
247
+ if (this.overlayVisible) return;
248
+ this.overlayVisible = true;
249
+ if (this.iframe) {
250
+ const backdrop = this.opts.document.createElement("div");
251
+ backdrop.setAttribute("data-wallet-overlay-backdrop", "");
252
+ backdrop.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:2147483646;";
253
+ this.opts.document.body.appendChild(backdrop);
254
+ this.backdrop = backdrop;
255
+ this.iframe.style.cssText = "position:fixed;border:0;z-index:2147483647;width:min(420px,100vw);height:min(640px,100vh);left:50%;top:50%;transform:translate(-50%,-50%);";
256
+ }
257
+ (_b = (_a = this.opts).onOverlay) == null ? void 0 : _b.call(_a, true);
258
+ }
259
+ hideOverlay() {
260
+ var _a, _b, _c;
261
+ if (!this.overlayVisible) return;
262
+ this.overlayVisible = false;
263
+ (_a = this.backdrop) == null ? void 0 : _a.remove();
264
+ this.backdrop = null;
265
+ if (this.iframe) {
266
+ this.iframe.style.cssText = this.hiddenCssText;
267
+ }
268
+ (_c = (_b = this.opts).onOverlay) == null ? void 0 : _c.call(_b, false);
269
+ }
270
+ ensureListening() {
271
+ if (this.listener) return;
272
+ this.listener = (e) => this.onMessage(e);
273
+ this.opts.hostWindow.addEventListener("message", this.listener);
274
+ }
275
+ request(_0, _1) {
276
+ return __async(this, arguments, function* (message, expectedTypes, init = {}) {
277
+ var _a;
278
+ this.ensureIframe();
279
+ const id = message.id;
280
+ const timeoutMs = (_a = init.timeoutMs) != null ? _a : this.opts.defaultTimeoutMs;
281
+ return new Promise((resolve, reject) => {
282
+ const timer = setTimeout(() => {
283
+ if (this.pending.has(id)) {
284
+ this.pending.delete(id);
285
+ this.outbox = this.outbox.filter((m) => m.id !== id);
286
+ reject(new WalletError("internal", `request "${message.type}" (id=${id}) timed out after ${timeoutMs}ms`));
287
+ }
288
+ }, timeoutMs);
289
+ this.pending.set(id, { expectedTypes, resolve: (m) => resolve(m), reject, timer });
290
+ if (this.ready) this.postToIframe(message);
291
+ else this.outbox.push(message);
292
+ });
293
+ });
294
+ }
295
+ postToIframe(message) {
296
+ var _a, _b;
297
+ (_b = (_a = this.iframe) == null ? void 0 : _a.contentWindow) == null ? void 0 : _b.postMessage(message, this.opts.walletOrigin);
298
+ }
299
+ close() {
300
+ var _a, _b;
301
+ this.rejectAllPending("transport closed");
302
+ (_a = this.backdrop) == null ? void 0 : _a.remove();
303
+ this.backdrop = null;
304
+ this.overlayVisible = false;
305
+ (_b = this.iframe) == null ? void 0 : _b.remove();
306
+ this.iframe = null;
307
+ this.ready = false;
308
+ this.outbox = [];
309
+ }
310
+ destroy() {
311
+ this.close();
312
+ if (this.listener) {
313
+ this.opts.hostWindow.removeEventListener("message", this.listener);
314
+ this.listener = null;
315
+ }
316
+ }
317
+ rejectAllPending(reason) {
318
+ for (const [id, p] of this.pending) {
319
+ clearTimeout(p.timer);
320
+ this.pending.delete(id);
321
+ p.reject(new WalletError("internal", reason));
322
+ }
323
+ }
324
+ onMessage(event) {
325
+ const data = event.data;
326
+ if (!isMatchingOrigin(event.origin, this.opts.walletOrigin)) {
327
+ if (data && typeof data.id === "string" && this.pending.has(data.id)) {
328
+ const p2 = this.pending.get(data.id);
329
+ this.pending.delete(data.id);
330
+ clearTimeout(p2.timer);
331
+ p2.reject(
332
+ new WalletError(
333
+ "origin-mismatch",
334
+ `message from "${event.origin}" rejected (expected "${this.opts.walletOrigin}")`
335
+ )
336
+ );
337
+ }
338
+ return;
339
+ }
340
+ if (!data || typeof data.type !== "string" || typeof data.id !== "string") return;
341
+ if (data.type === "wallet:ready") {
342
+ if (this.ready) return;
343
+ this.ready = true;
344
+ const flush = this.outbox;
345
+ this.outbox = [];
346
+ for (const m of flush) this.postToIframe(m);
347
+ return;
348
+ }
349
+ if (data.type === "wallet:ui:show") {
350
+ this.showOverlay();
351
+ return;
352
+ }
353
+ if (data.type === "wallet:ui:hide") {
354
+ this.hideOverlay();
355
+ return;
356
+ }
357
+ const p = this.pending.get(data.id);
358
+ if (!p) return;
359
+ clearTimeout(p.timer);
360
+ this.pending.delete(data.id);
361
+ if (data.type === "wallet:error") {
362
+ p.reject(new WalletError(data.code, data.message, data.traceId));
363
+ return;
364
+ }
365
+ if (!p.expectedTypes.includes(data.type)) {
366
+ p.reject(new WalletError("internal", `received unexpected response type "${data.type}" for id "${data.id}"`));
367
+ return;
368
+ }
369
+ p.resolve(data);
370
+ }
371
+ };
190
372
 
191
373
  // src/EmbedWallet.ts
192
374
  var EmbedWallet = class {
@@ -194,7 +376,7 @@ var EmbedWallet = class {
194
376
  this._addresses = null;
195
377
  this._credentialId = null;
196
378
  this._listeners = {};
197
- var _a, _b, _c, _d, _e, _f, _g;
379
+ var _a, _b, _c, _d, _e, _f, _g, _h;
198
380
  const injectedTransport = (_a = opts.__internal__) == null ? void 0 : _a.transport;
199
381
  if (!injectedTransport && !opts.walletOrigin) {
200
382
  throw new Error("EmbedWallet: `walletOrigin` is required (e.g. https://wallet.example.com).");
@@ -203,9 +385,8 @@ var EmbedWallet = class {
203
385
  this.walletOrigin = (_b = opts.walletOrigin) != null ? _b : "";
204
386
  this.popup = { width: (_d = (_c = opts.popup) == null ? void 0 : _c.width) != null ? _d : 420, height: (_f = (_e = opts.popup) == null ? void 0 : _e.height) != null ? _f : 640 };
205
387
  this.appName = (_g = opts.appName) != null ? _g : "";
206
- this.transport = injectedTransport != null ? injectedTransport : new PopupTransport({
207
- walletOrigin: this.walletOrigin
208
- });
388
+ this.regPopupOpener = (_h = opts.__internal__) == null ? void 0 : _h.registerPopupOpener;
389
+ this.transport = injectedTransport != null ? injectedTransport : opts.transport === "popup" ? new PopupTransport({ walletOrigin: this.walletOrigin }) : new IframeTransport({ walletOrigin: this.walletOrigin });
209
390
  }
210
391
  get isLoggedIn() {
211
392
  return this._addresses !== null;
@@ -216,9 +397,27 @@ var EmbedWallet = class {
216
397
  get credentialId() {
217
398
  return this._credentialId;
218
399
  }
400
+ /**
401
+ * Safari blocks WebAuthn `create()` in cross-origin iframes, so `register()`
402
+ * always runs its `wallet:hello` ceremony over a `PopupTransport` — even
403
+ * when the configured transport is the iframe. When the main transport is
404
+ * already a `PopupTransport` it's reused; otherwise a transient one is
405
+ * opened synchronously (within the caller's click) and closed afterward.
406
+ * `login()` and all other ops use the configured transport unchanged.
407
+ */
219
408
  register() {
220
409
  return __async(this, arguments, function* (opts = {}) {
221
- return this.helloAndAwaitLogin("register", opts);
410
+ if (this.transport instanceof PopupTransport) {
411
+ return this.helloAndAwaitLogin("register", opts);
412
+ }
413
+ const popup = new PopupTransport(__spreadValues({
414
+ walletOrigin: this.walletOrigin
415
+ }, this.regPopupOpener ? { openPopup: this.regPopupOpener } : {}));
416
+ try {
417
+ return yield this.helloAndAwaitLogin("register", opts, popup);
418
+ } finally {
419
+ popup.close();
420
+ }
222
421
  });
223
422
  }
224
423
  login() {
@@ -377,16 +576,18 @@ var EmbedWallet = class {
377
576
  return { appId: this.appId, name: this.appName, origin };
378
577
  }
379
578
  helloAndAwaitLogin(_0) {
380
- return __async(this, arguments, function* (intent, opts = {}) {
579
+ return __async(this, arguments, function* (intent, opts = {}, transport = this.transport) {
381
580
  const id = crypto.randomUUID();
382
581
  const helloMessage = {
383
582
  type: "wallet:hello",
384
583
  id,
385
584
  appContext: this.appContext(),
386
585
  intent,
387
- label: opts.label
586
+ label: opts.label,
587
+ name: opts.name,
588
+ email: opts.email
388
589
  };
389
- const result = yield this.transport.request(helloMessage, [
590
+ const result = yield transport.request(helloMessage, [
390
591
  "wallet:login:ok"
391
592
  ]);
392
593
  this._addresses = result.addresses;
@@ -419,7 +620,8 @@ var PopupHostBridge = class {
419
620
  this.handlers = {};
420
621
  this.listener = null;
421
622
  this.opts = __spreadValues({
422
- popupWindow: globalThis.window
623
+ popupWindow: globalThis.window,
624
+ replyTo: "opener"
423
625
  }, options);
424
626
  }
425
627
  /** Register a handler for a specific message type. Replaces any prior handler. */
@@ -428,16 +630,16 @@ var PopupHostBridge = class {
428
630
  return this;
429
631
  }
430
632
  /**
431
- * Send a `PopupToHost` message back to the opener. The bridge does not
432
- * remember the host-window reference itself — it reads `globalThis.opener`
433
- * each call, so it survives popup re-opens.
633
+ * Send a `PopupToHost` message back to the opener or parent, depending on
634
+ * `replyTo`. The bridge does not remember the host-window reference itself —
635
+ * it reads the target window each call, so it survives popup re-opens.
434
636
  */
435
637
  send(message) {
436
- const opener = globalThis.opener;
437
- if (!opener || typeof opener.postMessage !== "function") {
638
+ const target = this.opts.replyTo === "parent" ? globalThis.parent : globalThis.opener;
639
+ if (!target || typeof target.postMessage !== "function") {
438
640
  return;
439
641
  }
440
- opener.postMessage(message, this.opts.hostOrigin);
642
+ target.postMessage(message, this.opts.hostOrigin);
441
643
  }
442
644
  /**
443
645
  * Start listening for inbound messages.
@@ -523,8 +725,8 @@ var BroadcastSessionShare = class {
523
725
  });
524
726
  return;
525
727
  }
526
- if (msg.type === "logout") {
527
- (_b = (_a = this.opts).onLogout) == null ? void 0 : _b.call(_a);
728
+ if (msg.type === "session-available") {
729
+ (_b = (_a = this.opts).onAvailable) == null ? void 0 : _b.call(_a);
528
730
  return;
529
731
  }
530
732
  };
@@ -563,7 +765,13 @@ var BroadcastSessionShare = class {
563
765
  });
564
766
  });
565
767
  }
566
- /** Begin offering this tab's session in response to requests, and handle logouts. */
768
+ /**
769
+ * Begin offering this tab's session in response to requests, and invoke
770
+ * `onAvailable` on incoming `session-available` pushes. Callers that only
771
+ * care about `onAvailable` (no session to offer) still call `start()` —
772
+ * `getSession()` returning `null` simply means `request-session` replies
773
+ * are skipped.
774
+ */
567
775
  start() {
568
776
  if (this.listening) return;
569
777
  this.listening = true;
@@ -573,10 +781,24 @@ var BroadcastSessionShare = class {
573
781
  this.channel.removeEventListener("message", this.onMessage);
574
782
  this.listening = false;
575
783
  }
576
- /** Notify peers that the user logged out. Peers wipe their sessions. */
784
+ /**
785
+ * Notify peers that the user logged out. Kept for public-API compatibility;
786
+ * no `BroadcastSessionShare` instance currently handles the receive side
787
+ * (see class doc) — cross-tab logout is `CrossTabSync`'s responsibility.
788
+ */
577
789
  broadcastLogout() {
578
790
  this.channel.postMessage({ type: "logout" });
579
791
  }
792
+ /**
793
+ * Notify peers that this tab just established a session (register/login
794
+ * completed). Carries NO key material — it is purely a push trigger that
795
+ * tells session-less peers to issue their own `request()`. Key material
796
+ * still only ever moves as an `offer-session` reply to an explicit
797
+ * `request-session`; `announce()` does not bypass that gate.
798
+ */
799
+ announce() {
800
+ this.channel.postMessage({ type: "session-available" });
801
+ }
580
802
  /** Free the underlying channel handle. Call from unload handlers. */
581
803
  close() {
582
804
  this.stop();
@@ -584,6 +806,6 @@ var BroadcastSessionShare = class {
584
806
  }
585
807
  };
586
808
 
587
- export { ALLOWED_ORIGIN_SCHEMES, BROADCAST_CHANNEL_NAME, BroadcastSessionShare, EmbedWallet, PopupHostBridge, PopupTransport, isMatchingOrigin };
809
+ export { ALLOWED_ORIGIN_SCHEMES, BROADCAST_CHANNEL_NAME, BroadcastSessionShare, EmbedWallet, IframeTransport, PopupHostBridge, PopupTransport, isMatchingOrigin };
588
810
  //# sourceMappingURL=index.js.map
589
811
  //# sourceMappingURL=index.js.map