@bounded-sh/client 0.0.96 → 0.0.98

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.
Files changed (54) hide show
  1. package/dist/auth/turnkey-auth.d.ts +9 -7
  2. package/dist/bounded-widget-modal-C0Z7gmxH.mjs +31 -0
  3. package/dist/bounded-widget-modal-C0Z7gmxH.mjs.map +1 -0
  4. package/dist/bounded-widget-modal-s0PL86Y-.js +33 -0
  5. package/dist/bounded-widget-modal-s0PL86Y-.js.map +1 -0
  6. package/dist/{email-auth-provider-B1wYw68c.js → email-auth-provider-BhYzRN0r.js} +2 -2
  7. package/dist/{email-auth-provider-B1wYw68c.js.map → email-auth-provider-BhYzRN0r.js.map} +1 -1
  8. package/dist/{email-auth-provider-BgxLdrbB.mjs → email-auth-provider-CiPAD3vz.mjs} +2 -2
  9. package/dist/{email-auth-provider-Dhu15el8.mjs.map → email-auth-provider-CiPAD3vz.mjs.map} +1 -1
  10. package/dist/{email-auth-provider-Dhu15el8.mjs → email-auth-provider-DF7Urh8i.mjs} +2 -2
  11. package/dist/{email-auth-provider-BgxLdrbB.mjs.map → email-auth-provider-DF7Urh8i.mjs.map} +1 -1
  12. package/dist/{email-auth-provider-CiMNpEWJ.js → email-auth-provider-DxB_pJKO.js} +2 -2
  13. package/dist/{email-auth-provider-CiMNpEWJ.js.map → email-auth-provider-DxB_pJKO.js.map} +1 -1
  14. package/dist/{guest-auth-provider-BrMhyIse.js → guest-auth-provider-B9C3Iub_.js} +2 -2
  15. package/dist/{guest-auth-provider-BrMhyIse.js.map → guest-auth-provider-B9C3Iub_.js.map} +1 -1
  16. package/dist/{guest-auth-provider-BGBcSwsM.js → guest-auth-provider-BiM35KHb.js} +2 -2
  17. package/dist/{guest-auth-provider-BGBcSwsM.js.map → guest-auth-provider-BiM35KHb.js.map} +1 -1
  18. package/dist/{guest-auth-provider-U-zsTm0z.mjs → guest-auth-provider-DVw-tITC.mjs} +2 -2
  19. package/dist/{guest-auth-provider-U-zsTm0z.mjs.map → guest-auth-provider-DVw-tITC.mjs.map} +1 -1
  20. package/dist/{guest-auth-provider-BXJ2xm3C.mjs → guest-auth-provider-Dtsq4QaY.mjs} +2 -2
  21. package/dist/{guest-auth-provider-BXJ2xm3C.mjs.map → guest-auth-provider-Dtsq4QaY.mjs.map} +1 -1
  22. package/dist/{index-DWUx-v0z.js → index-CYdYsf8s.js} +514 -662
  23. package/dist/index-CYdYsf8s.js.map +1 -0
  24. package/dist/{index-B5Luqobz.mjs → index-Cflemdaf.mjs} +514 -662
  25. package/dist/index-Cflemdaf.mjs.map +1 -0
  26. package/dist/index.js +1 -1
  27. package/dist/index.mjs +1 -1
  28. package/dist/{index.native-DVMCCbwq.js → index.native-BZOWS_sR.js} +16 -332
  29. package/dist/index.native-BZOWS_sR.js.map +1 -0
  30. package/dist/{index.native-CAkdhYaP.mjs → index.native-gVbAsohv.mjs} +16 -332
  31. package/dist/index.native-gVbAsohv.mjs.map +1 -0
  32. package/dist/index.native.js +1 -1
  33. package/dist/index.native.mjs +1 -1
  34. package/dist/{injected-evm-wallet-provider-DvoG_pvE.js → injected-evm-wallet-provider-Bv8VLuB1.js} +2 -2
  35. package/dist/{injected-evm-wallet-provider-DvoG_pvE.js.map → injected-evm-wallet-provider-Bv8VLuB1.js.map} +1 -1
  36. package/dist/{injected-evm-wallet-provider-CgUCo_6H.js → injected-evm-wallet-provider-C3MwUTRd.js} +2 -2
  37. package/dist/{injected-evm-wallet-provider-CgUCo_6H.js.map → injected-evm-wallet-provider-C3MwUTRd.js.map} +1 -1
  38. package/dist/{injected-evm-wallet-provider-Da6kghfZ.mjs → injected-evm-wallet-provider-CMqUqqxN.mjs} +2 -2
  39. package/dist/{injected-evm-wallet-provider-Da6kghfZ.mjs.map → injected-evm-wallet-provider-CMqUqqxN.mjs.map} +1 -1
  40. package/dist/{injected-evm-wallet-provider-BmWa3lHu.mjs → injected-evm-wallet-provider-K0j697jt.mjs} +2 -2
  41. package/dist/{injected-evm-wallet-provider-BmWa3lHu.mjs.map → injected-evm-wallet-provider-K0j697jt.mjs.map} +1 -1
  42. package/dist/{injected-wallet-provider-B7xVks8k.js → injected-wallet-provider-BDt4CN0b.js} +2 -2
  43. package/dist/{injected-wallet-provider-B7xVks8k.js.map → injected-wallet-provider-BDt4CN0b.js.map} +1 -1
  44. package/dist/{injected-wallet-provider-DBWcA9tW.js → injected-wallet-provider-CG3oKdp_.js} +2 -2
  45. package/dist/{injected-wallet-provider-DBWcA9tW.js.map → injected-wallet-provider-CG3oKdp_.js.map} +1 -1
  46. package/dist/{injected-wallet-provider-TvPTmUdy.mjs → injected-wallet-provider-DH8yLpyf.mjs} +2 -2
  47. package/dist/{injected-wallet-provider-TvPTmUdy.mjs.map → injected-wallet-provider-DH8yLpyf.mjs.map} +1 -1
  48. package/dist/{injected-wallet-provider-CJFZ9O7q.mjs → injected-wallet-provider-Dlo96aV9.mjs} +2 -2
  49. package/dist/{injected-wallet-provider-CJFZ9O7q.mjs.map → injected-wallet-provider-Dlo96aV9.mjs.map} +1 -1
  50. package/package.json +2 -2
  51. package/dist/index-B5Luqobz.mjs.map +0 -1
  52. package/dist/index-DWUx-v0z.js.map +0 -1
  53. package/dist/index.native-CAkdhYaP.mjs.map +0 -1
  54. package/dist/index.native-DVMCCbwq.js.map +0 -1
