@bounded-sh/client 0.0.112-ux-audit.1 → 0.0.112

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 (51) hide show
  1. package/dist/auth/bounded-widget-modal.d.ts +24 -2
  2. package/dist/auth/oidc-auth.d.ts +1 -1
  3. package/dist/{email-auth-provider-eyi3Wpnx.mjs → email-auth-provider-BV1Iea4A.mjs} +2 -2
  4. package/dist/{email-auth-provider-BfCH7K6N.mjs.map → email-auth-provider-BV1Iea4A.mjs.map} +1 -1
  5. package/dist/{email-auth-provider-CG5cxOj0.js → email-auth-provider-CyFsnSzx.js} +2 -2
  6. package/dist/{email-auth-provider-CG5cxOj0.js.map → email-auth-provider-CyFsnSzx.js.map} +1 -1
  7. package/dist/{email-auth-provider-BfCH7K6N.mjs → email-auth-provider-D0_iXVYk.mjs} +2 -2
  8. package/dist/{email-auth-provider-eyi3Wpnx.mjs.map → email-auth-provider-D0_iXVYk.mjs.map} +1 -1
  9. package/dist/{email-auth-provider-DTrCfOJd.js → email-auth-provider-OVzQ63dn.js} +2 -2
  10. package/dist/{email-auth-provider-DTrCfOJd.js.map → email-auth-provider-OVzQ63dn.js.map} +1 -1
  11. package/dist/{guest-auth-provider-Tndt6tzJ.js → guest-auth-provider-0WlOutqU.js} +2 -2
  12. package/dist/{guest-auth-provider-Tndt6tzJ.js.map → guest-auth-provider-0WlOutqU.js.map} +1 -1
  13. package/dist/{guest-auth-provider-DjOFjtnk.mjs → guest-auth-provider-AP80a700.mjs} +2 -2
  14. package/dist/{guest-auth-provider-BsW56oJg.mjs.map → guest-auth-provider-AP80a700.mjs.map} +1 -1
  15. package/dist/{guest-auth-provider-Dtl1J-2F.js → guest-auth-provider-B4WIWOjV.js} +2 -2
  16. package/dist/{guest-auth-provider-Dtl1J-2F.js.map → guest-auth-provider-B4WIWOjV.js.map} +1 -1
  17. package/dist/{guest-auth-provider-BsW56oJg.mjs → guest-auth-provider-Cqx1zH23.mjs} +2 -2
  18. package/dist/{guest-auth-provider-DjOFjtnk.mjs.map → guest-auth-provider-Cqx1zH23.mjs.map} +1 -1
  19. package/dist/{index-Brdwqgqs.js → index-BmNNZlTl.js} +226 -67
  20. package/dist/index-BmNNZlTl.js.map +1 -0
  21. package/dist/{index-CRxls8Qc.mjs → index-D76-cZcD.mjs} +226 -67
  22. package/dist/index-D76-cZcD.mjs.map +1 -0
  23. package/dist/index.js +1 -1
  24. package/dist/index.mjs +1 -1
  25. package/dist/{index.native-ingNhWPw.js → index.native-C5glqiTU.js} +59 -14
  26. package/dist/index.native-C5glqiTU.js.map +1 -0
  27. package/dist/{index.native-CAH_ks0s.mjs → index.native-DyR93n8K.mjs} +59 -14
  28. package/dist/index.native-DyR93n8K.mjs.map +1 -0
  29. package/dist/index.native.js +1 -1
  30. package/dist/index.native.mjs +1 -1
  31. package/dist/{injected-evm-wallet-provider-Cao1U0NZ.js → injected-evm-wallet-provider-4ICYUEU4.js} +2 -2
  32. package/dist/{injected-evm-wallet-provider-Cao1U0NZ.js.map → injected-evm-wallet-provider-4ICYUEU4.js.map} +1 -1
  33. package/dist/{injected-evm-wallet-provider-CwOGXCDt.js → injected-evm-wallet-provider-C8YItSiB.js} +2 -2
  34. package/dist/{injected-evm-wallet-provider-CwOGXCDt.js.map → injected-evm-wallet-provider-C8YItSiB.js.map} +1 -1
  35. package/dist/{injected-evm-wallet-provider-DXjwPjLW.mjs → injected-evm-wallet-provider-CS4R8c51.mjs} +2 -2
  36. package/dist/{injected-evm-wallet-provider-DXjwPjLW.mjs.map → injected-evm-wallet-provider-CS4R8c51.mjs.map} +1 -1
  37. package/dist/{injected-evm-wallet-provider-BjkCeRBL.mjs → injected-evm-wallet-provider-WxUbyMZL.mjs} +2 -2
  38. package/dist/{injected-evm-wallet-provider-BjkCeRBL.mjs.map → injected-evm-wallet-provider-WxUbyMZL.mjs.map} +1 -1
  39. package/dist/{injected-wallet-provider-CfYNIIHd.mjs → injected-wallet-provider-D1ygndjX.mjs} +2 -2
  40. package/dist/{injected-wallet-provider-CfYNIIHd.mjs.map → injected-wallet-provider-D1ygndjX.mjs.map} +1 -1
  41. package/dist/{injected-wallet-provider-LTnqoaYD.js → injected-wallet-provider-D8vIsUHf.js} +2 -2
  42. package/dist/{injected-wallet-provider-LTnqoaYD.js.map → injected-wallet-provider-D8vIsUHf.js.map} +1 -1
  43. package/dist/{injected-wallet-provider-BG6gGNch.mjs → injected-wallet-provider-UuGrzdNW.mjs} +2 -2
  44. package/dist/{injected-wallet-provider-BG6gGNch.mjs.map → injected-wallet-provider-UuGrzdNW.mjs.map} +1 -1
  45. package/dist/{injected-wallet-provider-DSlHfgMU.js → injected-wallet-provider-nwd3ialR.js} +2 -2
  46. package/dist/{injected-wallet-provider-DSlHfgMU.js.map → injected-wallet-provider-nwd3ialR.js.map} +1 -1
  47. package/package.json +2 -2
  48. package/dist/index-Brdwqgqs.js.map +0 -1
  49. package/dist/index-CRxls8Qc.mjs.map +0 -1
  50. package/dist/index.native-CAH_ks0s.mjs.map +0 -1
  51. package/dist/index.native-ingNhWPw.js.map +0 -1