@@ -4199,402 +4199,6 @@ async function ensureSolanaMobileWalletRegistered(config, network) {
4199
4199
 
4200
4200
  var bufferExports = requireBuffer();
4201
4201
 
4202
- // bounded-widget-modal.ts - the shared in-app WIDGET shell for Bounded's login
4203
- // and signing surfaces. One Shadow-DOM overlay + panel that matches the Bounded
4204
- // widget (Seal) look and MOTION: a backdrop fade, a panel rise on desktop, a
4205
- // bottom-sheet on mobile, a height tween as content swaps, and a clean close.
4206
- //
4207
- // It hosts EITHER:
4208
- // - NATIVE content (the login card the SDK renders itself), or
4209
- // - an IFRAME (the Bounded-owned signer page, which must run on the single
4210
- // signer origin because the wallet's 24h signing session lives in that
4211
- // origin's storage, out of every app's reach).
4212
- //
4213
- // The modal is a warm singleton: it stays mounted (hidden) between uses so a
4214
- // second open - or a signature right after login - is instant and, when it is
4215
- // already open, a clean in-place view swap rather than a reopen.
4216
- function hasDOM$5() {
4217
- return typeof window !== "undefined" && typeof document !== "undefined";
4218
- }
4219
- const OVERLAY_CSS = `
4220
- :host{all:initial}
4221
- *{box-sizing:border-box}
4222
- .bw-ov{position:fixed;inset:0;z-index:2147483647;display:none;
4223
- font-family:"Schibsted Grotesk","Helvetica Neue",Arial,sans-serif}
4224
- .bw-ov.mounted{display:block}
4225
- .bw-back{position:fixed;inset:0;background:rgba(4,4,5,.62);opacity:0;
4226
- transition:opacity .24s ease;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}
4227
- .bw-wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:20px}
4228
- .bw-panel{position:relative;width:420px;max-width:calc(100vw - 28px);height:280px;
4229
- border-radius:14px;overflow:hidden;background:#0e0f11;border:1px solid rgba(232,230,222,.08);
4230
- box-shadow:0 1px 0 rgba(232,230,222,.03) inset,0 40px 90px -45px rgba(0,0,0,.85);
4231
- opacity:0;transform:translateY(14px) scale(.98);will-change:transform,opacity,height;
4232
- transition:opacity .28s cubic-bezier(.2,.8,.2,1),transform .28s cubic-bezier(.2,.8,.2,1),
4233
- height .3s cubic-bezier(.2,.8,.2,1)}
4234
- .bw-ov.show .bw-back{opacity:1}
4235
- .bw-ov.show .bw-panel{opacity:1;transform:none}
4236
- .bw-ov.closing .bw-back{opacity:0}
4237
- .bw-ov.closing .bw-panel{opacity:0;transform:translateY(12px) scale(.99)}
4238
- .bw-content{position:absolute;inset:0;overflow:hidden}
4239
- .bw-frame{display:block;width:100%;height:100%;border:0;background:transparent;color-scheme:dark}
4240
- /* editorial corner crop-marks, matching the reference frame */
4241
- .bw-mk{position:absolute;width:13px;height:13px;pointer-events:none;z-index:2;
4242
- border-color:rgba(232,230,222,.22);border-style:solid;border-width:0}
4243
- .bw-mk.tl{top:11px;left:11px;border-top-width:1px;border-left-width:1px}
4244
- .bw-mk.tr{top:11px;right:11px;border-top-width:1px;border-right-width:1px}
4245
- .bw-mk.bl{bottom:11px;left:11px;border-bottom-width:1px;border-left-width:1px}
4246
- .bw-mk.br{bottom:11px;right:11px;border-bottom-width:1px;border-right-width:1px}
4247
- @media(max-width:560px){
4248
- .bw-wrap{align-items:flex-end;padding:0}
4249
- .bw-panel{width:100%;max-width:100%;border-radius:18px 18px 0 0;border-bottom:0}
4250
- .bw-content{overflow-y:auto}
4251
- .bw-ov.show .bw-panel{transform:none}
4252
- .bw-ov:not(.show) .bw-panel,.bw-ov.closing .bw-panel{transform:translateY(18%)}
4253
- }
4254
- @media(prefers-reduced-motion:reduce){.bw-back,.bw-panel{transition:opacity .12s linear!important}}
4255
- `;
4256
- let modal = null;
4257
- function getModal() {
4258
- if (modal)
4259
- return modal;
4260
- const root = document.createElement("div");
4261
- root.setAttribute("data-bounded-widget", "");
4262
- const shadow = root.attachShadow({ mode: "open" });
4263
- const style = document.createElement("style");
4264
- style.textContent = OVERLAY_CSS;
4265
- shadow.appendChild(style);
4266
- const overlay = document.createElement("div");
4267
- overlay.className = "bw-ov";
4268
- const back = document.createElement("div");
4269
- back.className = "bw-back";
4270
- const wrap = document.createElement("div");
4271
- wrap.className = "bw-wrap";
4272
- const panel = document.createElement("div");
4273
- panel.className = "bw-panel";
4274
- const content = document.createElement("div");
4275
- content.className = "bw-content";
4276
- panel.appendChild(content);
4277
- for (const corner of ["tl", "tr", "bl", "br"]) {
4278
- const mk = document.createElement("span");
4279
- mk.className = "bw-mk " + corner;
4280
- panel.appendChild(mk);
4281
- }
4282
- wrap.appendChild(panel);
4283
- overlay.appendChild(back);
4284
- overlay.appendChild(wrap);
4285
- shadow.appendChild(overlay);
4286
- (document.body || document.documentElement).appendChild(root);
4287
- const m = {
4288
- root,
4289
- overlay,
4290
- panel,
4291
- content,
4292
- iframe: null,
4293
- iframeOrigin: "",
4294
- iframeReady: false,
4295
- onBackdrop: null,
4296
- onIframeMessage: null,
4297
- chain: Promise.resolve(),
4298
- fitObserver: null,
4299
- generation: 0,
4300
- onSupersede: null,
4301
- };
4302
- // The dismiss handler lives on `.bw-wrap`: it is the full-viewport flex layer
4303
- // that sits ABOVE `.bw-back`, so it (not the backdrop element) receives clicks
4304
- // outside the panel. Fire only when the click lands on the wrap itself, never
4305
- // when it bubbles up from the panel or its content.
4306
- wrap.addEventListener("click", (event) => {
4307
- if (event.target === wrap && m.onBackdrop)
4308
- m.onBackdrop();
4309
- });
4310
- window.addEventListener("message", (event) => {
4311
- if (!m.iframe)
4312
- return;
4313
- if (event.origin !== m.iframeOrigin)
4314
- return;
4315
- if (event.source !== m.iframe.contentWindow)
4316
- return;
4317
- const d = event.data;
4318
- if (!d || typeof d !== "object")
4319
- return;
4320
- if (m.onIframeMessage)
4321
- m.onIframeMessage(d);
4322
- });
4323
- modal = m;
4324
- return m;
4325
- }
4326
- function showModal(m) {
4327
- m.overlay.classList.remove("closing");
4328
- m.overlay.classList.add("mounted");
4329
- // Force a reflow so the .show transition runs from the hidden state every open.
4330
- void m.panel.offsetWidth;
4331
- m.overlay.classList.add("show");
4332
- }
4333
- function hideModal(m, after) {
4334
- m.overlay.classList.remove("show");
4335
- m.overlay.classList.add("closing");
4336
- // Capture the generation at hide time. If a newer open re-shows the modal within
4337
- // the close tween, this stale timer must NOT strip `.mounted`/`.closing` off the
4338
- // freshly reopened widget - only tear down when we are still the same open.
4339
- const gen = m.generation;
4340
- window.setTimeout(() => {
4341
- if (m.generation === gen)
4342
- m.overlay.classList.remove("mounted", "closing");
4343
- }, 260);
4344
- }
4345
- function setPanelHeight(m, px) {
4346
- const h = Math.max(140, Math.min(680, Math.round(px)));
4347
- m.panel.style.height = h + "px";
4348
- }
4349
- // True outer height of the mounted card: the max of its padding-box scroll height
4350
- // (catches overflowing descendants) and its border-box height plus own vertical
4351
- // margins (catches border + margins, which scrollHeight omits). Ceiled so a
4352
- // sub-pixel row is never clipped under `overflow:hidden`.
4353
- function measureOuterHeight(target) {
4354
- var _a;
4355
- const cs = (_a = target.ownerDocument.defaultView) === null || _a === void 0 ? void 0 : _a.getComputedStyle(target);
4356
- const marginY = cs ? parseFloat(cs.marginTop) + parseFloat(cs.marginBottom) : 0;
4357
- const borderBox = target.offsetHeight + (Number.isFinite(marginY) ? marginY : 0);
4358
- const rect = target.getBoundingClientRect().height;
4359
- return Math.ceil(Math.max(target.scrollHeight, borderBox, rect));
4360
- }
4361
- /**
4362
- * Measure the currently mounted native content and tween the panel to fit it.
4363
- * Re-fits automatically when the card's intrinsic height changes - notably after
4364
- * the Newsreader/Schibsted web fonts swap in (which grows line boxes a few px
4365
- * after first paint) and on inline view swaps (email -> OTP code -> success).
4366
- */
4367
- function fitPanelToContent(m) {
4368
- var _a, _b;
4369
- const first = m.content.firstElementChild;
4370
- const target = first || m.content;
4371
- const apply = () => {
4372
- // Only fit while this exact node is still the mounted card.
4373
- if (m.content.firstElementChild !== first && first)
4374
- return;
4375
- const h = measureOuterHeight(target);
4376
- if (h > 0)
4377
- setPanelHeight(m, h);
4378
- };
4379
- apply();
4380
- const view = target.ownerDocument.defaultView;
4381
- // Re-fit after fonts load (one-shot) and once the ResizeObserver-less browsers
4382
- // have painted, so the height never lags a late reflow.
4383
- (_a = view === null || view === void 0 ? void 0 : view.requestAnimationFrame) === null || _a === void 0 ? void 0 : _a.call(view, apply);
4384
- const fonts = target.ownerDocument.fonts;
4385
- if (fonts === null || fonts === void 0 ? void 0 : fonts.ready)
4386
- fonts.ready.then(apply).catch(() => { });
4387
- (_b = m.fitObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
4388
- m.fitObserver = null;
4389
- if (first && typeof (view === null || view === void 0 ? void 0 : view.ResizeObserver) === "function") {
4390
- const ro = new view.ResizeObserver(() => apply());
4391
- ro.observe(first);
4392
- m.fitObserver = ro;
4393
- }
4394
- }
4395
- /** Switch the modal into NATIVE-content mode: detach any iframe, mount `node`. */
4396
- function mountContent(m, node) {
4397
- var _a;
4398
- detachIframe(m);
4399
- (_a = m.fitObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
4400
- m.fitObserver = null;
4401
- m.content.replaceChildren(node);
4402
- m.content.style.display = "block";
4403
- }
4404
- /** Get (or lazily create) the warm iframe for signer/hosted-page mode. */
4405
- function ensureIframe(m, url, origin) {
4406
- var _a;
4407
- if (m.iframe && m.iframeOrigin === origin && m.iframe.src === url)
4408
- return m.iframe;
4409
- detachIframe(m);
4410
- (_a = m.fitObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
4411
- m.fitObserver = null;
4412
- m.content.style.display = "none";
4413
- const iframe = document.createElement("iframe");
4414
- iframe.className = "bw-frame";
4415
- iframe.setAttribute("title", "Bounded");
4416
- iframe.setAttribute("allow", "clipboard-write");
4417
- iframe.src = url;
4418
- m.iframeOrigin = origin;
4419
- m.iframeReady = false;
4420
- m.panel.appendChild(iframe);
4421
- m.iframe = iframe;
4422
- return iframe;
4423
- }
4424
- /**
4425
- * Destroy the warm signer iframe if one is currently mounted for `origin`, and report
4426
- * whether there was one. Logout needs this: a signing session can live in that document's
4427
- * MEMORY (the signer keeps an in-memory mirror when the browser blocks its storage), and a
4428
- * throwaway iframe opened by the sweep can only ever clear the shared store, never another
4429
- * document's memory. Removing the iframe discards its browsing context, which takes the
4430
- * mirror with it.
4431
- *
4432
- * Deliberately does NOT call getModal(): that would BUILD a modal just to tear it down, on
4433
- * every logout of every app, most of which never mounted one.
4434
- */
4435
- function detachWarmSignerIframe(origin) {
4436
- if (!modal || !modal.iframe || modal.iframeOrigin !== origin)
4437
- return false;
4438
- detachIframe(modal);
4439
- return true;
4440
- }
4441
- function detachIframe(m) {
4442
- if (m.iframe) {
4443
- try {
4444
- m.iframe.remove();
4445
- }
4446
- catch (_a) {
4447
- /* ignore */
4448
- }
4449
- }
4450
- m.iframe = null;
4451
- m.iframeReady = false;
4452
- m.iframeOrigin = "";
4453
- m.onIframeMessage = null;
4454
- }
4455
- // Load the Bounded editorial typefaces (Newsreader display + Schibsted Grotesk body +
4456
- // JetBrains Mono) once at the document level so they apply inside the shadow root. A
4457
- // no-op if already present or off-DOM. Fonts loaded here match the hosted auth page.
4458
- const FONTS_HREF = "https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght@6..72,400;6..72,500&family=Schibsted+Grotesk:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap";
4459
- function ensureFonts() {
4460
- if (!hasDOM$5())
4461
- return;
4462
- if (document.querySelector('link[data-bounded-fonts="1"]'))
4463
- return;
4464
- const pre1 = document.createElement("link");
4465
- pre1.rel = "preconnect";
4466
- pre1.href = "https://fonts.googleapis.com";
4467
- const pre2 = document.createElement("link");
4468
- pre2.rel = "preconnect";
4469
- pre2.href = "https://fonts.gstatic.com";
4470
- pre2.crossOrigin = "anonymous";
4471
- const link = document.createElement("link");
4472
- link.rel = "stylesheet";
4473
- link.href = FONTS_HREF;
4474
- link.setAttribute("data-bounded-fonts", "1");
4475
- document.head.append(pre1, pre2, link);
4476
- }
4477
-
4478
- var boundedWidgetModal = /*#__PURE__*/Object.freeze({
4479
- __proto__: null,
4480
- detachIframe: detachIframe,
4481
- detachWarmSignerIframe: detachWarmSignerIframe,
4482
- ensureFonts: ensureFonts,
4483
- ensureIframe: ensureIframe,
4484
- fitPanelToContent: fitPanelToContent,
4485
- getModal: getModal,
4486
- hasDOM: hasDOM$5,
4487
- hideModal: hideModal,
4488
- mountContent: mountContent,
4489
- setPanelHeight: setPanelHeight,
4490
- showModal: showModal
4491
- });
4492
-
4493
- // turnkey-wallet-widget.ts - runs one Turnkey signer exchange inside the shared
4494
- // in-app WIDGET (bounded-widget-modal.ts). The Bounded-owned signer page is loaded
4495
- // in a warm iframe behind the modal so signing feels native: an in-app approve
4496
- // card (with inline code entry when the 24h signing session must first be
4497
- // established), never a jarring popup.
4498
- //
4499
- // WHY AN IFRAME: the wallet's signing session lives in the single Bounded signer
4500
- // origin's storage, out of every app's reach, so signing MUST run on that origin;
4501
- // the iframe brings it in-app.
4502
- //
4503
- // Restricted embedders: if the frame never loads, the SAME request reruns through
4504
- // `popupFallback` (a real window). Callers pass a popup implementation and never
4505
- // see the difference.
4506
- /**
4507
- * Run one signer exchange through the in-app widget. Resolves when `onResult`
4508
- * accepts a result; falls back to `popupFallback` if the frame cannot be mounted
4509
- * or never loads.
4510
- */
4511
- async function runWidget(url, signerOrigin, ex, popupFallback) {
4512
- if (!hasDOM$5())
4513
- return popupFallback();
4514
- const m = getModal();
4515
- const requestId = String(ex.request.requestId);
4516
- // Serialize: the approve card is modal, and one warm frame handles one request at a time.
4517
- const run = m.chain.then(() => new Promise((resolve, reject) => {
4518
- let settled = false;
4519
- let usedFallback = false;
4520
- const iframe = ensureIframe(m, url, signerOrigin);
4521
- const send = () => {
4522
- if (!iframe.contentWindow)
4523
- return;
4524
- try {
4525
- iframe.contentWindow.postMessage(ex.request, signerOrigin);
4526
- }
4527
- catch (_a) {
4528
- /* frame gone */
4529
- }
4530
- };
4531
- let loadTimer = 0;
4532
- let ceilingTimer = 0;
4533
- const finish = (fn) => {
4534
- if (settled)
4535
- return;
4536
- settled = true;
4537
- m.onIframeMessage = null;
4538
- m.onBackdrop = null;
4539
- window.clearTimeout(loadTimer);
4540
- window.clearTimeout(ceilingTimer);
4541
- fn();
4542
- };
4543
- const goFallback = () => {
4544
- if (usedFallback || settled)
4545
- return;
4546
- usedFallback = true;
4547
- hideModal(m);
4548
- finish(() => popupFallback().then(resolve, reject));
4549
- };
4550
- m.onBackdrop = () => {
4551
- hideModal(m);
4552
- finish(() => reject(new Error("cancelled")));
4553
- };
4554
- m.onIframeMessage = (d) => {
4555
- if (d.type === "bounded:turnkey:ready") {
4556
- m.iframeReady = true;
4557
- send();
4558
- return;
4559
- }
4560
- if (d.type === "bounded:turnkey:resize" && typeof d.height === "number") {
4561
- setPanelHeight(m, d.height);
4562
- return;
4563
- }
4564
- if (d.type === "bounded:turnkey:result" && d.requestId === requestId) {
4565
- const outcome = ex.onResult(d);
4566
- if ("error" in outcome) {
4567
- hideModal(m);
4568
- finish(() => reject(new Error(outcome.error)));
4569
- return;
4570
- }
4571
- if (outcome.done) {
4572
- hideModal(m);
4573
- finish(() => resolve(outcome.value));
4574
- }
4575
- }
4576
- };
4577
- showModal(m);
4578
- if (m.iframeReady)
4579
- send();
4580
- // If the frame never loads, degrade to the popup (best-effort - a post-load
4581
- // popup may be blocked without a gesture, in which case the fallback rejects).
4582
- loadTimer = window.setTimeout(() => {
4583
- if (!m.iframeReady && !settled)
4584
- goFallback();
4585
- }, 8000);
4586
- // Absolute ceiling so a stuck flow cannot hang forever.
4587
- ceilingTimer = window.setTimeout(() => {
4588
- if (!settled) {
4589
- hideModal(m);
4590
- finish(() => reject(new Error("Wallet approval timed out.")));
4591
- }
4592
- }, 5 * 60 * 1000);
4593
- }));
4594
- m.chain = run.then(() => undefined, () => undefined);
4595
- return run;
4596
- }
4597
-
4598
4202
  // turnkey-signer-frame.ts - a HIDDEN, offscreen iframe of the Bounded signer page
4599
4203
  // for UI-less signer exchanges: sealing a login code (bounded:turnkey:encryptOtp),
4600
4204
  // establishing the 24h signing session right after login
@@ -4605,7 +4209,7 @@ async function runWidget(url, signerOrigin, ex, popupFallback) {
4605
4209
  // exchanges (seal a code, then establish the session with the keypair the SEAL
4606
4210
  // left in that exact page instance) talk to the same page. Origin + source +
4607
4211
  // requestId are all pinned; dispose() tears everything down.
4608
- function hasDOM$4() {
4212
+ function hasDOM$5() {
4609
4213
  return typeof window !== 'undefined' && typeof document !== 'undefined';
4610
4214
  }
4611
4215
  async function signerPage() {
@@ -4615,7 +4219,7 @@ async function signerPage() {
4615
4219
  }
4616
4220
  /** Mount the hidden signer frame. Callers MUST dispose() when done. */
4617
4221
  async function openHiddenSignerFrame() {
4618
- if (!hasDOM$4())
4222
+ if (!hasDOM$5())
4619
4223
  throw new Error('The Bounded signer frame is only available in the browser.');
4620
4224
  const { url, origin } = await signerPage();
4621
4225
  const frame = document.createElement('iframe');
@@ -4820,17 +4424,17 @@ function buildTurnkeyActionRequest(opts) {
4820
4424
  // storage, out of every app's reach, so ALL Bounded apps (any subdomain / custom
4821
4425
  // domain) sign through this one origin - giving every user one wallet that roams
4822
4426
  // across every app.
4823
- function hasDOM$3() {
4427
+ function hasDOM$4() {
4824
4428
  return typeof window !== 'undefined' && typeof document !== 'undefined';
4825
4429
  }
4826
- // Dispatch one signer exchange through the in-app WIDGET, degrading to the POPUP
4827
- // when we cannot embed (no DOM, or the frame never mounts/loads). Nothing in the
4828
- // email-session flow needs a popup by preference: the code entry and the approval
4829
- // click both run fine inside a cross-origin iframe on every engine (the old
4830
- // Safari popup preference existed only for in-iframe WebAuthn CREATE).
4430
+ // BSA-2026-006: dispatch every signing / get-or-create exchange through a TOP-LEVEL
4431
+ // POPUP the caller opened from the user gesture - NEVER a framed widget. An authorized
4432
+ // tenant that embedded the signer could overlay its predictable Approve control and steal
4433
+ // one concealed click into a fund-moving signature; a top-level window it cannot reach or
4434
+ // visually redress closes that. A blocked popup surfaces `popup_blocked` (runPopup) and we
4435
+ // never silently fall back to an iframe.
4831
4436
  async function deliver(url, origin, ex) {
4832
- const popup = () => runPopup(url, origin, ex);
4833
- return runWidget(url, origin, { request: ex.request, onResult: ex.onResult }, popup);
4437
+ return runPopup(url, origin, ex);
4834
4438
  }
4835
4439
  async function turnkeySignerPage() {
4836
4440
  const cfg = await core.getConfig();
@@ -4881,7 +4485,7 @@ function activeTurnkeyAddress() {
4881
4485
  ? address
4882
4486
  : null;
4883
4487
  }
4884
- let requestSeq$1 = 0;
4488
+ let requestSeq = 0;
4885
4489
  // Shared popup driver: open the signer page, do the ready/postMessage handshake, and
4886
4490
  // resolve when `onResult` accepts a result message. Origin + source are pinned.
4887
4491
  async function runPopup(url, signerOrigin, ex) {
@@ -4891,7 +4495,7 @@ async function runPopup(url, signerOrigin, ex) {
4891
4495
  const top = window.screenY + Math.max(0, (window.outerHeight - h) / 2);
4892
4496
  const popup = window.open(url, 'bounded-turnkey-signer', `width=${w},height=${h},left=${left},top=${top}`);
4893
4497
  if (!popup) {
4894
- throw new Error('Popup blocked - call this from a user gesture (e.g. a click handler) so the wallet approval window can open.');
4498
+ throw new Error('popup_blocked - the wallet approval window was blocked. Call this from a user gesture (e.g. a click handler) so it can open; we never fall back to an in-page frame.');
4895
4499
  }
4896
4500
  return await new Promise((resolve, reject) => {
4897
4501
  let settled = false;
@@ -5035,7 +4639,7 @@ async function fetchActionAuthorization(action) {
5035
4639
  * it just costs its owner nothing and a stranger nothing.
5036
4640
  */
5037
4641
  async function clearTurnkeySigningSession() {
5038
- if (!hasDOM$3())
4642
+ if (!hasDOM$4())
5039
4643
  return;
5040
4644
  // The session can live in TWO places, and the sweep has to reach both:
5041
4645
  // 1. the signer origin's store, shared by every signer frame in this app's partition -
@@ -5055,7 +4659,7 @@ async function clearTurnkeySigningSession() {
5055
4659
  let frame = null;
5056
4660
  try {
5057
4661
  frame = await openHiddenSignerFrame();
5058
- await frame.request({ type: 'bounded:turnkey:clearSigningSession', requestId: `tkclear_${Date.now()}_${++requestSeq$1}` }, 4000);
4662
+ await frame.request({ type: 'bounded:turnkey:clearSigningSession', requestId: `tkclear_${Date.now()}_${++requestSeq}` }, 4000);
5059
4663
  }
5060
4664
  catch (_b) {
5061
4665
  /* best-effort: never block or fail logout on the sweep */
@@ -5076,10 +4680,10 @@ function hasTurnkeyWallet() {
5076
4680
  * Use this to learn the fee-payer address BEFORE building a transaction to sign.
5077
4681
  */
5078
4682
  async function getOrCreateTurnkeyWallet() {
5079
- if (!hasDOM$3())
4683
+ if (!hasDOM$4())
5080
4684
  throw new Error('Turnkey embedded wallets are only available in the browser (popup-based).');
5081
4685
  const { url, origin } = await turnkeySignerPage();
5082
- const requestId = `tkcreate_${Date.now()}_${++requestSeq$1}`;
4686
+ const requestId = `tkcreate_${Date.now()}_${++requestSeq}`;
5083
4687
  const authorization = await fetchActionAuthorization('createWallet');
5084
4688
  return await deliver(url, origin, {
5085
4689
  request: buildTurnkeyActionRequest({ requestId, action: 'createWallet', authorization }),
@@ -5101,7 +4705,7 @@ async function getOrCreateTurnkeyWallet() {
5101
4705
  * that tab - it never transits Bounded.
5102
4706
  */
5103
4707
  async function openTurnkeyKeyExport() {
5104
- if (!hasDOM$3())
4708
+ if (!hasDOM$4())
5105
4709
  throw new Error('Turnkey key export is only available in the browser.');
5106
4710
  if (!hasTurnkeyWallet())
5107
4711
  throw new Error('This session has no Turnkey wallet to export.');
@@ -5149,11 +4753,11 @@ async function openTurnkeyKeyExport() {
5149
4753
  * completes the signature.
5150
4754
  */
5151
4755
  async function signSolanaMessageViaTurnkey(message) {
5152
- if (!hasDOM$3()) {
4756
+ if (!hasDOM$4()) {
5153
4757
  throw new Error('Turnkey embedded-wallet signing is only available in the browser (popup-based).');
5154
4758
  }
5155
4759
  const { url, origin } = await turnkeySignerPage();
5156
- const requestId = `tksign_${Date.now()}_${++requestSeq$1}`;
4760
+ const requestId = `tksign_${Date.now()}_${++requestSeq}`;
5157
4761
  const messageHex = toHex(message);
5158
4762
  // AS-02: obtain the issuer-signed capability for THESE exact bytes first; the signer will
5159
4763
  // refuse (capability_required) without it. Fetched per signature (each is single-payload).
@@ -5307,7 +4911,7 @@ const ISSUER_SESSION_KIND_KEY = 'bounded_issuer_session_kind';
5307
4911
  // not by `typeof window` (truthy on RN). OIDC redirect/popup is web-only, but the
5308
4912
  // session it mints is read back through this same selector everywhere.
5309
4913
  const sessionManager = core.getActiveSessionManager;
5310
- function hasDOM$2() {
4914
+ function hasDOM$3() {
5311
4915
  return typeof window !== 'undefined' && typeof document !== 'undefined';
5312
4916
  }
5313
4917
  function decodeJwt(token) {
@@ -5458,7 +5062,7 @@ async function buildAuthorize(opts) {
5458
5062
  throw new Error('appId is not configured (init the SDK with your appId)');
5459
5063
  // Web: default to the current page so callers don't have to pass redirectUri. Native has no
5460
5064
  // window — it MUST pass an https universal link (the system auth session needs a real URL).
5461
- const redirectUri = opts.redirectUri || (hasDOM$2() ? window.location.origin + window.location.pathname : undefined);
5065
+ const redirectUri = opts.redirectUri || (hasDOM$3() ? window.location.origin + window.location.pathname : undefined);
5462
5066
  if (!redirectUri)
5463
5067
  throw new Error('redirectUri is required on React Native — pass the https universal link the app owner registered.');
5464
5068
  const { verifier, challenge } = await makePkce();
@@ -5566,7 +5170,7 @@ async function runNativeLogin(opts) {
5566
5170
  async function loginWithRedirect(opts) {
5567
5171
  if (isNative())
5568
5172
  return runNativeLogin(opts);
5569
- if (!hasDOM$2())
5173
+ if (!hasDOM$3())
5570
5174
  throw new Error('loginWithRedirect requires a browser or a configured React Native platform');
5571
5175
  const { url, pkce } = await buildAuthorize(opts);
5572
5176
  // Stash the verifier + state so the redirect_uri page can finish the exchange. sessionStorage
@@ -5583,7 +5187,7 @@ async function loginWithRedirect(opts) {
5583
5187
  * loginWithRedirect() (which resolves with the User), so there is no separate redirect
5584
5188
  * page to finish from. Calling it on RN is harmless. */
5585
5189
  async function completeLoginFromRedirect() {
5586
- if (!hasDOM$2())
5190
+ if (!hasDOM$3())
5587
5191
  return null;
5588
5192
  const url = new URL(window.location.href);
5589
5193
  const code = url.searchParams.get('code');
@@ -5631,7 +5235,7 @@ async function completeLoginFromRedirect() {
5631
5235
  * redirect_uri page posts the code back. The redirect_uri page just calls the usual
5632
5236
  * completeLoginFromRedirect() (which auto-detects the popup) — no separate call needed. */
5633
5237
  async function loginWithPopup(opts) {
5634
- if (!hasDOM$2())
5238
+ if (!hasDOM$3())
5635
5239
  throw new Error('loginWithPopup is web-only — on React Native use loginWithRedirect()');
5636
5240
  // Open synchronously, before PKCE/config awaits consume the click's user-
5637
5241
  // activation window. Opening only after buildAuthorize() intermittently
@@ -5791,7 +5395,7 @@ async function loginWithPopup(opts) {
5791
5395
  /** Call this on the redirect_uri page when login was opened via loginWithPopup(): posts the
5792
5396
  * code back to the opener and closes. (For the redirect variant use completeLoginFromRedirect.) */
5793
5397
  function completeLoginInPopup(openerOrigin) {
5794
- if (!hasDOM$2() || !window.opener)
5398
+ if (!hasDOM$3() || !window.opener)
5795
5399
  return;
5796
5400
  const url = new URL(window.location.href);
5797
5401
  const code = url.searchParams.get('code');
@@ -6245,7 +5849,7 @@ async function getAuthProvider(config) {
6245
5849
  // manager as legacy email sessions. Human login should be started
6246
5850
  // with loginWithRedirect(); this provider is kept as the restore and
6247
5851
  // logout surface for those sessions.
6248
- currentAuthProvider = new (await Promise.resolve().then(function () { return require('./email-auth-provider-CiMNpEWJ.js'); })).EmailAuthProvider();
5852
+ currentAuthProvider = new (await Promise.resolve().then(function () { return require('./email-auth-provider-DxB_pJKO.js'); })).EmailAuthProvider();
6249
5853
  break;
6250
5854
  case "phantom": {
6251
5855
  // First-class Solana WALLET LOGIN (the poof/TaroBase-style "connect
@@ -6287,7 +5891,7 @@ async function getAuthProvider(config) {
6287
5891
  // (bounded-login-widget.ts), so registration never counts as
6288
5892
  // evidence the user has a wallet.
6289
5893
  await ensureSolanaMobileWalletRegistered(config, configuredNetwork);
6290
- const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-B7xVks8k.js'); });
5894
+ const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-BDt4CN0b.js'); });
6291
5895
  currentAuthProvider = new InjectedWalletProvider(resolveInjectedWalletConfig(config), rpcUrl, configuredNetwork);
6292
5896
  break;
6293
5897
  }
@@ -6300,7 +5904,7 @@ async function getAuthProvider(config) {
6300
5904
  // Selecting authMethod:'evm-wallet' IS the opt-in — pass evmWalletConfig
6301
5905
  // to point at a specific wallet or set chainId/domain.
6302
5906
  const evmWalletConfig = ((_a = config.evmWalletConfig) !== null && _a !== void 0 ? _a : {});
6303
- const { InjectedEvmWalletProvider } = await Promise.resolve().then(function () { return require('./injected-evm-wallet-provider-DvoG_pvE.js'); });
5907
+ const { InjectedEvmWalletProvider } = await Promise.resolve().then(function () { return require('./injected-evm-wallet-provider-Bv8VLuB1.js'); });
6304
5908
  currentAuthProvider = new InjectedEvmWalletProvider(evmWalletConfig);
6305
5909
  break;
6306
5910
  }
@@ -6325,7 +5929,7 @@ async function getAuthProvider(config) {
6325
5929
  break;
6326
5930
  case "guest":
6327
5931
  // Anonymous auth: a device-local ed25519 keypair signs the challenge.
6328
- currentAuthProvider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-BrMhyIse.js'); })).GuestAuthProvider();
5932
+ currentAuthProvider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-B9C3Iub_.js'); })).GuestAuthProvider();
6329
5933
  break;
6330
5934
  }
6331
5935
  if (!currentAuthProvider) {
@@ -6387,7 +5991,7 @@ async function signInAnonymously() {
6387
5991
  // Lazy-load the guest provider so its ed25519/web3 dependencies stay out of
6388
5992
  // the main bundle for data-only apps. Specifier is a static string literal
6389
5993
  // so bundle-splitting can see it.
6390
- const provider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-BrMhyIse.js'); })).GuestAuthProvider();
5994
+ const provider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-B9C3Iub_.js'); })).GuestAuthProvider();
6391
5995
  setAuthLoading(true); // reflect "signing in…" while the guest keypair session is minted
6392
5996
  // adoptSessionProvider awaits only getConfig() (already resolved here) and then
6393
5997
  // runs every write - provider/method/config/marker/user - in one synchronous
@@ -6558,7 +6162,7 @@ function ensureSharedWalletLoginReady() {
6558
6162
  if (!sharedWalletReady) {
6559
6163
  sharedWalletReady = (async () => {
6560
6164
  await core.getConfig();
6561
- await Promise.resolve().then(function () { return require('./injected-wallet-provider-B7xVks8k.js'); });
6165
+ await Promise.resolve().then(function () { return require('./injected-wallet-provider-BDt4CN0b.js'); });
6562
6166
  })().catch((error) => {
6563
6167
  sharedWalletReady = null;
6564
6168
  throw error;
@@ -6661,7 +6265,7 @@ async function loginWithWallet(options) {
6661
6265
  await ensureSharedWalletLoginReady();
6662
6266
  else
6663
6267
  await ensureWalletLoginReady();
6664
- const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-B7xVks8k.js'); });
6268
+ const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-BDt4CN0b.js'); });
6665
6269
  // Keep the RAW wallet provider: the offchain wrapper forwards only a fixed
6666
6270
  // set of methods, so cleanup below must speak to the provider that actually
6667
6271
  // holds the config rather than to whatever is wrapping it.
@@ -7112,80 +6716,350 @@ function setAuthLoading(loading) {
7112
6716
  function getAuthLoading() {
7113
6717
  return isAuthLoading;
7114
6718
  }
7115
- async function login(options) {
7116
- if (!authProviderInstance) {
7117
- throw new Error('SDK not initialized. Please call init() first.');
6719
+ async function login(options) {
6720
+ if (!authProviderInstance) {
6721
+ throw new Error('SDK not initialized. Please call init() first.');
6722
+ }
6723
+ const loggedInUser = await login$1(options);
6724
+ setCurrentUser(loggedInUser);
6725
+ return currentUser;
6726
+ }
6727
+ async function logout(options) {
6728
+ if (!authProviderInstance) {
6729
+ throw new Error('SDK not initialized. Please call init() first.');
6730
+ }
6731
+ await logout$1(options);
6732
+ setCurrentUser(null);
6733
+ }
6734
+ var hasRunFirstTime = false;
6735
+ function userPrincipalKey(user) {
6736
+ var _a, _b, _c;
6737
+ if (!user)
6738
+ return null;
6739
+ return (_c = (_b = (_a = user.id) !== null && _a !== void 0 ? _a : user.address) !== null && _b !== void 0 ? _b : user.email) !== null && _c !== void 0 ? _c : null;
6740
+ }
6741
+ // Update the currentUser and notify listeners
6742
+ function setCurrentUser(user) {
6743
+ // Enrich with the universal identity fields (id/email) derived from the
6744
+ // active session's idToken — central chokepoint so every provider's user
6745
+ // object gains { id, address, email } without each construction site
6746
+ // having to set them.
6747
+ user = withUserIdentity(user);
6748
+ const lastUser = currentUser;
6749
+ const wasInitializedBefore = hasRunFirstTime;
6750
+ const lastPrincipal = userPrincipalKey(currentUser);
6751
+ const nextPrincipal = userPrincipalKey(user);
6752
+ const shouldRunListeners = !hasRunFirstTime || lastPrincipal !== nextPrincipal;
6753
+ hasRunFirstTime = true;
6754
+ currentUser = user;
6755
+ if (shouldRunListeners) {
6756
+ // Isolate each listener: a consumer callback that throws (during logout in
6757
+ // particular) must not abort the notification loop OR skip the socket reopen
6758
+ // below. Before this, a single throwing listener propagated out of
6759
+ // setCurrentUser, so the realtime socket kept streaming as the signed-out
6760
+ // user and the other listeners were never told the principal changed.
6761
+ authStateListeners.forEach((callback) => {
6762
+ try {
6763
+ callback(user);
6764
+ }
6765
+ catch (error) {
6766
+ console.error('Error in onAuthStateChanged listener:', error);
6767
+ }
6768
+ });
6769
+ // Reconnect WebSocket connections with new auth when user changes.
6770
+ // Skip on the first setCurrentUser call during init() — there are no
6771
+ // existing connections to reconnect at that point.
6772
+ if (wasInitializedBefore && userPrincipalKey(lastUser) !== nextPrincipal) {
6773
+ core.reconnectWithNewAuth().catch((error) => {
6774
+ console.error('Error reconnecting WebSocket after auth change:', error);
6775
+ });
6776
+ }
6777
+ }
6778
+ // Return this commit's normalized snapshot. A listener can start another
6779
+ // login, so the mutable currentUser may already belong to a newer session.
6780
+ return user;
6781
+ }
6782
+ function getCurrentUser() {
6783
+ return currentUser;
6784
+ }
6785
+ /** @internal Snapshot/restore surface for transactional auth-provider switches. */
6786
+ function getAuthProviderInstance() {
6787
+ return authProviderInstance;
6788
+ }
6789
+ function setAuthProviderInstance(provider) {
6790
+ authProviderInstance = provider;
6791
+ }
6792
+
6793
+ // bounded-widget-modal.ts - the shared in-app WIDGET shell for Bounded's login
6794
+ // and signing surfaces. One Shadow-DOM overlay + panel that matches the Bounded
6795
+ // widget (Seal) look and MOTION: a backdrop fade, a panel rise on desktop, a
6796
+ // bottom-sheet on mobile, a height tween as content swaps, and a clean close.
6797
+ //
6798
+ // It hosts EITHER:
6799
+ // - NATIVE content (the login card the SDK renders itself), or
6800
+ // - an IFRAME (the Bounded-owned signer page, which must run on the single
6801
+ // signer origin because the wallet's 24h signing session lives in that
6802
+ // origin's storage, out of every app's reach).
6803
+ //
6804
+ // The modal is a warm singleton: it stays mounted (hidden) between uses so a
6805
+ // second open - or a signature right after login - is instant and, when it is
6806
+ // already open, a clean in-place view swap rather than a reopen.
6807
+ function hasDOM$2() {
6808
+ return typeof window !== "undefined" && typeof document !== "undefined";
6809
+ }
6810
+ const OVERLAY_CSS = `
6811
+ :host{all:initial}
6812
+ *{box-sizing:border-box}
6813
+ .bw-ov{position:fixed;inset:0;z-index:2147483647;display:none;
6814
+ font-family:"Schibsted Grotesk","Helvetica Neue",Arial,sans-serif}
6815
+ .bw-ov.mounted{display:block}
6816
+ .bw-back{position:fixed;inset:0;background:rgba(4,4,5,.62);opacity:0;
6817
+ transition:opacity .24s ease;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}
6818
+ .bw-wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:20px}
6819
+ .bw-panel{position:relative;width:420px;max-width:calc(100vw - 28px);height:280px;
6820
+ border-radius:14px;overflow:hidden;background:#0e0f11;border:1px solid rgba(232,230,222,.08);
6821
+ box-shadow:0 1px 0 rgba(232,230,222,.03) inset,0 40px 90px -45px rgba(0,0,0,.85);
6822
+ opacity:0;transform:translateY(14px) scale(.98);will-change:transform,opacity,height;
6823
+ transition:opacity .28s cubic-bezier(.2,.8,.2,1),transform .28s cubic-bezier(.2,.8,.2,1),
6824
+ height .3s cubic-bezier(.2,.8,.2,1)}
6825
+ .bw-ov.show .bw-back{opacity:1}
6826
+ .bw-ov.show .bw-panel{opacity:1;transform:none}
6827
+ .bw-ov.closing .bw-back{opacity:0}
6828
+ .bw-ov.closing .bw-panel{opacity:0;transform:translateY(12px) scale(.99)}
6829
+ .bw-content{position:absolute;inset:0;overflow:hidden}
6830
+ .bw-frame{display:block;width:100%;height:100%;border:0;background:transparent;color-scheme:dark}
6831
+ /* editorial corner crop-marks, matching the reference frame */
6832
+ .bw-mk{position:absolute;width:13px;height:13px;pointer-events:none;z-index:2;
6833
+ border-color:rgba(232,230,222,.22);border-style:solid;border-width:0}
6834
+ .bw-mk.tl{top:11px;left:11px;border-top-width:1px;border-left-width:1px}
6835
+ .bw-mk.tr{top:11px;right:11px;border-top-width:1px;border-right-width:1px}
6836
+ .bw-mk.bl{bottom:11px;left:11px;border-bottom-width:1px;border-left-width:1px}
6837
+ .bw-mk.br{bottom:11px;right:11px;border-bottom-width:1px;border-right-width:1px}
6838
+ @media(max-width:560px){
6839
+ .bw-wrap{align-items:flex-end;padding:0}
6840
+ .bw-panel{width:100%;max-width:100%;border-radius:18px 18px 0 0;border-bottom:0}
6841
+ .bw-content{overflow-y:auto}
6842
+ .bw-ov.show .bw-panel{transform:none}
6843
+ .bw-ov:not(.show) .bw-panel,.bw-ov.closing .bw-panel{transform:translateY(18%)}
6844
+ }
6845
+ @media(prefers-reduced-motion:reduce){.bw-back,.bw-panel{transition:opacity .12s linear!important}}
6846
+ `;
6847
+ let modal = null;
6848
+ function getModal() {
6849
+ if (modal)
6850
+ return modal;
6851
+ const root = document.createElement("div");
6852
+ root.setAttribute("data-bounded-widget", "");
6853
+ const shadow = root.attachShadow({ mode: "open" });
6854
+ const style = document.createElement("style");
6855
+ style.textContent = OVERLAY_CSS;
6856
+ shadow.appendChild(style);
6857
+ const overlay = document.createElement("div");
6858
+ overlay.className = "bw-ov";
6859
+ const back = document.createElement("div");
6860
+ back.className = "bw-back";
6861
+ const wrap = document.createElement("div");
6862
+ wrap.className = "bw-wrap";
6863
+ const panel = document.createElement("div");
6864
+ panel.className = "bw-panel";
6865
+ const content = document.createElement("div");
6866
+ content.className = "bw-content";
6867
+ panel.appendChild(content);
6868
+ for (const corner of ["tl", "tr", "bl", "br"]) {
6869
+ const mk = document.createElement("span");
6870
+ mk.className = "bw-mk " + corner;
6871
+ panel.appendChild(mk);
6872
+ }
6873
+ wrap.appendChild(panel);
6874
+ overlay.appendChild(back);
6875
+ overlay.appendChild(wrap);
6876
+ shadow.appendChild(overlay);
6877
+ (document.body || document.documentElement).appendChild(root);
6878
+ const m = {
6879
+ root,
6880
+ overlay,
6881
+ panel,
6882
+ content,
6883
+ iframe: null,
6884
+ iframeOrigin: "",
6885
+ iframeReady: false,
6886
+ onBackdrop: null,
6887
+ onIframeMessage: null,
6888
+ chain: Promise.resolve(),
6889
+ fitObserver: null,
6890
+ generation: 0,
6891
+ onSupersede: null,
6892
+ };
6893
+ // The dismiss handler lives on `.bw-wrap`: it is the full-viewport flex layer
6894
+ // that sits ABOVE `.bw-back`, so it (not the backdrop element) receives clicks
6895
+ // outside the panel. Fire only when the click lands on the wrap itself, never
6896
+ // when it bubbles up from the panel or its content.
6897
+ wrap.addEventListener("click", (event) => {
6898
+ if (event.target === wrap && m.onBackdrop)
6899
+ m.onBackdrop();
6900
+ });
6901
+ window.addEventListener("message", (event) => {
6902
+ if (!m.iframe)
6903
+ return;
6904
+ if (event.origin !== m.iframeOrigin)
6905
+ return;
6906
+ if (event.source !== m.iframe.contentWindow)
6907
+ return;
6908
+ const d = event.data;
6909
+ if (!d || typeof d !== "object")
6910
+ return;
6911
+ if (m.onIframeMessage)
6912
+ m.onIframeMessage(d);
6913
+ });
6914
+ modal = m;
6915
+ return m;
6916
+ }
6917
+ function showModal(m) {
6918
+ m.overlay.classList.remove("closing");
6919
+ m.overlay.classList.add("mounted");
6920
+ // Force a reflow so the .show transition runs from the hidden state every open.
6921
+ void m.panel.offsetWidth;
6922
+ m.overlay.classList.add("show");
6923
+ }
6924
+ function hideModal(m, after) {
6925
+ m.overlay.classList.remove("show");
6926
+ m.overlay.classList.add("closing");
6927
+ // Capture the generation at hide time. If a newer open re-shows the modal within
6928
+ // the close tween, this stale timer must NOT strip `.mounted`/`.closing` off the
6929
+ // freshly reopened widget - only tear down when we are still the same open.
6930
+ const gen = m.generation;
6931
+ window.setTimeout(() => {
6932
+ if (m.generation === gen)
6933
+ m.overlay.classList.remove("mounted", "closing");
6934
+ }, 260);
6935
+ }
6936
+ function setPanelHeight(m, px) {
6937
+ const h = Math.max(140, Math.min(680, Math.round(px)));
6938
+ m.panel.style.height = h + "px";
6939
+ }
6940
+ // True outer height of the mounted card: the max of its padding-box scroll height
6941
+ // (catches overflowing descendants) and its border-box height plus own vertical
6942
+ // margins (catches border + margins, which scrollHeight omits). Ceiled so a
6943
+ // sub-pixel row is never clipped under `overflow:hidden`.
6944
+ function measureOuterHeight(target) {
6945
+ var _a;
6946
+ const cs = (_a = target.ownerDocument.defaultView) === null || _a === void 0 ? void 0 : _a.getComputedStyle(target);
6947
+ const marginY = cs ? parseFloat(cs.marginTop) + parseFloat(cs.marginBottom) : 0;
6948
+ const borderBox = target.offsetHeight + (Number.isFinite(marginY) ? marginY : 0);
6949
+ const rect = target.getBoundingClientRect().height;
6950
+ return Math.ceil(Math.max(target.scrollHeight, borderBox, rect));
6951
+ }
6952
+ /**
6953
+ * Measure the currently mounted native content and tween the panel to fit it.
6954
+ * Re-fits automatically when the card's intrinsic height changes - notably after
6955
+ * the Newsreader/Schibsted web fonts swap in (which grows line boxes a few px
6956
+ * after first paint) and on inline view swaps (email -> OTP code -> success).
6957
+ */
6958
+ function fitPanelToContent(m) {
6959
+ var _a, _b;
6960
+ const first = m.content.firstElementChild;
6961
+ const target = first || m.content;
6962
+ const apply = () => {
6963
+ // Only fit while this exact node is still the mounted card.
6964
+ if (m.content.firstElementChild !== first && first)
6965
+ return;
6966
+ const h = measureOuterHeight(target);
6967
+ if (h > 0)
6968
+ setPanelHeight(m, h);
6969
+ };
6970
+ apply();
6971
+ const view = target.ownerDocument.defaultView;
6972
+ // Re-fit after fonts load (one-shot) and once the ResizeObserver-less browsers
6973
+ // have painted, so the height never lags a late reflow.
6974
+ (_a = view === null || view === void 0 ? void 0 : view.requestAnimationFrame) === null || _a === void 0 ? void 0 : _a.call(view, apply);
6975
+ const fonts = target.ownerDocument.fonts;
6976
+ if (fonts === null || fonts === void 0 ? void 0 : fonts.ready)
6977
+ fonts.ready.then(apply).catch(() => { });
6978
+ (_b = m.fitObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
6979
+ m.fitObserver = null;
6980
+ if (first && typeof (view === null || view === void 0 ? void 0 : view.ResizeObserver) === "function") {
6981
+ const ro = new view.ResizeObserver(() => apply());
6982
+ ro.observe(first);
6983
+ m.fitObserver = ro;
7118
6984
  }
7119
- const loggedInUser = await login$1(options);
7120
- setCurrentUser(loggedInUser);
7121
- return currentUser;
7122
6985
  }
7123
- async function logout(options) {
7124
- if (!authProviderInstance) {
7125
- throw new Error('SDK not initialized. Please call init() first.');
7126
- }
7127
- await logout$1(options);
7128
- setCurrentUser(null);
6986
+ /** Switch the modal into NATIVE-content mode: detach any iframe, mount `node`. */
6987
+ function mountContent(m, node) {
6988
+ var _a;
6989
+ detachIframe(m);
6990
+ (_a = m.fitObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
6991
+ m.fitObserver = null;
6992
+ m.content.replaceChildren(node);
6993
+ m.content.style.display = "block";
7129
6994
  }
7130
- var hasRunFirstTime = false;
7131
- function userPrincipalKey(user) {
7132
- var _a, _b, _c;
7133
- if (!user)
7134
- return null;
7135
- return (_c = (_b = (_a = user.id) !== null && _a !== void 0 ? _a : user.address) !== null && _b !== void 0 ? _b : user.email) !== null && _c !== void 0 ? _c : null;
6995
+ /**
6996
+ * Destroy the warm signer iframe if one is currently mounted for `origin`, and report
6997
+ * whether there was one. Logout needs this: a signing session can live in that document's
6998
+ * MEMORY (the signer keeps an in-memory mirror when the browser blocks its storage), and a
6999
+ * throwaway iframe opened by the sweep can only ever clear the shared store, never another
7000
+ * document's memory. Removing the iframe discards its browsing context, which takes the
7001
+ * mirror with it.
7002
+ *
7003
+ * Deliberately does NOT call getModal(): that would BUILD a modal just to tear it down, on
7004
+ * every logout of every app, most of which never mounted one.
7005
+ */
7006
+ function detachWarmSignerIframe(origin) {
7007
+ if (!modal || !modal.iframe || modal.iframeOrigin !== origin)
7008
+ return false;
7009
+ detachIframe(modal);
7010
+ return true;
7136
7011
  }
7137
- // Update the currentUser and notify listeners
7138
- function setCurrentUser(user) {
7139
- // Enrich with the universal identity fields (id/email) derived from the
7140
- // active session's idToken — central chokepoint so every provider's user
7141
- // object gains { id, address, email } without each construction site
7142
- // having to set them.
7143
- user = withUserIdentity(user);
7144
- const lastUser = currentUser;
7145
- const wasInitializedBefore = hasRunFirstTime;
7146
- const lastPrincipal = userPrincipalKey(currentUser);
7147
- const nextPrincipal = userPrincipalKey(user);
7148
- const shouldRunListeners = !hasRunFirstTime || lastPrincipal !== nextPrincipal;
7149
- hasRunFirstTime = true;
7150
- currentUser = user;
7151
- if (shouldRunListeners) {
7152
- // Isolate each listener: a consumer callback that throws (during logout in
7153
- // particular) must not abort the notification loop OR skip the socket reopen
7154
- // below. Before this, a single throwing listener propagated out of
7155
- // setCurrentUser, so the realtime socket kept streaming as the signed-out
7156
- // user and the other listeners were never told the principal changed.
7157
- authStateListeners.forEach((callback) => {
7158
- try {
7159
- callback(user);
7160
- }
7161
- catch (error) {
7162
- console.error('Error in onAuthStateChanged listener:', error);
7163
- }
7164
- });
7165
- // Reconnect WebSocket connections with new auth when user changes.
7166
- // Skip on the first setCurrentUser call during init() — there are no
7167
- // existing connections to reconnect at that point.
7168
- if (wasInitializedBefore && userPrincipalKey(lastUser) !== nextPrincipal) {
7169
- core.reconnectWithNewAuth().catch((error) => {
7170
- console.error('Error reconnecting WebSocket after auth change:', error);
7171
- });
7012
+ function detachIframe(m) {
7013
+ if (m.iframe) {
7014
+ try {
7015
+ m.iframe.remove();
7016
+ }
7017
+ catch (_a) {
7018
+ /* ignore */
7172
7019
  }
7173
7020
  }
7174
- // Return this commit's normalized snapshot. A listener can start another
7175
- // login, so the mutable currentUser may already belong to a newer session.
7176
- return user;
7177
- }
7178
- function getCurrentUser() {
7179
- return currentUser;
7180
- }
7181
- /** @internal Snapshot/restore surface for transactional auth-provider switches. */
7182
- function getAuthProviderInstance() {
7183
- return authProviderInstance;
7021
+ m.iframe = null;
7022
+ m.iframeReady = false;
7023
+ m.iframeOrigin = "";
7024
+ m.onIframeMessage = null;
7184
7025
  }
7185
- function setAuthProviderInstance(provider) {
7186
- authProviderInstance = provider;
7026
+ // Load the Bounded editorial typefaces (Newsreader display + Schibsted Grotesk body +
7027
+ // JetBrains Mono) once at the document level so they apply inside the shadow root. A
7028
+ // no-op if already present or off-DOM. Fonts loaded here match the hosted auth page.
7029
+ const FONTS_HREF = "https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght@6..72,400;6..72,500&family=Schibsted+Grotesk:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap";
7030
+ function ensureFonts() {
7031
+ if (!hasDOM$2())
7032
+ return;
7033
+ if (document.querySelector('link[data-bounded-fonts="1"]'))
7034
+ return;
7035
+ const pre1 = document.createElement("link");
7036
+ pre1.rel = "preconnect";
7037
+ pre1.href = "https://fonts.googleapis.com";
7038
+ const pre2 = document.createElement("link");
7039
+ pre2.rel = "preconnect";
7040
+ pre2.href = "https://fonts.gstatic.com";
7041
+ pre2.crossOrigin = "anonymous";
7042
+ const link = document.createElement("link");
7043
+ link.rel = "stylesheet";
7044
+ link.href = FONTS_HREF;
7045
+ link.setAttribute("data-bounded-fonts", "1");
7046
+ document.head.append(pre1, pre2, link);
7187
7047
  }
7188
7048
 
7049
+ var boundedWidgetModal = /*#__PURE__*/Object.freeze({
7050
+ __proto__: null,
7051
+ detachIframe: detachIframe,
7052
+ detachWarmSignerIframe: detachWarmSignerIframe,
7053
+ ensureFonts: ensureFonts,
7054
+ fitPanelToContent: fitPanelToContent,
7055
+ getModal: getModal,
7056
+ hasDOM: hasDOM$2,
7057
+ hideModal: hideModal,
7058
+ mountContent: mountContent,
7059
+ setPanelHeight: setPanelHeight,
7060
+ showModal: showModal
7061
+ });
7062
+
7189
7063
  // bounded-widget-ownership.ts - per-open ownership generation for the singleton
7190
7064
  // login/signing modal (bounded-widget-modal.ts).
7191
7065
  //
@@ -7626,72 +7500,11 @@ function shouldOfferSolanaMobileWallet(input) {
7626
7500
  function hasDOM$1() {
7627
7501
  return typeof window !== 'undefined' && typeof document !== 'undefined';
7628
7502
  }
7629
- let requestSeq = 0;
7630
- // Seal the OTP in the given hidden signer iframe on the Bounded origin. The signer page
7631
- // runs the vendored @turnkey/crypto encryptOtpCodeToBundle (which verifies the enclave's
7632
- // signature on the target bundle first) and KEEPS the sealing keypair for the follow-up
7633
- // establishSigningSession exchange below.
7634
- async function sealOtpInFrame(frame, code, otpEncryptionTargetBundle) {
7635
- const d = await frame.request({
7636
- type: 'bounded:turnkey:encryptOtp',
7637
- requestId: `tkotp_${Date.now()}_${++requestSeq}`,
7638
- otpCode: code,
7639
- otpEncryptionTargetBundle,
7640
- }, 30000).catch(() => { throw new Error('Securing the code timed out. Please try again.'); });
7641
- if (d.ok && typeof d.encryptedOtpBundle === 'string' && d.encryptedOtpBundle) {
7642
- return d.encryptedOtpBundle;
7643
- }
7644
- throw new Error(String(d.error || 'Could not secure the code. Please try again.'));
7645
- }
7646
- // Best-effort login-time signing-session establishment. Runs AFTER the login session is
7647
- // live (the action authorization is minted from the freshly-established id token) against
7648
- // the SAME frame that sealed the code - only that page instance holds the keypair the
7649
- // verificationToken is bound to. Owns the frame: disposes it whatever happens. Never
7650
- // throws; a failure only means the first signature costs one inline code.
7651
- async function establishSigningSessionInBackground(frame, baseUrl, signingSession) {
7652
- // The moment this work begins - captured before ANY await, because verify() has already
7653
- // resolved and the user can hit logout throughout the authorization mint below. The
7654
- // signer drops a session whose work began at-or-before the newest logout, so a stamp
7655
- // taken later (in the frame, after the mint) would let this establishment out-run a
7656
- // logout and resurrect the session.
7657
- const startedAtMs = Date.now();
7658
- try {
7659
- let idToken = null;
7660
- try {
7661
- idToken = core.getActiveSessionManager().getIdToken();
7662
- }
7663
- catch (_a) {
7664
- idToken = null;
7665
- }
7666
- if (!idToken)
7667
- return;
7668
- const authorization = await requestTurnkeyActionAuthorization({
7669
- baseUrl,
7670
- idToken,
7671
- action: 'createWallet',
7672
- fetchImpl: (url, init) => fetch(url, init),
7673
- });
7674
- const d = await frame.request({
7675
- type: 'bounded:turnkey:establishSigningSession',
7676
- requestId: `tkestablish_${Date.now()}_${++requestSeq}`,
7677
- verificationToken: signingSession.verificationToken,
7678
- authorization,
7679
- startedAtMs,
7680
- }, 20000);
7681
- if (!d.ok)
7682
- throw new Error(String(d.error || 'establish_failed'));
7683
- }
7684
- catch (_b) {
7685
- /* best-effort: the signer's inline code flow covers the first signature */
7686
- }
7687
- finally {
7688
- frame.dispose();
7689
- }
7690
- }
7691
7503
  /**
7692
- * Begin authMode:"turnkey" login: ask Turnkey to email a one-time code to `email`.
7693
- * Resolves with a handle whose `verify(code)` finishes the login. Web-only (the sealing
7694
- * step needs the signer iframe); on React Native use loginWithRedirect() instead.
7504
+ * Begin authMode:"turnkey" login: ask Turnkey to email a one-time code to `email`, and open a
7505
+ * server-bound login ceremony. Resolves with a handle whose `run(iframe)` finishes the login inside
7506
+ * the Bounded OTP frame. Web-only (the sealing runs in the frame); on React Native use
7507
+ * loginWithRedirect() instead.
7695
7508
  */
7696
7509
  async function startTurnkeyEmailLogin(email) {
7697
7510
  if (!hasDOM$1()) {
@@ -7714,51 +7527,120 @@ async function startTurnkeyEmailLogin(email) {
7714
7527
  }
7715
7528
  const otpId = data.otpId;
7716
7529
  const otpEncryptionTargetBundle = data.otpEncryptionTargetBundle;
7530
+ // A tenant-origin login always gets a ceremony id; without one the OTP frame refuses to run
7531
+ // (there is nothing to bind the frame key to), which is the correct fail-closed posture.
7532
+ const ceremonyId = typeof data.ceremonyId === 'string' ? data.ceremonyId : '';
7533
+ const signerOrigin = new URL(base).origin;
7717
7534
  return {
7718
7535
  email: trimmed,
7719
- async verify(code) {
7720
- const trimmedCode = (code || '').trim();
7721
- if (!trimmedCode)
7722
- throw new Error('Enter the code from your email.');
7723
- // One frame per attempt: the sealing keypair lives in this exact page instance,
7724
- // and a failed attempt's frame (and keypair) is discarded with it.
7725
- const frame = await openHiddenSignerFrame();
7726
- let vdata = null;
7727
- try {
7728
- const encryptedOtpBundle = await sealOtpInFrame(frame, trimmedCode, otpEncryptionTargetBundle);
7729
- const vres = await fetch(`${base}/wallet/turnkey/auth/verify`, {
7730
- method: 'POST',
7731
- headers: { 'content-type': 'application/json' },
7732
- body: JSON.stringify({ email: trimmed, appId, otpId, encryptedOtpBundle }),
7733
- });
7734
- vdata = await vres.json().catch(() => null);
7735
- if (!vres.ok || !vdata) {
7736
- throw new Error(String((vdata && vdata.error) || 'Invalid or expired code.'));
7536
+ run(iframe) {
7537
+ if (!ceremonyId) {
7538
+ return Promise.reject(new Error('This app is not configured for in-frame wallet login. Please try again.'));
7539
+ }
7540
+ return runOtpFrame({ iframe, base, signerOrigin, appId, email: trimmed, otpId, otpEncryptionTargetBundle, ceremonyId });
7541
+ },
7542
+ };
7543
+ }
7544
+ // Point the iframe at the Bounded OTP frame and run the postMessage handshake:
7545
+ // frame 'ready' -> we post {otpId, targetBundle}; frame registers its key + shows the input;
7546
+ // frame 'result' {session} -> we establish the inline session and resolve the User.
7547
+ // When the frame asks for the signing-session authorization ('need-authorization') we mint a
7548
+ // createWallet action authorization on THIS (registered) origin and post it in - never a key.
7549
+ function runOtpFrame(run) {
7550
+ const tenantOrigin = window.location.origin;
7551
+ const url = `${run.base.replace(/\/$/, '')}/wallet/turnkey/otp-frame`
7552
+ + `?appId=${encodeURIComponent(run.appId)}`
7553
+ + `&origin=${encodeURIComponent(tenantOrigin)}`
7554
+ + `&email=${encodeURIComponent(run.email)}`
7555
+ + `&ceremonyId=${encodeURIComponent(run.ceremonyId)}`;
7556
+ return new Promise((resolve, reject) => {
7557
+ let settled = false;
7558
+ let started = false;
7559
+ const cleanup = () => { settled = true; window.removeEventListener('message', onMsg); window.clearTimeout(timer); };
7560
+ const onMsg = async (event) => {
7561
+ if (settled)
7562
+ return;
7563
+ // Trust ONLY the signer origin and the frame's own window.
7564
+ if (event.origin !== run.signerOrigin || event.source !== run.iframe.contentWindow)
7565
+ return;
7566
+ const d = event.data;
7567
+ if (!d || typeof d !== 'object')
7568
+ return;
7569
+ if (d.type === 'bounded:otp:resize') {
7570
+ if (typeof d.height === 'number' && Number.isFinite(d.height) && d.height > 0) {
7571
+ run.iframe.style.height = `${Math.max(140, Math.min(680, Math.ceil(d.height)))}px`;
7737
7572
  }
7573
+ return;
7738
7574
  }
7739
- catch (error) {
7740
- frame.dispose();
7741
- throw error;
7575
+ if (d.type === 'bounded:otp:ready') {
7576
+ if (started)
7577
+ return;
7578
+ started = true;
7579
+ post({ type: 'bounded:otp:start', otpId: run.otpId, otpEncryptionTargetBundle: run.otpEncryptionTargetBundle });
7580
+ return;
7742
7581
  }
7743
- const user = await establishInlineSession(vdata).catch((error) => {
7744
- frame.dispose();
7745
- throw error;
7746
- });
7747
- // SINGLE CODE AT LOGIN: turn the same code's verificationToken into the 24h
7748
- // signing session, in the background - login is already resolved and must never
7749
- // wait on (or fail with) this. The task owns the frame's disposal.
7750
- const signingSession = vdata.signingSession;
7751
- if (signingSession && typeof signingSession.verificationToken === 'string' && signingSession.verificationToken) {
7752
- void establishSigningSessionInBackground(frame, base, {
7753
- verificationToken: signingSession.verificationToken,
7754
- });
7582
+ if (d.type === 'bounded:otp:need-authorization') {
7583
+ // Best-effort: mint the createWallet authorization on our own registered origin and hand it
7584
+ // to the frame so it can complete the 24h signing session with the key it holds. Never throws.
7585
+ try {
7586
+ let idToken = null;
7587
+ try {
7588
+ idToken = core.getActiveSessionManager().getIdToken();
7589
+ }
7590
+ catch (_a) {
7591
+ idToken = null;
7592
+ }
7593
+ if (!idToken) {
7594
+ post({ type: 'bounded:otp:authorization', authorization: '' });
7595
+ return;
7596
+ }
7597
+ const authorization = await requestTurnkeyActionAuthorization({
7598
+ baseUrl: run.base, idToken, action: 'createWallet', fetchImpl: (u, i) => fetch(u, i),
7599
+ });
7600
+ post({ type: 'bounded:otp:authorization', authorization });
7601
+ }
7602
+ catch (_b) {
7603
+ post({ type: 'bounded:otp:authorization', authorization: '' });
7604
+ }
7605
+ return;
7755
7606
  }
7756
- else {
7757
- frame.dispose();
7607
+ if (d.type === 'bounded:otp:result') {
7608
+ if (d.ok && d.session && typeof d.session === 'object') {
7609
+ try {
7610
+ const user = await establishInlineSession(d.session);
7611
+ cleanup();
7612
+ resolve(user);
7613
+ }
7614
+ catch (error) {
7615
+ cleanup();
7616
+ reject(error instanceof Error ? error : new Error(String(error)));
7617
+ }
7618
+ return;
7619
+ }
7620
+ cleanup();
7621
+ reject(new Error(String((d && d.error) || 'Invalid or expired code.')));
7622
+ return;
7758
7623
  }
7759
- return user;
7760
- },
7761
- };
7624
+ // 'bounded:otp:registered' is advisory; login does not depend on it.
7625
+ };
7626
+ const post = (msg) => {
7627
+ var _a;
7628
+ try {
7629
+ (_a = run.iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage(msg, run.signerOrigin);
7630
+ }
7631
+ catch ( /* frame gone */_b) { /* frame gone */ }
7632
+ };
7633
+ window.addEventListener('message', onMsg);
7634
+ // 3 minutes: the human must read the email and type the code. A frame that never reports is a
7635
+ // failed login the user can retry.
7636
+ const timer = window.setTimeout(() => {
7637
+ if (settled)
7638
+ return;
7639
+ cleanup();
7640
+ reject(new Error('The sign-in code entry timed out. Please try again.'));
7641
+ }, 3 * 60 * 1000);
7642
+ run.iframe.src = url;
7643
+ });
7762
7644
  }
7763
7645
 
7764
7646
  // bounded-login-widget.ts - the UNIFIED Bounded login surface: one in-app card
@@ -7983,7 +7865,7 @@ function untilCancelled(work, pending) {
7983
7865
  * run first; call from a user gesture so social/wallet popups are allowed.
7984
7866
  */
7985
7867
  async function openBoundedWidget(opts = {}) {
7986
- if (!hasDOM$5()) {
7868
+ if (!hasDOM$2()) {
7987
7869
  throw new Error("openBoundedWidget is web-only - on React Native use loginWithRedirect()");
7988
7870
  }
7989
7871
  // Registered before the first await - see PendingOpen.
@@ -8228,6 +8110,7 @@ async function prepareAndOpen(opts, pending) {
8228
8110
  m.onBackdrop = cancel;
8229
8111
  const style = el("style");
8230
8112
  style.textContent = CARD_CSS;
8113
+ let emailDraft = "";
8231
8114
  const renderChoose = () => {
8232
8115
  const card = el("div", "c");
8233
8116
  card.appendChild(style.cloneNode(true));
@@ -8262,6 +8145,8 @@ async function prepareAndOpen(opts, pending) {
8262
8145
  input.autocomplete = "email";
8263
8146
  input.placeholder = "you@example.com";
8264
8147
  input.required = true;
8148
+ input.value = emailDraft;
8149
+ input.addEventListener("input", () => { emailDraft = input.value; });
8265
8150
  const submit = el("button", "btn primary");
8266
8151
  submit.type = "submit";
8267
8152
  submit.textContent = "Continue";
@@ -8270,6 +8155,7 @@ async function prepareAndOpen(opts, pending) {
8270
8155
  e.preventDefault();
8271
8156
  if (!input.value)
8272
8157
  return;
8158
+ emailDraft = input.value;
8273
8159
  // Starting another lane takes the card from a wallet attempt still
8274
8160
  // preparing, and this lane holds the lock until its own request
8275
8161
  // settles - otherwise a failed email login would keep every other lane
@@ -8286,7 +8172,7 @@ async function prepareAndOpen(opts, pending) {
8286
8172
  setBusy(submit, "Sending code\u2026");
8287
8173
  startTurnkeyEmailLogin(input.value)
8288
8174
  .then((handle) => { if (settleAttempt(attempt))
8289
- swap(renderOtp(handle)); })
8175
+ swap(renderOtpFrame(handle)); })
8290
8176
  .catch((err) => {
8291
8177
  if (!settleAttempt(attempt))
8292
8178
  return;
@@ -8299,6 +8185,7 @@ async function prepareAndOpen(opts, pending) {
8299
8185
  setBusy(submit, "Opening secure sign-in\u2026");
8300
8186
  const emailOpts = {
8301
8187
  provider: "email",
8188
+ prompt: "login",
8302
8189
  loginHint: input.value,
8303
8190
  redirectUri: opts.redirectUri,
8304
8191
  };
@@ -8570,75 +8457,25 @@ async function prepareAndOpen(opts, pending) {
8570
8457
  card.appendChild(status);
8571
8458
  return card;
8572
8459
  };
8573
- // authMode:"turnkey" inline code entry. Same editorial card; the code is sealed
8574
- // on the Bounded signer origin and verified server-side by Turnkey.
8575
- const renderOtp = (handle) => {
8460
+ // BSA-2026-004: authMode:"turnkey" code entry runs ENTIRELY inside a Bounded-origin OTP frame,
8461
+ // never in this tenant DOM. The widget only embeds that frame and reacts to its result - it
8462
+ // never sees the plaintext code or the sealing key. The editorial card wraps the frame so the
8463
+ // experience still feels native; the code input, sealing, and verify all live behind the frame.
8464
+ const renderOtpFrame = (handle) => {
8576
8465
  const card = el("div", "c");
8577
8466
  card.appendChild(style.cloneNode(true));
8578
8467
  const back = el("button", "back", "\u2190 Back");
8579
8468
  back.type = "button";
8580
- // Abandoning the code entry settles the email attempt that opened it, so
8581
- // the next lane the user picks is not refused by a claim nobody owns.
8582
- back.addEventListener("click", () => { invalidateAttempts(); swap(renderChoose()); });
8583
8469
  card.appendChild(back);
8584
- card.appendChild(el("h1", undefined, "Enter your code"));
8585
- card.appendChild(el("p", "sub", `We sent a 6-digit code to ${handle.email}.`));
8586
8470
  const status = el("div", "status");
8587
8471
  const setErr = (msg) => { status.textContent = msg; status.classList.add("err"); };
8588
- const form = el("form");
8589
- const label = el("label");
8590
- label.htmlFor = "bw-otp";
8591
- label.textContent = "Verification code";
8592
- const input = el("input", "otp");
8593
- input.id = "bw-otp";
8594
- input.type = "text";
8595
- input.inputMode = "numeric";
8596
- input.autocomplete = "one-time-code";
8597
- input.maxLength = 6;
8598
- input.placeholder = "000000";
8599
- input.required = true;
8600
- const submit = el("button", "btn primary");
8601
- submit.type = "submit";
8602
- submit.textContent = "Verify";
8603
- form.append(label, input, submit);
8604
- const verify = () => {
8605
- const code = input.value.replace(/\D+/g, "");
8606
- if (code.length < 6) {
8607
- setErr("Enter the 6-digit code.");
8608
- return;
8609
- }
8610
- // Verifying MINTS a session, so it takes the lock like every other lane.
8611
- const attempt = beginAttempt();
8612
- if (attempt === null) {
8613
- setErr(BUSY_MSG);
8614
- return;
8615
- }
8616
- status.classList.remove("err");
8617
- status.textContent = "";
8618
- submit.disabled = true;
8619
- input.disabled = true;
8620
- submit.replaceChildren(el("span", "spin"), document.createTextNode("Verifying\u2026"));
8621
- handle
8622
- .verify(code)
8623
- .then((u) => { settleAttempt(attempt); succeed(u); })
8624
- .catch((err) => {
8625
- if (!settleAttempt(attempt))
8626
- return;
8627
- submit.disabled = false;
8628
- input.disabled = false;
8629
- submit.textContent = "Verify";
8630
- input.value = "";
8631
- input.focus();
8632
- setErr(errText(err));
8633
- });
8634
- };
8635
- form.addEventListener("submit", (e) => { e.preventDefault(); verify(); });
8636
- // Auto-submit once six digits are in - the buttery one-shot paste/type path.
8637
- input.addEventListener("input", () => {
8638
- if (input.value.replace(/\D+/g, "").length === 6 && !submit.disabled)
8639
- verify();
8640
- });
8641
- card.appendChild(form);
8472
+ // The Bounded-origin OTP frame. Its route restricts frame-ancestors to this exact origin, so
8473
+ // only this app may embed it; the code + the P-256 key never leave it into this realm.
8474
+ // Its trusted resize messages fit the heading, input, and any error copy.
8475
+ const frame = document.createElement("iframe");
8476
+ frame.setAttribute("title", "Enter your code");
8477
+ frame.style.cssText = "width:100%;height:210px;border:0;background:transparent;color-scheme:dark";
8478
+ card.appendChild(frame);
8642
8479
  card.appendChild(status);
8643
8480
  const foot = el("div", "foot");
8644
8481
  const poweredBy = el("a", undefined, "Powered by Bounded");
@@ -8647,7 +8484,22 @@ async function prepareAndOpen(opts, pending) {
8647
8484
  poweredBy.rel = "noopener";
8648
8485
  foot.appendChild(poweredBy);
8649
8486
  card.appendChild(foot);
8650
- requestAnimationFrame(() => input.focus());
8487
+ // Running the frame MINTS a session, so it takes the lock like every other lane. Abandoning
8488
+ // via Back settles it so the next lane the user picks is not refused by a claim nobody owns.
8489
+ const attempt = beginAttempt();
8490
+ if (attempt === null) {
8491
+ setErr(BUSY_MSG);
8492
+ return card;
8493
+ }
8494
+ back.addEventListener("click", () => { settleAttempt(attempt); invalidateAttempts(); swap(renderChoose()); });
8495
+ handle
8496
+ .run(frame)
8497
+ .then((u) => { settleAttempt(attempt); succeed(u); })
8498
+ .catch((err) => {
8499
+ if (!settleAttempt(attempt))
8500
+ return;
8501
+ setErr(errText(err));
8502
+ });
8651
8503
  return card;
8652
8504
  };
8653
8505
  const swap = (node) => {
@@ -10354,4 +10206,4 @@ exports.useAuth = useAuth;
10354
10206
  exports.useBuildRun = useBuildRun;
10355
10207
  exports.useQuery = useQuery;
10356
10208
  exports.waitForAuthReady = waitForAuthReady;
10357
- //# sourceMappingURL=index-DWUx-v0z.js.map
10209
+ //# sourceMappingURL=index-CYdYsf8s.js.map