@@ -56,6 +56,13 @@ function walletOperationGuard() {
56
56
  throw new Error('wallet_session_changed');
57
57
  };
58
58
  }
59
+ function walletLoginGuard(signal) {
60
+ const generation = ++loginGeneration;
61
+ return () => {
62
+ if ((signal === null || signal === void 0 ? void 0 : signal.aborted) || generation !== loginGeneration)
63
+ throw new Error('wallet_login_cancelled');
64
+ };
65
+ }
59
66
  // This realm owns one active SDK session and one retained signer frame.
60
67
  let activeWalletState;
61
68
  function setWalletSessionState(key, state) { activeWalletState = { key, state }; }
@@ -3890,9 +3897,13 @@ async function ensureSolanaMobileWalletRegistered(config, network) {
3890
3897
  }
3891
3898
  catch (_e) {
3892
3899
  throw new WalletConfigError(`Bounded: Solana Mobile wallet support needs the optional peer dependency ` +
3893
- `'@solana-mobile/wallet-standard-mobile', which is not installed. Install it ` +
3894
- `in your app to enable the mobile wallet, or leave the Solana Mobile wallet ` +
3895
- `out of this app's wallet configuration.`);
3900
+ `'@solana-mobile/wallet-standard-mobile', which is not installed. Until it is, ` +
3901
+ `Android users (Seeker, Saga, any phone browser) get NO wallet to connect: ` +
3902
+ `installed wallet apps are never injected into a page there, the mobile adapter ` +
3903
+ `is the only route. Fix: npm install @solana-mobile/wallet-standard-mobile ` +
3904
+ `(a web app also needs legacy-peer-deps=true in its .npmrc, because the adapter's ` +
3905
+ `protocol package declares react-native as a peer). Or leave the Solana Mobile ` +
3906
+ `wallet out of this app's wallet configuration on purpose.`);
3896
3907
  }
3897
3908
  const { registerMwa, createDefaultChainSelector, SolanaMobileWalletAdapterWalletName, SolanaMobileWalletAdapterRemoteWalletName, } = mwa;
3898
3909
  const before = new Set(getWallets().get());
@@ -3979,7 +3990,7 @@ const OVERLAY_CSS = `
3979
3990
  :host{all:initial}
3980
3991
  *{box-sizing:border-box}
3981
3992
  .bw-ov{position:fixed;inset:0;z-index:2147483647;display:none;
3982
- font-family:"Schibsted Grotesk","Helvetica Neue",Arial,sans-serif}
3993
+ font-family:"Bounded Schibsted Grotesk","Schibsted Grotesk","Helvetica Neue",Arial,sans-serif}
3983
3994
  .bw-ov.mounted{display:block}
3984
3995
  .bw-back{position:fixed;inset:0;background:rgba(4,4,5,.62);opacity:0;
3985
3996
  transition:opacity .24s ease;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}
@@ -4012,6 +4023,29 @@ const OVERLAY_CSS = `
4012
4023
  }
4013
4024
  @media(prefers-reduced-motion:reduce){.bw-back,.bw-panel{transition:opacity .12s linear!important}}
4014
4025
  `;
4026
+ /**
4027
+ * Style the shadow tree with `css` as constructed stylesheets, and say whether it could. A host
4028
+ * CSP whose style-src lacks 'unsafe-inline' refuses an inline <style> even inside a shadow root,
4029
+ * which left the widget unstyled (the Bounded router compiles exactly `style-src 'self'` for an
4030
+ * app that declares browser boundaries and names no style sources); CSP does not govern
4031
+ * constructed sheets. A browser without them (they shipped with adoptedStyleSheets everywhere)
4032
+ * gets <style> elements instead (styleTag).
4033
+ */
4034
+ function adoptStyles(shadow, css) {
4035
+ if (!("adoptedStyleSheets" in shadow))
4036
+ return false;
4037
+ shadow.adoptedStyleSheets = css.map((text) => {
4038
+ const sheet = new CSSStyleSheet();
4039
+ sheet.replaceSync(text);
4040
+ return sheet;
4041
+ });
4042
+ return true;
4043
+ }
4044
+ function styleTag(css) {
4045
+ const style = document.createElement("style");
4046
+ style.textContent = css;
4047
+ return style;
4048
+ }
4015
4049
  let modal = null;
4016
4050
  function getModal() {
4017
4051
  if (modal)
@@ -4019,9 +4053,8 @@ function getModal() {
4019
4053
  const root = document.createElement("div");
4020
4054
  root.setAttribute("data-bounded-widget", "");
4021
4055
  const shadow = root.attachShadow({ mode: "open" });
4022
- const style = document.createElement("style");
4023
- style.textContent = OVERLAY_CSS;
4024
- shadow.appendChild(style);
4056
+ if (!adoptStyles(shadow, [OVERLAY_CSS]))
4057
+ shadow.appendChild(styleTag(OVERLAY_CSS));
4025
4058
  const overlay = document.createElement("div");
4026
4059
  overlay.className = "bw-ov";
4027
4060
  const back = document.createElement("div");
@@ -4156,12 +4189,15 @@ function fitPanelToContent(m) {
4156
4189
  m.fitObserver = ro;
4157
4190
  }
4158
4191
  }
4159
- /** Switch the modal into NATIVE-content mode: detach any iframe, mount `node`. */
4160
- function mountContent(m, node) {
4192
+ /** Switch the modal into NATIVE-content mode: detach any iframe, mount `node`, styled by `css` until the next mount. */
4193
+ function mountContent(m, node, css) {
4161
4194
  var _a;
4162
4195
  detachIframe(m);
4163
4196
  (_a = m.fitObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
4164
4197
  m.fitObserver = null;
4198
+ const shadow = m.root.shadowRoot;
4199
+ if (!(shadow && adoptStyles(shadow, [OVERLAY_CSS, css] )) && css)
4200
+ node.prepend(styleTag(css));
4165
4201
  m.content.replaceChildren(node);
4166
4202
  m.content.style.display = "block";
4167
4203
  }
@@ -4223,27 +4259,39 @@ function detachIframe(m) {
4223
4259
  m.iframeContext = "";
4224
4260
  m.onIframeMessage = null;
4225
4261
  }
4226
- // Load the Bounded editorial typefaces (Newsreader display + Schibsted Grotesk body +
4227
- // JetBrains Mono) once at the document level so they apply inside the shadow root. A
4228
- // no-op if already present or off-DOM. Fonts loaded here match the hosted auth page.
4229
- 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";
4262
+ /**
4263
+ * The Bounded editorial typefaces the hosted auth page uses (Newsreader display, Schibsted
4264
+ * Grotesk body), registered once at the document level so they apply inside the shadow root.
4265
+ *
4266
+ * The Bounded router serves them same-origin at /__bounded/fonts/*.woff2 on every site it
4267
+ * serves (bounded-router scripts/build-widget-styles.cjs), and they load through the FontFace
4268
+ * API: no stylesheet, so a host CSP's style-src never applies, and the only fetch is same-origin,
4269
+ * which font-src 'self' admits. Google Fonts CSS injected here was refused by strict hosts such
4270
+ * as the OpenApps venue (style-src 'self'), with a CSP error on every page. A page the router
4271
+ * does not serve has none of its /__bounded/ scripts: nothing is fetched there, and the widget's
4272
+ * font stacks fall back to the page's own faces or system ones. The Bounded-prefixed family
4273
+ * names never restyle the host page's own text.
4274
+ */
4275
+ const WIDGET_FONT_FACES = [
4276
+ { family: "Bounded Newsreader", url: "/__bounded/fonts/newsreader.woff2", weight: "200 800" },
4277
+ { family: "Bounded Schibsted Grotesk", url: "/__bounded/fonts/schibsted.woff2", weight: "400 900" },
4278
+ ];
4230
4279
  function ensureFonts() {
4231
- if (!hasDOM$5())
4280
+ if (!hasDOM$5() || typeof FontFace === "undefined" || !document.fonts)
4232
4281
  return;
4233
- if (document.querySelector('link[data-bounded-fonts="1"]'))
4282
+ if (!document.querySelector('script[src^="/__bounded/"]'))
4234
4283
  return;
4235
- const pre1 = document.createElement("link");
4236
- pre1.rel = "preconnect";
4237
- pre1.href = "https://fonts.googleapis.com";
4238
- const pre2 = document.createElement("link");
4239
- pre2.rel = "preconnect";
4240
- pre2.href = "https://fonts.gstatic.com";
4241
- pre2.crossOrigin = "anonymous";
4242
- const link = document.createElement("link");
4243
- link.rel = "stylesheet";
4244
- link.href = FONTS_HREF;
4245
- link.setAttribute("data-bounded-fonts", "1");
4246
- document.head.append(pre1, pre2, link);
4284
+ const registered = new Set();
4285
+ document.fonts.forEach((face) => registered.add(face.family.replace(/^["']|["']$/g, "")));
4286
+ for (const { family, url, weight } of WIDGET_FONT_FACES) {
4287
+ if (registered.has(family))
4288
+ continue;
4289
+ // Against the page's origin, not its base URL: a <base href> elsewhere must not move the fetch off this origin.
4290
+ const face = new FontFace(family, `url("${location.origin}${url}") format("woff2")`, { weight, display: "swap" });
4291
+ document.fonts.add(face);
4292
+ // A face that cannot load is taken back out; the font stack falls back to the next face.
4293
+ face.load().catch(() => { document.fonts.delete(face); });
4294
+ }
4247
4295
  }
4248
4296
 
4249
4297
  // turnkey-wallet-widget.ts - runs one Turnkey signer exchange inside the shared
@@ -5781,20 +5829,21 @@ async function loginWithPopup(opts) {
5781
5829
  // Turnkey-native login path (authMode:"turnkey"), which mints the session server-side
5782
5830
  // after Turnkey verifies the email OTP - there is no OIDC code to exchange. Wiring is
5783
5831
  // identical to exchangeCode's tail so the realtime worker cannot tell them apart.
5784
- async function establishInlineSession(data, beforeCommit, beforeStore) {
5832
+ async function establishInlineSession(data, beforeCommit, beforeStore, onCommitting) {
5785
5833
  const base = await issuerBase();
5834
+ beforeCommit === null || beforeCommit === void 0 ? void 0 : beforeCommit();
5786
5835
  const idToken = tokenField(data, 'id_token', 'idToken');
5787
5836
  if (!idToken)
5788
5837
  throw new Error('No id_token in the inline session response');
5789
5838
  const accessToken = tokenField(data, 'access_token', 'accessToken') || idToken;
5790
5839
  const refreshToken = boundedRefreshTokenField(data);
5791
5840
  const user = userFromIdToken(idToken);
5792
- const rollback = await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base, () => { });
5841
+ const rollback = await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base, () => { beforeCommit === null || beforeCommit === void 0 ? void 0 : beforeCommit(); });
5793
5842
  // Adopt the OIDC provider on EVERY session surface - including core's signing
5794
5843
  // pointer (getConfig().authProvider), which this path used to skip, leaving
5795
5844
  // onchain writes routed through a previous (e.g. logged-out wallet) provider.
5796
5845
  try {
5797
- await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user, beforeCommit });
5846
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user, beforeCommit: () => { beforeCommit === null || beforeCommit === void 0 ? void 0 : beforeCommit(); onCommitting === null || onCommitting === void 0 ? void 0 : onCommitting(); } });
5798
5847
  }
5799
5848
  catch (error) {
5800
5849
  if (typeof rollback === 'function')
@@ -6262,7 +6311,7 @@ async function getAuthProvider(config, fromStorage = false) {
6262
6311
  // manager as legacy email sessions. Human login should be started
6263
6312
  // with loginWithRedirect(); this provider is kept as the restore and
6264
6313
  // logout surface for those sessions.
6265
- currentAuthProvider = new (await Promise.resolve().then(function () { return require('./email-auth-provider-CG5cxOj0.js'); })).EmailAuthProvider();
6314
+ currentAuthProvider = new (await Promise.resolve().then(function () { return require('./email-auth-provider-CyFsnSzx.js'); })).EmailAuthProvider();
6266
6315
  break;
6267
6316
  case "phantom": {
6268
6317
  // First-class Solana WALLET LOGIN (the poof/TaroBase-style "connect
@@ -6304,7 +6353,7 @@ async function getAuthProvider(config, fromStorage = false) {
6304
6353
  // (bounded-login-widget.ts), so registration never counts as
6305
6354
  // evidence the user has a wallet.
6306
6355
  await ensureSolanaMobileWalletRegistered(config, configuredNetwork);
6307
- const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-LTnqoaYD.js'); });
6356
+ const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-D8vIsUHf.js'); });
6308
6357
  currentAuthProvider = new InjectedWalletProvider(resolveInjectedWalletConfig(config), rpcUrl, configuredNetwork);
6309
6358
  break;
6310
6359
  }
@@ -6317,7 +6366,7 @@ async function getAuthProvider(config, fromStorage = false) {
6317
6366
  // Selecting authMethod:'evm-wallet' IS the opt-in — pass evmWalletConfig
6318
6367
  // to point at a specific wallet or set chainId/domain.
6319
6368
  const evmWalletConfig = ((_a = config.evmWalletConfig) !== null && _a !== void 0 ? _a : {});
6320
- const { InjectedEvmWalletProvider } = await Promise.resolve().then(function () { return require('./injected-evm-wallet-provider-CwOGXCDt.js'); });
6369
+ const { InjectedEvmWalletProvider } = await Promise.resolve().then(function () { return require('./injected-evm-wallet-provider-C8YItSiB.js'); });
6321
6370
  currentAuthProvider = new InjectedEvmWalletProvider(evmWalletConfig);
6322
6371
  break;
6323
6372
  }
@@ -6342,7 +6391,7 @@ async function getAuthProvider(config, fromStorage = false) {
6342
6391
  break;
6343
6392
  case "guest":
6344
6393
  // Anonymous auth: a device-local ed25519 keypair signs the challenge.
6345
- currentAuthProvider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-Dtl1J-2F.js'); })).GuestAuthProvider();
6394
+ currentAuthProvider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-B4WIWOjV.js'); })).GuestAuthProvider();
6346
6395
  break;
6347
6396
  }
6348
6397
  if (!currentAuthProvider) {
@@ -6404,7 +6453,7 @@ async function signInAnonymously() {
6404
6453
  // Lazy-load the guest provider so its ed25519/web3 dependencies stay out of
6405
6454
  // the main bundle for data-only apps. Specifier is a static string literal
6406
6455
  // so bundle-splitting can see it.
6407
- const provider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-Dtl1J-2F.js'); })).GuestAuthProvider();
6456
+ const provider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-B4WIWOjV.js'); })).GuestAuthProvider();
6408
6457
  setAuthLoading(true); // reflect "signing in…" while the guest keypair session is minted
6409
6458
  // adoptSessionProvider awaits only getConfig() (already resolved here) and then
6410
6459
  // runs every write - provider/method/config/marker/user - in one synchronous
@@ -6575,7 +6624,7 @@ function ensureSharedWalletLoginReady() {
6575
6624
  if (!sharedWalletReady) {
6576
6625
  sharedWalletReady = (async () => {
6577
6626
  await core.getConfig();
6578
- await Promise.resolve().then(function () { return require('./injected-wallet-provider-LTnqoaYD.js'); });
6627
+ await Promise.resolve().then(function () { return require('./injected-wallet-provider-D8vIsUHf.js'); });
6579
6628
  })().catch((error) => {
6580
6629
  sharedWalletReady = null;
6581
6630
  throw error;
@@ -6678,7 +6727,7 @@ async function loginWithWallet(options) {
6678
6727
  await ensureSharedWalletLoginReady();
6679
6728
  else
6680
6729
  await ensureWalletLoginReady();
6681
- const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-LTnqoaYD.js'); });
6730
+ const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-D8vIsUHf.js'); });
6682
6731
  // Keep the RAW wallet provider: the offchain wrapper forwards only a fixed
6683
6732
  // set of methods, so cleanup below must speak to the provider that actually
6684
6733
  // holds the config rather than to whatever is wrapping it.
@@ -7045,6 +7094,13 @@ function registerStorageBridge() {
7045
7094
  void syncStoredSession().catch((error) => console.warn('Bounded session synchronization failed:', error));
7046
7095
  });
7047
7096
  }
7097
+ function sameSessionIdentity(next, previous) {
7098
+ if (!next || !previous)
7099
+ return false;
7100
+ const a = core.deriveUserIdentityFromIdToken(next);
7101
+ const b = core.deriveUserIdentityFromIdToken(previous);
7102
+ return !!a.id && a.id === b.id && a.address === b.address && a.email === b.email && a.evmAddress === b.evmAddress;
7103
+ }
7048
7104
  async function syncStoredSession() {
7049
7105
  const config = initializedConfig;
7050
7106
  if (!config)
@@ -7053,6 +7109,18 @@ async function syncStoredSession() {
7053
7109
  const issuer = core.WebSessionManager.getIssuer();
7054
7110
  if (token === observedIdToken && issuer === observedIssuer)
7055
7111
  return;
7112
+ // Another tab refreshed this same session (tokens rotate every few minutes):
7113
+ // who is signed in did not change, so keep the user and its live reads, whose
7114
+ // sockets take the new token on their own, instead of hiding and restoring it.
7115
+ if (currentUser && issuer === observedIssuer && sameSessionIdentity(token, observedIdToken)) {
7116
+ const record = await core.WebSessionManager.readSessionWithGeneration();
7117
+ if (config !== initializedConfig || core.WebSessionManager.getIdToken() !== token)
7118
+ return;
7119
+ if ((record === null || record === void 0 ? void 0 : record.idToken) === token && (!record.appId || record.appId === config.appId)) {
7120
+ observedIdToken = token;
7121
+ return;
7122
+ }
7123
+ }
7056
7124
  observedIdToken = token;
7057
7125
  observedIssuer = issuer;
7058
7126
  const generation = ++initializationGeneration;
@@ -7847,10 +7915,18 @@ async function startTurnkeyEmailLogin(email) {
7847
7915
  if (!hasDOM$1()) {
7848
7916
  throw new Error('Turnkey-native email login is web-only - on React Native use loginWithRedirect()');
7849
7917
  }
7918
+ const generation = getInitializationGeneration();
7919
+ const walletGuard = walletOperationGuard();
7920
+ const assertContext = () => {
7921
+ walletGuard();
7922
+ if (generation !== getInitializationGeneration())
7923
+ throw new Error('Login was superseded by SDK reinitialization. Start sign-in again.');
7924
+ };
7850
7925
  const trimmed = email.trim();
7851
7926
  if (!trimmed)
7852
7927
  throw new Error('Enter your email to continue.');
7853
7928
  const { base, appId } = await inlineAuthEndpoint();
7929
+ assertContext();
7854
7930
  if (!appId)
7855
7931
  throw new Error('appId is not configured (init the SDK with your appId)');
7856
7932
  const res = await fetch(`${base}/wallet/turnkey/auth/init`, {
@@ -7862,6 +7938,7 @@ async function startTurnkeyEmailLogin(email) {
7862
7938
  if (!res.ok || !data || typeof data.otpId !== 'string' || typeof data.otpEncryptionTargetBundle !== 'string') {
7863
7939
  throw new Error(String((data && data.error) || 'Could not send a code. Please try again.'));
7864
7940
  }
7941
+ assertContext();
7865
7942
  const otpId = data.otpId;
7866
7943
  const otpEncryptionTargetBundle = data.otpEncryptionTargetBundle;
7867
7944
  // A tenant-origin login always gets a ceremony id; without one the OTP frame refuses to run
@@ -7877,14 +7954,20 @@ async function startTurnkeyEmailLogin(email) {
7877
7954
  : undefined;
7878
7955
  return {
7879
7956
  email: trimmed, frameUrl, signerOrigin,
7880
- run(iframe, _signal) {
7957
+ run(iframe, signal) {
7958
+ try {
7959
+ assertContext();
7960
+ }
7961
+ catch (error) {
7962
+ return Promise.reject(error);
7963
+ }
7881
7964
  if (!ceremonyId) {
7882
7965
  return Promise.reject(new Error('This app is not configured for in-frame wallet login. Please try again.'));
7883
7966
  }
7884
- const run = { iframe, base, signerOrigin, appId, email: trimmed, otpId, otpEncryptionTargetBundle, ceremonyId };
7967
+ const run = { iframe, base, signerOrigin, appId, email: trimmed, otpId, otpEncryptionTargetBundle, ceremonyId, generation };
7885
7968
  // BSA-2026-004: the login code is typed only in /wallet/turnkey/otp-frame.
7886
7969
  // The scoped signer URL is for post-login signing, not OTP entry.
7887
- return runOtpFrame(run);
7970
+ return runOtpFrame(run, signal);
7888
7971
  },
7889
7972
  };
7890
7973
  }
@@ -7893,7 +7976,7 @@ async function startTurnkeyEmailLogin(email) {
7893
7976
  // frame 'result' {session} -> we establish the inline session and resolve the User.
7894
7977
  // When the frame asks for the signing-session authorization ('need-authorization') we mint a
7895
7978
  // createWallet action authorization on THIS (registered) origin and post it in - never a key.
7896
- function runOtpFrame(run) {
7979
+ function runOtpFrame(run, signal) {
7897
7980
  const tenantOrigin = window.location.origin;
7898
7981
  const url = `${run.base.replace(/\/$/, '')}/wallet/turnkey/otp-frame`
7899
7982
  + `?appId=${encodeURIComponent(run.appId)}`
@@ -7903,13 +7986,48 @@ function runOtpFrame(run) {
7903
7986
  return new Promise((resolve, reject) => {
7904
7987
  let settled = false;
7905
7988
  let started = false;
7906
- const cleanup = () => { settled = true; window.removeEventListener('message', onMsg); window.clearTimeout(timer); };
7989
+ let receivingResult = false;
7990
+ let committing = false;
7991
+ const walletGuard = walletLoginGuard(signal);
7992
+ const beforeCommit = () => {
7993
+ if (settled)
7994
+ throw new Error('wallet_login_cancelled');
7995
+ walletGuard();
7996
+ if (run.generation !== getInitializationGeneration())
7997
+ throw new Error('Login was superseded by SDK reinitialization. Start sign-in again.');
7998
+ };
7999
+ const finish = (error, user) => {
8000
+ if (settled)
8001
+ return;
8002
+ settled = true;
8003
+ window.removeEventListener('message', onMsg);
8004
+ window.clearTimeout(timer);
8005
+ signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
8006
+ if (error) {
8007
+ // Destroy the expired frame's code/key realm as well as its parent listener.
8008
+ run.iframe.src = 'about:blank';
8009
+ reject(error);
8010
+ }
8011
+ else
8012
+ resolve(user);
8013
+ };
8014
+ const onAbort = () => { if (!committing)
8015
+ finish(new Error('cancelled')); };
7907
8016
  const onMsg = async (event) => {
7908
8017
  if (settled)
7909
8018
  return;
7910
8019
  // Trust ONLY the signer origin and the frame's own window.
7911
8020
  if (event.origin !== run.signerOrigin || event.source !== run.iframe.contentWindow)
7912
8021
  return;
8022
+ if (!committing) {
8023
+ try {
8024
+ beforeCommit();
8025
+ }
8026
+ catch (error) {
8027
+ finish(error instanceof Error ? error : new Error(String(error)));
8028
+ return;
8029
+ }
8030
+ }
7913
8031
  const d = event.data;
7914
8032
  if (!d || typeof d !== 'object')
7915
8033
  return;
@@ -7952,40 +8070,56 @@ function runOtpFrame(run) {
7952
8070
  return;
7953
8071
  }
7954
8072
  if (d.type === 'bounded:otp:result') {
8073
+ if (receivingResult)
8074
+ return;
8075
+ receivingResult = true;
7955
8076
  if (d.ok && d.session && typeof d.session === 'object') {
7956
8077
  try {
7957
- const user = await establishInlineSession(d.session);
7958
- cleanup();
7959
- resolve(user);
8078
+ const user = await establishInlineSession(d.session, beforeCommit, undefined, () => { committing = true; });
8079
+ finish(undefined, user);
7960
8080
  }
7961
8081
  catch (error) {
7962
- cleanup();
7963
- reject(error instanceof Error ? error : new Error(String(error)));
8082
+ finish(error instanceof Error ? error : new Error(String(error)));
7964
8083
  }
7965
8084
  return;
7966
8085
  }
7967
- cleanup();
7968
- reject(new Error(String((d && d.error) || 'Invalid or expired code.')));
8086
+ finish(new Error(String((d && d.error) || 'Invalid or expired code.')));
7969
8087
  return;
7970
8088
  }
7971
8089
  // 'bounded:otp:registered' is advisory; login does not depend on it.
7972
8090
  };
7973
8091
  const post = (msg) => {
7974
8092
  var _a;
8093
+ if (settled)
8094
+ return;
8095
+ if (!committing) {
8096
+ try {
8097
+ beforeCommit();
8098
+ }
8099
+ catch (_b) {
8100
+ return;
8101
+ }
8102
+ }
7975
8103
  try {
7976
8104
  (_a = run.iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage(msg, run.signerOrigin);
7977
8105
  }
7978
- catch ( /* frame gone */_b) { /* frame gone */ }
8106
+ catch ( /* frame gone */_c) { /* frame gone */ }
7979
8107
  };
7980
8108
  window.addEventListener('message', onMsg);
7981
8109
  // 3 minutes: the human must read the email and type the code. A frame that never reports is a
7982
8110
  // failed login the user can retry.
7983
8111
  const timer = window.setTimeout(() => {
7984
- if (settled)
7985
- return;
7986
- cleanup();
7987
- reject(new Error('The sign-in code entry timed out. Please try again.'));
8112
+ if (!committing)
8113
+ finish(new Error('The sign-in code entry timed out. Please try again.'));
7988
8114
  }, 3 * 60 * 1000);
8115
+ signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
8116
+ try {
8117
+ beforeCommit();
8118
+ }
8119
+ catch (error) {
8120
+ finish(error instanceof Error ? error : new Error(String(error)));
8121
+ return;
8122
+ }
7989
8123
  run.iframe.src = url;
7990
8124
  });
7991
8125
  }
@@ -8014,8 +8148,8 @@ const CARD_CSS = `
8014
8148
  --line:rgba(232,230,222,.08);--line-strong:rgba(232,230,222,.16);
8015
8149
  --text:#e8e6de;--dim:#9b9991;--faint:#85837c;
8016
8150
  --green:#58c98f;--green-bg:rgba(88,201,143,.09);--green-line:rgba(88,201,143,.28);--red:#ec8488;
8017
- --disp:"Newsreader","Iowan Old Style",Georgia,serif;
8018
- --body:"Schibsted Grotesk","Helvetica Neue",Arial,sans-serif;
8151
+ --disp:"Bounded Newsreader","Newsreader","Iowan Old Style",Georgia,serif;
8152
+ --body:"Bounded Schibsted Grotesk","Schibsted Grotesk","Helvetica Neue",Arial,sans-serif;
8019
8153
  position:relative;padding:34px 32px 20px;color:var(--text);font-family:var(--body);
8020
8154
  -webkit-font-smoothing:antialiased}
8021
8155
  .x{position:absolute;top:20px;right:24px;border:0;background:transparent;color:var(--faint);
@@ -8136,6 +8270,17 @@ function discoverWallets(configuredNetwork) {
8136
8270
  // beats a click that does nothing, which is what "one at a time" looks like from
8137
8271
  // the outside.
8138
8272
  const BUSY_MSG = "A sign-in is already in progress - finish or cancel it first.";
8273
+ // Shown instead of an empty card when no lane can be offered (wallet-only app,
8274
+ // no wallet in this browser). A phone browser never sees installed wallet apps
8275
+ // as injected providers, so the honest advice there is a different browser,
8276
+ // not "install a wallet".
8277
+ function noLaneCopy(mobile) {
8278
+ return mobile
8279
+ ? "No wallet is available in this browser. Open this page in your wallet app's " +
8280
+ "browser, or on a desktop browser with a wallet extension, then try again."
8281
+ : "No wallet was found in this browser. Install a Solana wallet extension such as " +
8282
+ "Phantom or Solflare, reload the page, then try again.";
8283
+ }
8139
8284
  const METHOD_LABEL = {
8140
8285
  google: "Continue with Google",
8141
8286
  apple: "Continue with Apple",
@@ -8477,12 +8622,9 @@ async function prepareAndOpen(opts, pending) {
8477
8622
  cancelPendingOpens();
8478
8623
  };
8479
8624
  m.onBackdrop = cancel;
8480
- const style = el("style");
8481
- style.textContent = CARD_CSS;
8482
8625
  let emailDraft = "";
8483
8626
  const renderChoose = (initialError) => {
8484
8627
  const card = el("div", "c");
8485
- card.appendChild(style.cloneNode(true));
8486
8628
  const x = el("button", "x", "Close");
8487
8629
  x.type = "button";
8488
8630
  x.setAttribute("aria-label", "Close");
@@ -8699,6 +8841,24 @@ async function prepareAndOpen(opts, pending) {
8699
8841
  });
8700
8842
  };
8701
8843
  }
8844
+ // A card with no lane at all. It happens when an app asks for wallet-only
8845
+ // (`methods: []`, `wallet: true`) and this browser has no wallet to offer:
8846
+ // no injected wallet, and no Solana Mobile wallet (off-Android, or the app
8847
+ // never installed the adapter). A blank card between the title and the
8848
+ // footer was the shipped behaviour, and it reads as "the site is broken"
8849
+ // to the person and tells the developer nothing. Say what is missing on
8850
+ // screen, and name the usual cause once in the console, where the
8851
+ // developer's own preview run will show it.
8852
+ if (!hasEmail && social.length === 0 && !walletEnabled) {
8853
+ card.appendChild(el("p", "sub", noLaneCopy(detectMobile())));
8854
+ console.warn("[Bounded] openBoundedWidget has no sign-in lane to show: the app asked for " +
8855
+ "no email or social method and no wallet is available in this browser. " +
8856
+ (offerMobile
8857
+ ? "On Android the Solana Mobile wallet is offered only when the app installs " +
8858
+ "'@solana-mobile/wallet-standard-mobile' (see the warning above if registration failed); "
8859
+ : "") +
8860
+ "Add a method, or make sure wallet login is enabled and the mobile adapter is installed.");
8861
+ }
8702
8862
  card.appendChild(status);
8703
8863
  const foot = el("div", "foot");
8704
8864
  const poweredBy = el("a", undefined, "Powered by Bounded");
@@ -8745,7 +8905,6 @@ async function prepareAndOpen(opts, pending) {
8745
8905
  const renderSignInGesture = (choice, done) => {
8746
8906
  var _a;
8747
8907
  const card = el("div", "c");
8748
- card.appendChild(style.cloneNode(true));
8749
8908
  const x = el("button", "x", "Close");
8750
8909
  x.type = "button";
8751
8910
  x.setAttribute("aria-label", "Close");
@@ -8766,7 +8925,6 @@ async function prepareAndOpen(opts, pending) {
8766
8925
  };
8767
8926
  const renderWallets = (initialError) => {
8768
8927
  const card = el("div", "c");
8769
- card.appendChild(style.cloneNode(true));
8770
8928
  const back = el("button", "back", "\u2190 Back");
8771
8929
  back.type = "button";
8772
8930
  // Leaving the wallet list supersedes any attempt still preparing, so it
@@ -8846,7 +9004,6 @@ async function prepareAndOpen(opts, pending) {
8846
9004
  // experience still feels native; the code input, sealing, and verify all live behind the frame.
8847
9005
  const renderOtpFrame = (handle) => {
8848
9006
  const card = el("div", "c");
8849
- card.appendChild(style.cloneNode(true));
8850
9007
  const back = el("button", "back", "\u2190 Back");
8851
9008
  back.type = "button";
8852
9009
  card.appendChild(back);
@@ -8874,14 +9031,16 @@ async function prepareAndOpen(opts, pending) {
8874
9031
  setErr(BUSY_MSG);
8875
9032
  return card;
8876
9033
  }
8877
- back.addEventListener("click", () => { settleAttempt(attempt); invalidateAttempts(); swap(renderChoose()); });
9034
+ back.addEventListener("click", () => { invalidateAttempts(); swap(renderChoose()); });
9035
+ const controller = new AbortController();
9036
+ abortScopedLogin = () => controller.abort();
8878
9037
  handle
8879
- .run(frame)
9038
+ .run(frame, controller.signal)
8880
9039
  .then((u) => { settleAttempt(attempt); succeed(u); })
8881
9040
  .catch((err) => {
8882
9041
  if (!settleAttempt(attempt))
8883
9042
  return;
8884
- setErr(errText(err));
9043
+ swap(renderChoose(errText(err)));
8885
9044
  });
8886
9045
  return card;
8887
9046
  };
@@ -8891,7 +9050,7 @@ async function prepareAndOpen(opts, pending) {
8891
9050
  // land after a newer open has taken over the DOM.
8892
9051
  if (!owner.owns())
8893
9052
  return;
8894
- mountContent(m, node);
9053
+ mountContent(m, node, CARD_CSS);
8895
9054
  requestAnimationFrame(() => {
8896
9055
  if (owner.owns())
8897
9056
  fitPanelToContent(m);
@@ -11283,4 +11442,4 @@ exports.useAuth = useAuth;
11283
11442
  exports.useBuildRun = useBuildRun;
11284
11443
  exports.useQuery = useQuery;
11285
11444
  exports.waitForAuthReady = waitForAuthReady;
11286
- //# sourceMappingURL=index-Brdwqgqs.js.map
11445
+ //# sourceMappingURL=index-BmNNZlTl.js.map