@bounded-sh/client 0.0.58 → 0.0.60

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 (71) hide show
  1. package/dist/auth/index.d.ts +1 -1
  2. package/dist/{email-auth-provider-BHtR1H_E.js → email-auth-provider-B5yIZe1l.js} +3 -3
  3. package/dist/email-auth-provider-B5yIZe1l.js.map +1 -0
  4. package/dist/{email-auth-provider-BlJI-8w9.js → email-auth-provider-BCEdcnRu.js} +3 -3
  5. package/dist/email-auth-provider-BCEdcnRu.js.map +1 -0
  6. package/dist/{email-auth-provider-CfNSS8Du.mjs → email-auth-provider-BF-vvbAt.mjs} +3 -3
  7. package/dist/email-auth-provider-BF-vvbAt.mjs.map +1 -0
  8. package/dist/{email-auth-provider-CX7foEhA.mjs → email-auth-provider-nBv7mW9q.mjs} +3 -3
  9. package/dist/email-auth-provider-nBv7mW9q.mjs.map +1 -0
  10. package/dist/{guest-auth-provider-BPXDJoEQ.js → guest-auth-provider-CZ-CpNhD.js} +7 -7
  11. package/dist/guest-auth-provider-CZ-CpNhD.js.map +1 -0
  12. package/dist/{guest-auth-provider-BRjJV8Zq.js → guest-auth-provider-CrRreOyf.js} +7 -7
  13. package/dist/guest-auth-provider-CrRreOyf.js.map +1 -0
  14. package/dist/{guest-auth-provider-DxhZ5sWN.mjs → guest-auth-provider-DFNAHmtY.mjs} +7 -7
  15. package/dist/guest-auth-provider-DFNAHmtY.mjs.map +1 -0
  16. package/dist/{guest-auth-provider-DJmuReVa.mjs → guest-auth-provider-DGvQgIwm.mjs} +7 -7
  17. package/dist/guest-auth-provider-DGvQgIwm.mjs.map +1 -0
  18. package/dist/{index-DWodlTFG.mjs → index-CQtGkKVd.mjs} +284 -38
  19. package/dist/index-CQtGkKVd.mjs.map +1 -0
  20. package/dist/{index-MKE9tHpl.js → index-iu8vB4ye.js} +285 -36
  21. package/dist/index-iu8vB4ye.js.map +1 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +4 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/index.mjs +1 -1
  26. package/dist/{index.native-hu3o3zXU.js → index.native-CDfx0N6p.js} +274 -25
  27. package/dist/index.native-CDfx0N6p.js.map +1 -0
  28. package/dist/{index.native-CQiw9baS.mjs → index.native-Crfn_NOB.mjs} +273 -27
  29. package/dist/index.native-Crfn_NOB.mjs.map +1 -0
  30. package/dist/index.native.d.ts +2 -0
  31. package/dist/index.native.js +4 -1
  32. package/dist/index.native.js.map +1 -1
  33. package/dist/index.native.mjs +1 -1
  34. package/dist/{injected-evm-wallet-provider-DCGnUedJ.mjs → injected-evm-wallet-provider-Bi9G3X35.mjs} +4 -4
  35. package/dist/injected-evm-wallet-provider-Bi9G3X35.mjs.map +1 -0
  36. package/dist/{injected-evm-wallet-provider-Xbt2CUPi.js → injected-evm-wallet-provider-CGa692L1.js} +4 -4
  37. package/dist/injected-evm-wallet-provider-CGa692L1.js.map +1 -0
  38. package/dist/{injected-evm-wallet-provider-CnJRLE8P.mjs → injected-evm-wallet-provider-D8lYGcM2.mjs} +4 -4
  39. package/dist/injected-evm-wallet-provider-D8lYGcM2.mjs.map +1 -0
  40. package/dist/{injected-evm-wallet-provider-BOPzdm7E.js → injected-evm-wallet-provider-LTWiACl5.js} +4 -4
  41. package/dist/injected-evm-wallet-provider-LTWiACl5.js.map +1 -0
  42. package/dist/{injected-wallet-provider-Bps6a9IP.mjs → injected-wallet-provider-CcyA5JBS.mjs} +3 -3
  43. package/dist/injected-wallet-provider-CcyA5JBS.mjs.map +1 -0
  44. package/dist/{injected-wallet-provider-BUvN5Y6_.js → injected-wallet-provider-DYZFxIdd.js} +3 -3
  45. package/dist/injected-wallet-provider-DYZFxIdd.js.map +1 -0
  46. package/dist/{injected-wallet-provider-7ep-jcal.js → injected-wallet-provider-TPx8Wzvq.js} +3 -3
  47. package/dist/injected-wallet-provider-TPx8Wzvq.js.map +1 -0
  48. package/dist/{injected-wallet-provider-Dg3MieTq.mjs → injected-wallet-provider-kx8tcMKM.mjs} +3 -3
  49. package/dist/injected-wallet-provider-kx8tcMKM.mjs.map +1 -0
  50. package/dist/onramp.d.ts +72 -0
  51. package/package.json +2 -2
  52. package/dist/email-auth-provider-BHtR1H_E.js.map +0 -1
  53. package/dist/email-auth-provider-BlJI-8w9.js.map +0 -1
  54. package/dist/email-auth-provider-CX7foEhA.mjs.map +0 -1
  55. package/dist/email-auth-provider-CfNSS8Du.mjs.map +0 -1
  56. package/dist/guest-auth-provider-BPXDJoEQ.js.map +0 -1
  57. package/dist/guest-auth-provider-BRjJV8Zq.js.map +0 -1
  58. package/dist/guest-auth-provider-DJmuReVa.mjs.map +0 -1
  59. package/dist/guest-auth-provider-DxhZ5sWN.mjs.map +0 -1
  60. package/dist/index-DWodlTFG.mjs.map +0 -1
  61. package/dist/index-MKE9tHpl.js.map +0 -1
  62. package/dist/index.native-CQiw9baS.mjs.map +0 -1
  63. package/dist/index.native-hu3o3zXU.js.map +0 -1
  64. package/dist/injected-evm-wallet-provider-BOPzdm7E.js.map +0 -1
  65. package/dist/injected-evm-wallet-provider-CnJRLE8P.mjs.map +0 -1
  66. package/dist/injected-evm-wallet-provider-DCGnUedJ.mjs.map +0 -1
  67. package/dist/injected-evm-wallet-provider-Xbt2CUPi.js.map +0 -1
  68. package/dist/injected-wallet-provider-7ep-jcal.js.map +0 -1
  69. package/dist/injected-wallet-provider-BUvN5Y6_.js.map +0 -1
  70. package/dist/injected-wallet-provider-Bps6a9IP.mjs.map +0 -1
  71. package/dist/injected-wallet-provider-Dg3MieTq.mjs.map +0 -1
@@ -3475,7 +3475,7 @@ const EMBEDDED_WALLET_UNSUPPORTED = 'Unsupported for embedded smart wallets —
3475
3475
  const NO_EMBEDDED_WALLET = 'This login has no embedded wallet. Enable `"auth": { "wallets": true }` in ' +
3476
3476
  'the app policy so email logins get a non-custodial wallet (@user.address), ' +
3477
3477
  'then sign from a user gesture.';
3478
- function hasDOM$4() {
3478
+ function hasDOM$5() {
3479
3479
  return typeof window !== 'undefined' && typeof document !== 'undefined';
3480
3480
  }
3481
3481
  function decodeJwt$1(token) {
@@ -3535,7 +3535,7 @@ let requestSeq$1 = 0;
3535
3535
  * wallet's owner, and validates that the wallet address matches the account.
3536
3536
  */
3537
3537
  async function signViaSignerPopup(action) {
3538
- if (!hasDOM$4()) {
3538
+ if (!hasDOM$5()) {
3539
3539
  throw new Error('Embedded-wallet signing is only available in the browser (popup-based).');
3540
3540
  }
3541
3541
  const isEvm = action.kind === 'evmTransaction';
@@ -3711,7 +3711,7 @@ const STORED_AUTH_METHOD_KEY$1 = 'bounded_last_auth_method';
3711
3711
  // not by `typeof window` (truthy on RN). OIDC redirect/popup is web-only, but the
3712
3712
  // session it mints is read back through this same selector everywhere.
3713
3713
  const sessionManager = core.getActiveSessionManager;
3714
- function hasDOM$3() {
3714
+ function hasDOM$4() {
3715
3715
  return typeof window !== 'undefined' && typeof document !== 'undefined';
3716
3716
  }
3717
3717
  function decodeJwt(token) {
@@ -3742,7 +3742,7 @@ const oidcProviderSingleton = {
3742
3742
  const idToken = sessionManager().getIdToken();
3743
3743
  return idToken ? userFromIdToken(idToken) : null;
3744
3744
  },
3745
- async logout() { sessionManager().clearSession(); },
3745
+ async logout() { await sessionManager().clearSession(); },
3746
3746
  async signMessage(_message) {
3747
3747
  throw new Error(hasEmbeddedWallet() ? EMBEDDED_WALLET_UNSUPPORTED : OIDC_NO_WALLET);
3748
3748
  },
@@ -3851,7 +3851,7 @@ async function buildAuthorize(opts) {
3851
3851
  throw new Error('appId is not configured (init the SDK with your appId)');
3852
3852
  // Web: default to the current page so callers don't have to pass redirectUri. Native has no
3853
3853
  // window — it MUST pass an https universal link (the system auth session needs a real URL).
3854
- const redirectUri = opts.redirectUri || (hasDOM$3() ? window.location.origin + window.location.pathname : undefined);
3854
+ const redirectUri = opts.redirectUri || (hasDOM$4() ? window.location.origin + window.location.pathname : undefined);
3855
3855
  if (!redirectUri)
3856
3856
  throw new Error('redirectUri is required on React Native — pass the https universal link the app owner registered.');
3857
3857
  const { verifier, challenge } = await makePkce();
@@ -3959,7 +3959,7 @@ async function runNativeLogin(opts) {
3959
3959
  async function loginWithRedirect(opts) {
3960
3960
  if (isNative())
3961
3961
  return runNativeLogin(opts);
3962
- if (!hasDOM$3())
3962
+ if (!hasDOM$4())
3963
3963
  throw new Error('loginWithRedirect requires a browser or a configured React Native platform');
3964
3964
  const { url, pkce } = await buildAuthorize(opts);
3965
3965
  // Stash the verifier + state so the redirect_uri page can finish the exchange. sessionStorage
@@ -3975,7 +3975,7 @@ async function loginWithRedirect(opts) {
3975
3975
  * loginWithRedirect() (which resolves with the User), so there is no separate redirect
3976
3976
  * page to finish from. Calling it on RN is harmless. */
3977
3977
  async function completeLoginFromRedirect() {
3978
- if (!hasDOM$3())
3978
+ if (!hasDOM$4())
3979
3979
  return null;
3980
3980
  const url = new URL(window.location.href);
3981
3981
  const code = url.searchParams.get('code');
@@ -4021,7 +4021,7 @@ async function completeLoginFromRedirect() {
4021
4021
  * redirect_uri page posts the code back. The redirect_uri page just calls the usual
4022
4022
  * completeLoginFromRedirect() (which auto-detects the popup) — no separate call needed. */
4023
4023
  async function loginWithPopup(opts) {
4024
- if (!hasDOM$3())
4024
+ if (!hasDOM$4())
4025
4025
  throw new Error('loginWithPopup is web-only — on React Native use loginWithRedirect()');
4026
4026
  // Open synchronously, before PKCE/config awaits consume the click's user-
4027
4027
  // activation window. Opening only after buildAuthorize() intermittently
@@ -4161,7 +4161,7 @@ async function loginWithPopup(opts) {
4161
4161
  /** Call this on the redirect_uri page when login was opened via loginWithPopup(): posts the
4162
4162
  * code back to the opener and closes. (For the redirect variant use completeLoginFromRedirect.) */
4163
4163
  function completeLoginInPopup(openerOrigin) {
4164
- if (!hasDOM$3() || !window.opener)
4164
+ if (!hasDOM$4() || !window.opener)
4165
4165
  return;
4166
4166
  const url = new URL(window.location.href);
4167
4167
  const code = url.searchParams.get('code');
@@ -4390,15 +4390,20 @@ function authProviderConfigKey(config) {
4390
4390
  * so both web (WebSessionManager) and RN (ReactNativeSessionManager) sessions
4391
4391
  * are properly cleared.
4392
4392
  */
4393
- function clearIncompatibleSession() {
4393
+ async function clearIncompatibleSession() {
4394
4394
  if (getStoredAuthMethod() === currentAuthMethod) {
4395
4395
  return false;
4396
4396
  }
4397
4397
  // Clear both managers: on web the RN manager is a no-op (not configured),
4398
4398
  // and on RN the web manager is a no-op (no localStorage).
4399
- core.WebSessionManager.clearSession();
4399
+ //
4400
+ // AWAITED. Session removal is serialized through a cross-tab lock now, so a
4401
+ // fire-and-forget call would let init() reach setCurrentUser while the old
4402
+ // credential was still readable — and a request issued in that gap would
4403
+ // authenticate as the session we are in the middle of discarding.
4404
+ await core.WebSessionManager.clearSession();
4400
4405
  try {
4401
- core.ReactNativeSessionManager.clearSession();
4406
+ await core.ReactNativeSessionManager.clearSession();
4402
4407
  }
4403
4408
  catch ( /* not configured on web */_a) { /* not configured on web */ }
4404
4409
  setStoredAuthMethod(null);
@@ -4531,7 +4536,7 @@ async function getAuthProvider(config) {
4531
4536
  // manager as legacy email sessions. Human login should be started
4532
4537
  // with loginWithRedirect(); this provider is kept as the restore and
4533
4538
  // logout surface for those sessions.
4534
- currentAuthProvider = new (await Promise.resolve().then(function () { return require('./email-auth-provider-BHtR1H_E.js'); })).EmailAuthProvider();
4539
+ currentAuthProvider = new (await Promise.resolve().then(function () { return require('./email-auth-provider-B5yIZe1l.js'); })).EmailAuthProvider();
4535
4540
  break;
4536
4541
  case "wallet":
4537
4542
  case "phantom": {
@@ -4550,7 +4555,7 @@ async function getAuthProvider(config) {
4550
4555
  if (!isWalletLoginEnabled(config)) {
4551
4556
  throw walletLoginNotEnabledError(authMethod);
4552
4557
  }
4553
- const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-BUvN5Y6_.js'); });
4558
+ const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-DYZFxIdd.js'); });
4554
4559
  currentAuthProvider = new InjectedWalletProvider(resolveInjectedWalletConfig(config), rpcUrl, configuredNetwork);
4555
4560
  break;
4556
4561
  }
@@ -4563,7 +4568,7 @@ async function getAuthProvider(config) {
4563
4568
  // Selecting authMethod:'evm-wallet' IS the opt-in — pass evmWalletConfig
4564
4569
  // to point at a specific wallet or set chainId/domain.
4565
4570
  const evmWalletConfig = ((_a = config.evmWalletConfig) !== null && _a !== void 0 ? _a : {});
4566
- const { InjectedEvmWalletProvider } = await Promise.resolve().then(function () { return require('./injected-evm-wallet-provider-BOPzdm7E.js'); });
4571
+ const { InjectedEvmWalletProvider } = await Promise.resolve().then(function () { return require('./injected-evm-wallet-provider-LTWiACl5.js'); });
4567
4572
  currentAuthProvider = new InjectedEvmWalletProvider(evmWalletConfig);
4568
4573
  break;
4569
4574
  }
@@ -4591,7 +4596,7 @@ async function getAuthProvider(config) {
4591
4596
  break;
4592
4597
  case "guest":
4593
4598
  // Anonymous auth: a device-local ed25519 keypair signs the challenge.
4594
- currentAuthProvider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-BPXDJoEQ.js'); })).GuestAuthProvider();
4599
+ currentAuthProvider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-CZ-CpNhD.js'); })).GuestAuthProvider();
4595
4600
  break;
4596
4601
  }
4597
4602
  if (!currentAuthProvider) {
@@ -4647,7 +4652,7 @@ async function signInAnonymously() {
4647
4652
  // Lazy-load the guest provider so its ed25519/web3 dependencies stay out of
4648
4653
  // the main bundle for data-only apps. Specifier is a static string literal
4649
4654
  // so bundle-splitting can see it.
4650
- const provider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-BPXDJoEQ.js'); })).GuestAuthProvider();
4655
+ const provider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-CZ-CpNhD.js'); })).GuestAuthProvider();
4651
4656
  setAuthLoading(true); // reflect "signing in…" while the guest keypair session is minted
4652
4657
  const commit = (user) => {
4653
4658
  // No await/interleaving inside this block: provider/method/config/marker
@@ -4689,7 +4694,7 @@ async function signInAnonymously() {
4689
4694
  }
4690
4695
  catch (_b) {
4691
4696
  try {
4692
- core.getActiveSessionManager().clearSession();
4697
+ await core.getActiveSessionManager().clearSession();
4693
4698
  }
4694
4699
  catch ( /* fail closed below */_c) { /* fail closed below */ }
4695
4700
  }
@@ -4798,7 +4803,7 @@ async function loginWithWallet(options) {
4798
4803
  ? Object.assign(Object.assign({}, base), { getProvider: options.getProvider }) : base;
4799
4804
  const rpcUrl = normalizeSolanaRpcUrl(cfg.rpcUrl);
4800
4805
  const network = getConfiguredSolanaNetwork(cfg);
4801
- const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-BUvN5Y6_.js'); });
4806
+ const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-DYZFxIdd.js'); });
4802
4807
  let provider = new InjectedWalletProvider(injectedConfig, rpcUrl, network);
4803
4808
  if (coreConfig.chain === 'offchain')
4804
4809
  provider = new OffchainAuthProvider(provider);
@@ -4870,7 +4875,7 @@ function getCurrentAuthMethod() {
4870
4875
  // allowlist here: on a trusted origin we end the issuer session with a
4871
4876
  // top-level bounce; anywhere else (custom domains, http dev) we keep the
4872
4877
  // local-only logout instead of stranding the user on the issuer origin.
4873
- function issuerLogoutBounceUrl(issuerBase) {
4878
+ function issuerLogoutBounceUrl(issuerBase, idTokenHint) {
4874
4879
  if (typeof window === 'undefined' || typeof document === 'undefined')
4875
4880
  return null; // RN/Node: no shared cookie jar to clear this way
4876
4881
  try {
@@ -4893,12 +4898,27 @@ function issuerLogoutBounceUrl(issuerBase) {
4893
4898
  if (!trusted)
4894
4899
  return null;
4895
4900
  const base = issuerBase.replace(/\/$/, '');
4896
- return `${base}/logout?redirect_uri=${encodeURIComponent(here.href)}`;
4901
+ // #228: send the id_token_hint so the issuer can bind the post-logout redirect to this
4902
+ // session + app + registered origin. Without it the issuer safely falls back to its own
4903
+ // origin (it no longer trusts a bare suffix match), so the return-to-app bounce is a no-op.
4904
+ const hint = idTokenHint ? `&id_token_hint=${encodeURIComponent(idTokenHint)}` : '';
4905
+ return `${base}/logout?redirect_uri=${encodeURIComponent(here.href)}${hint}`;
4897
4906
  }
4898
4907
  catch (_b) {
4899
4908
  return null;
4900
4909
  }
4901
4910
  }
4911
+ // #228 helper, deliberately OUTSIDE logout(): the logout body is source-guarded
4912
+ // against any `catch` so a revokeSession failure can never be swallowed, and a
4913
+ // missing id token is the one benign absence worth tolerating here.
4914
+ function idTokenHintOrNull(sm) {
4915
+ try {
4916
+ return sm.getIdToken();
4917
+ }
4918
+ catch (_a) {
4919
+ return null; /* no id token to hint with */
4920
+ }
4921
+ }
4902
4922
  async function logout$1(options) {
4903
4923
  // Re-initialize the provider if it was cleared by a prior logout — the exact mirror
4904
4924
  // of login()'s self-heal above. A hosted popup/redirect login (loginWithPopup /
@@ -4923,6 +4943,9 @@ async function logout$1(options) {
4923
4943
  const sm = core.getActiveSessionManager();
4924
4944
  const refreshToken = sm.getRefreshToken();
4925
4945
  const sessionIssuer = sm.getIssuer();
4946
+ // #228: capture the id token NOW, before currentAuthProvider.logout() clears the session, so we
4947
+ // can hand it to the issuer /logout as an id_token_hint (which binds the post-logout redirect).
4948
+ const idTokenHint = idTokenHintOrNull(sm);
4926
4949
  if (refreshToken)
4927
4950
  await core.revokeSession(refreshToken, sessionIssuer !== null && sessionIssuer !== void 0 ? sessionIssuer : undefined);
4928
4951
  // Hosted sessions ('email' covers the whole hosted surface: email OTP +
@@ -4943,7 +4966,7 @@ async function logout$1(options) {
4943
4966
  const issuerBase = sessionIssuer
4944
4967
  || (await core.getConfig()).humanAuthApiUrl
4945
4968
  || 'https://auth.bounded.sh';
4946
- const bounce = issuerLogoutBounceUrl(issuerBase);
4969
+ const bounce = issuerLogoutBounceUrl(issuerBase, idTokenHint);
4947
4970
  // Local state is already cleared, so even if the navigation is stopped
4948
4971
  // (tab close, extension) the app-side logout has fully happened.
4949
4972
  if (bounce)
@@ -4972,6 +4995,22 @@ var index = /*#__PURE__*/Object.freeze({
4972
4995
  signInAnonymously: signInAnonymously
4973
4996
  });
4974
4997
 
4998
+ /**
4999
+ * Subscribed once per process, not per init(): re-registering on every init would
5000
+ * stack duplicate handlers on repeated initialization.
5001
+ */
5002
+ let sessionClearedBridgeRegistered = false;
5003
+ function registerSessionClearedBridge() {
5004
+ if (sessionClearedBridgeRegistered)
5005
+ return;
5006
+ sessionClearedBridgeRegistered = true;
5007
+ core.onSessionCleared(() => {
5008
+ // Core fires this only when storage was ACTUALLY cleared, never on a
5009
+ // transient refresh failure that preserved the session — so this cannot
5010
+ // sign anyone out over a network blip.
5011
+ setCurrentUser(null);
5012
+ });
5013
+ }
4975
5014
  /**
4976
5015
  * Read the active session's idToken synchronously from whichever store is live
4977
5016
  * for this runtime (WebSessionManager on web, ReactNativeSessionManager on RN —
@@ -5029,6 +5068,15 @@ async function init(newConfig) {
5029
5068
  // setting it here makes "no flash" the SDK's default behavior for every consumer.
5030
5069
  setAuthLoading(true);
5031
5070
  try {
5071
+ // Bridge core's "the stored session is gone" signal into SDK auth state.
5072
+ //
5073
+ // Core cannot import this package, and a session can die deep inside a
5074
+ // session manager (dead refresh token, rejected refresh, app-id mismatch).
5075
+ // Before this, setCurrentUser(null) was only ever called by logout() and
5076
+ // init(), so storage could be wiped while `currentUser` stayed populated —
5077
+ // onAuthStateChanged never fired, and every consumer UI went on showing a
5078
+ // connected wallet for a session the server no longer knew about.
5079
+ registerSessionClearedBridge();
5032
5080
  // Get auth provider first
5033
5081
  authProviderInstance = await getAuthProvider(newConfig);
5034
5082
  // Initialize config with auth provider
@@ -5045,7 +5093,12 @@ async function init(newConfig) {
5045
5093
  let user = await authProviderInstance.restoreSession();
5046
5094
  // If a session exists but was created before auth method tracking (legacy session),
5047
5095
  // clear it to avoid mismatches when the app switches auth methods (e.g. email → phantom)
5048
- if (user && clearIncompatibleSession()) {
5096
+ //
5097
+ // AWAITED: the clear is serialized through a cross-tab lock now, and an
5098
+ // unawaited call would return a Promise (always truthy) — nulling the user
5099
+ // unconditionally AND letting setCurrentUser run while the old credential
5100
+ // was still in storage.
5101
+ if (user && await clearIncompatibleSession()) {
5049
5102
  user = null;
5050
5103
  }
5051
5104
  setCurrentUser(user);
@@ -5163,7 +5216,7 @@ function setAuthProviderInstance(provider) {
5163
5216
  // The modal is a warm singleton: it stays mounted (hidden) between uses so a
5164
5217
  // second open - or a signature right after login - is instant and, when it is
5165
5218
  // already open, a clean in-place view swap rather than a reopen.
5166
- function hasDOM$2() {
5219
+ function hasDOM$3() {
5167
5220
  return typeof window !== "undefined" && typeof document !== "undefined";
5168
5221
  }
5169
5222
  const OVERLAY_CSS = `
@@ -5385,7 +5438,7 @@ function detachIframe(m) {
5385
5438
  // no-op if already present or off-DOM. Fonts loaded here match the hosted auth page.
5386
5439
  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";
5387
5440
  function ensureFonts() {
5388
- if (!hasDOM$2())
5441
+ if (!hasDOM$3())
5389
5442
  return;
5390
5443
  if (document.querySelector('link[data-bounded-fonts="1"]'))
5391
5444
  return;
@@ -5691,7 +5744,7 @@ function discoverStandardWallets() {
5691
5744
  return out;
5692
5745
  }
5693
5746
 
5694
- function hasDOM$1() {
5747
+ function hasDOM$2() {
5695
5748
  return typeof window !== 'undefined' && typeof document !== 'undefined';
5696
5749
  }
5697
5750
  async function signerOrigin() {
@@ -5768,7 +5821,7 @@ function sealOtpInIframe(code, otpEncryptionTargetBundle) {
5768
5821
  * step needs the signer iframe); on React Native use loginWithRedirect() instead.
5769
5822
  */
5770
5823
  async function startTurnkeyEmailLogin(email) {
5771
- if (!hasDOM$1()) {
5824
+ if (!hasDOM$2()) {
5772
5825
  throw new Error('Turnkey-native email login is web-only - on React Native use loginWithRedirect()');
5773
5826
  }
5774
5827
  const trimmed = email.trim();
@@ -5957,7 +6010,7 @@ function el(tag, cls, text) {
5957
6010
  */
5958
6011
  async function openBoundedWidget(opts = {}) {
5959
6012
  var _a, _b, _c, _d, _e, _f, _g, _h;
5960
- if (!hasDOM$2()) {
6013
+ if (!hasDOM$3()) {
5961
6014
  throw new Error("openBoundedWidget is web-only - on React Native use loginWithRedirect()");
5962
6015
  }
5963
6016
  // Imported lazily to avoid a static cycle with the auth index barrel.
@@ -7064,7 +7117,7 @@ async function deserializeTransaction(tx) {
7064
7117
  * (Safari cross-origin create) or cannot be mounted.
7065
7118
  */
7066
7119
  async function runWidget(url, signerOrigin, ex, popupFallback) {
7067
- if (!hasDOM$2())
7120
+ if (!hasDOM$3())
7068
7121
  return popupFallback();
7069
7122
  const m = getModal();
7070
7123
  const requestId = String(ex.request.requestId);
@@ -7173,7 +7226,7 @@ async function runWidget(url, signerOrigin, ex, popupFallback) {
7173
7226
  // ONE ORIGIN, ONE PASSKEY: a WebAuthn passkey is bound to a single origin, so ALL
7174
7227
  // Bounded apps (any subdomain / custom domain) sign through this one origin - giving
7175
7228
  // every user one wallet + one passkey that roams across every app.
7176
- function hasDOM() {
7229
+ function hasDOM$1() {
7177
7230
  return typeof window !== 'undefined' && typeof document !== 'undefined';
7178
7231
  }
7179
7232
  // Safari (incl. iOS WebViews) is unreliable at running a WebAuthn CREATE inside a
@@ -7182,7 +7235,7 @@ function hasDOM() {
7182
7235
  // There we pick the popup up front, from the original click, so enrollment is solid.
7183
7236
  // Everywhere else the inline widget is used (buttery in-app modal).
7184
7237
  function preferPopup() {
7185
- if (!hasDOM() || typeof navigator === 'undefined')
7238
+ if (!hasDOM$1() || typeof navigator === 'undefined')
7186
7239
  return true;
7187
7240
  const ua = navigator.userAgent || '';
7188
7241
  return /^((?!chrome|android|crios|fxios|edg|edga|opr).)*safari/i.test(ua);
@@ -7299,7 +7352,7 @@ async function runPopup(url, signerOrigin, ex) {
7299
7352
  * Use this to learn the fee-payer address BEFORE building a transaction to sign.
7300
7353
  */
7301
7354
  async function getOrCreateTurnkeyWallet() {
7302
- if (!hasDOM())
7355
+ if (!hasDOM$1())
7303
7356
  throw new Error('Turnkey embedded wallets are only available in the browser (popup-based).');
7304
7357
  const { url, origin } = await turnkeySignerPage();
7305
7358
  const requestId = `tkcreate_${Date.now()}_${++requestSeq}`;
@@ -7323,7 +7376,7 @@ async function getOrCreateTurnkeyWallet() {
7323
7376
  * signatures are just a passkey approval.
7324
7377
  */
7325
7378
  async function signSolanaMessageViaTurnkey(message) {
7326
- if (!hasDOM()) {
7379
+ if (!hasDOM$1()) {
7327
7380
  throw new Error('Turnkey embedded-wallet signing is only available in the browser (popup-based).');
7328
7381
  }
7329
7382
  const { url, origin } = await turnkeySignerPage();
@@ -7340,6 +7393,199 @@ async function signSolanaMessageViaTurnkey(message) {
7340
7393
  });
7341
7394
  }
7342
7395
 
7396
+ // bounded.onramp() - fund the signed-in user's wallet with Coinbase Onramp.
7397
+ //
7398
+ // Web-only. Opens Coinbase's hosted checkout (pay.coinbase.com) in a popup
7399
+ // (new-tab fallback) using a single-use session token minted by the Bounded
7400
+ // platform at POST <origin>/__bounded/onramp/session on the app's own origin.
7401
+ // The platform holds the Coinbase credential; the app never sees it. The
7402
+ // session is bound server-side to the caller's verified Bounded JWT, and the
7403
+ // destination defaults to that user's embedded wallet.
7404
+ //
7405
+ // Amount semantics:
7406
+ // amountUsd - exact fiat in; the fee/output breakdown comes back as `quote`.
7407
+ // minAmountOut - a FLOOR on crypto received. The platform quotes Coinbase's
7408
+ // worst-fee payment method (card) and pads by `slippagePad` (default 1%),
7409
+ // so cheaper methods overshoot the floor rather than undershooting it. The
7410
+ // floor holds unless the market moves more than the pad while the user sits
7411
+ // on the checkout screen - which is why completion is VERIFIED: after the
7412
+ // popup closes we poll the platform's status leg and resolve with the
7413
+ // actual filled amount when Coinbase reports it.
7414
+ //
7415
+ // Works on Bounded-served origins (<app>.bounded.page / custom domains). On a
7416
+ // local dev origin the session endpoint does not exist; we fail with a clear
7417
+ // error rather than guessing a host.
7418
+ function hasDOM() {
7419
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
7420
+ }
7421
+ async function authedFetch(path, init) {
7422
+ const idToken = await getIdToken();
7423
+ if (!idToken)
7424
+ throw new Error('onramp: no active session - call login() first');
7425
+ return fetch(`${window.location.origin}${path}`, Object.assign(Object.assign({}, init), { headers: Object.assign(Object.assign({}, init === null || init === void 0 ? void 0 : init.headers), { Authorization: `Bearer ${idToken}` }) }));
7426
+ }
7427
+ async function readError(res) {
7428
+ var _a;
7429
+ try {
7430
+ return (_a = (await res.json()).error) !== null && _a !== void 0 ? _a : '';
7431
+ }
7432
+ catch (_b) {
7433
+ return '';
7434
+ }
7435
+ }
7436
+ function sessionBody(options, extra) {
7437
+ return JSON.stringify(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (options.asset ? { asset: options.asset } : {})), (options.address ? { address: options.address } : {})), (options.amountUsd !== undefined ? { amountUsd: options.amountUsd } : {})), (options.minAmountOut !== undefined ? { minAmountOut: options.minAmountOut } : {})), (options.slippagePad !== undefined ? { slippagePad: options.slippagePad } : {})), extra));
7438
+ }
7439
+ function resolveAddress(options) {
7440
+ var _a, _b;
7441
+ const user = getCurrentUser();
7442
+ const address = (_b = (_a = options.address) !== null && _a !== void 0 ? _a : user === null || user === void 0 ? void 0 : user.address) !== null && _b !== void 0 ? _b : undefined;
7443
+ if (!address) {
7444
+ throw new Error(user
7445
+ ? 'onramp: the signed-in account has no wallet address - pass { address } explicitly'
7446
+ : 'onramp: no signed-in user - call login() first or pass { address }');
7447
+ }
7448
+ return address;
7449
+ }
7450
+ /**
7451
+ * Preview a quote (fees + exact/floor output) WITHOUT opening any window or
7452
+ * consuming a session. Show this to the user, then call onramp() to check out.
7453
+ */
7454
+ async function onrampQuote(options = {}) {
7455
+ if (!hasDOM())
7456
+ throw new Error('onrampQuote is web-only');
7457
+ resolveAddress(options);
7458
+ const res = await authedFetch('/__bounded/onramp/session', {
7459
+ method: 'POST',
7460
+ headers: { 'content-type': 'application/json' },
7461
+ body: sessionBody(options, { quoteOnly: true }),
7462
+ });
7463
+ if (!res.ok)
7464
+ throw new Error(`onrampQuote: failed (${res.status}${await readError(res).then((e) => (e ? `: ${e}` : ''))})`);
7465
+ return (await res.json());
7466
+ }
7467
+ /** The signed-in user's recent onramp transactions (newest first). */
7468
+ async function onrampStatus(sinceMs) {
7469
+ if (!hasDOM())
7470
+ throw new Error('onrampStatus is web-only');
7471
+ const res = await authedFetch(`/__bounded/onramp/status${sinceMs ? `?since=${Math.floor(sinceMs)}` : ''}`);
7472
+ if (!res.ok)
7473
+ throw new Error(`onrampStatus: failed (${res.status})`);
7474
+ return (await res.json()).transactions;
7475
+ }
7476
+ function isSuccess(t) {
7477
+ return /SUCCESS|COMPLETED/i.test(t.status);
7478
+ }
7479
+ function isPending(t) {
7480
+ return /PROGRESS|PENDING|CREATED/i.test(t.status);
7481
+ }
7482
+ async function verifyCompletion(openedAt, asset, timeoutMs) {
7483
+ const deadline = Date.now() + timeoutMs;
7484
+ // 60s of clock-skew slack on `since`: Coinbase stamps server-side.
7485
+ const since = openedAt - 60000;
7486
+ for (;;) {
7487
+ let sawPending = false;
7488
+ try {
7489
+ const txs = await onrampStatus(since);
7490
+ const done = txs.find((t) => isSuccess(t) && (t.asset === null || t.asset === asset));
7491
+ if (done)
7492
+ return done;
7493
+ sawPending = txs.some(isPending);
7494
+ }
7495
+ catch ( /* transient - keep polling until the deadline */_a) { /* transient - keep polling until the deadline */ }
7496
+ // No completed fill and nothing in flight: one grace poll cycle still
7497
+ // runs (Coinbase's record can lag the redirect), but don't burn the
7498
+ // full window when the user plainly abandoned checkout.
7499
+ if (Date.now() >= deadline)
7500
+ return null;
7501
+ if (!sawPending && Date.now() >= openedAt + 12000 && Date.now() - openedAt > timeoutMs / 2)
7502
+ return null;
7503
+ await new Promise((r) => setTimeout(r, 3000));
7504
+ }
7505
+ }
7506
+ /**
7507
+ * Open Coinbase Onramp for the signed-in user. Resolves after the window
7508
+ * closes, with completion VERIFIED against Coinbase when possible.
7509
+ * Call from a click handler - popup blockers require user activation.
7510
+ */
7511
+ async function onramp(options = {}) {
7512
+ var _a;
7513
+ if (!hasDOM())
7514
+ throw new Error('onramp() is web-only');
7515
+ resolveAddress(options);
7516
+ // Open the window synchronously so the click's user-activation window is not
7517
+ // consumed by the fetch below (same hard-won pattern as loginWithPopup).
7518
+ let popup = null;
7519
+ if (!options.newTab) {
7520
+ const w = options.width || 460;
7521
+ const h = options.height || 720;
7522
+ const left = window.screenX + (window.outerWidth - w) / 2;
7523
+ const top = window.screenY + (window.outerHeight - h) / 2;
7524
+ popup = window.open('', 'bounded-onramp', `width=${w},height=${h},left=${left},top=${top}`);
7525
+ }
7526
+ let session;
7527
+ try {
7528
+ const res = await authedFetch('/__bounded/onramp/session', {
7529
+ method: 'POST',
7530
+ headers: { 'content-type': 'application/json' },
7531
+ body: sessionBody(options),
7532
+ });
7533
+ if (!res.ok) {
7534
+ const detail = await readError(res);
7535
+ if (res.status === 404) {
7536
+ throw new Error('onramp: session endpoint not found - onramp works on Bounded-served origins (your deployed app URL), not local dev servers');
7537
+ }
7538
+ throw new Error(`onramp: session mint failed (${res.status}${detail ? `: ${detail}` : ''})`);
7539
+ }
7540
+ session = (await res.json());
7541
+ }
7542
+ catch (error) {
7543
+ try {
7544
+ popup === null || popup === void 0 ? void 0 : popup.close();
7545
+ }
7546
+ catch ( /* ignore */_b) { /* ignore */ }
7547
+ throw error;
7548
+ }
7549
+ const openedAt = Date.now();
7550
+ const base = Object.assign({ address: session.address, asset: session.asset }, (session.quote ? { quote: session.quote } : {}));
7551
+ if (popup) {
7552
+ try {
7553
+ popup.location.replace(session.url);
7554
+ }
7555
+ catch (_c) {
7556
+ try {
7557
+ popup.location.href = session.url;
7558
+ }
7559
+ catch (_d) {
7560
+ try {
7561
+ popup.close();
7562
+ }
7563
+ catch ( /* ignore */_e) { /* ignore */ }
7564
+ throw new Error('onramp: popup failed to navigate - retry with { newTab: true }');
7565
+ }
7566
+ }
7567
+ await new Promise((resolve) => {
7568
+ const timer = setInterval(() => {
7569
+ if (popup.closed) {
7570
+ clearInterval(timer);
7571
+ resolve();
7572
+ }
7573
+ }, 500);
7574
+ });
7575
+ const fill = await verifyCompletion(openedAt, session.asset, (_a = options.completionTimeoutMs) !== null && _a !== void 0 ? _a : 24000);
7576
+ if (fill) {
7577
+ return Object.assign(Object.assign(Object.assign(Object.assign({}, base), { status: 'completed' }), (fill.amountOut !== null ? { actualOut: fill.amountOut } : {})), (fill.txHash ? { txHash: fill.txHash } : {}));
7578
+ }
7579
+ return Object.assign(Object.assign({}, base), { status: 'closed' });
7580
+ }
7581
+ // New-tab path (popup was blocked or explicitly requested): cross-origin tab
7582
+ // close is not observable, so resolve immediately; verify via onrampStatus().
7583
+ const tab = window.open(session.url, '_blank');
7584
+ if (!tab)
7585
+ throw new Error('onramp: window.open was blocked - call onramp() from a click handler');
7586
+ return Object.assign(Object.assign({}, base), { status: 'opened' });
7587
+ }
7588
+
7343
7589
  /**
7344
7590
  * Extracts a human-readable error message from Solana transaction logs.
7345
7591
  */
@@ -7469,7 +7715,7 @@ class PrivyExpoProvider {
7469
7715
  return { provider: this, address: session.address };
7470
7716
  }
7471
7717
  // Stale session — clear and continue to fresh login flow
7472
- core.getActiveSessionManager().clearSession();
7718
+ await core.getActiveSessionManager().clearSession();
7473
7719
  }
7474
7720
  // Trigger Privy login
7475
7721
  const privyUser = await this.privyMethods.login();
@@ -7650,7 +7896,7 @@ class PrivyExpoProvider {
7650
7896
  if ((_a = this.privyMethods) === null || _a === void 0 ? void 0 : _a.isAuthenticated) {
7651
7897
  await this.privyMethods.logout();
7652
7898
  }
7653
- core.getActiveSessionManager().clearSession();
7899
+ await core.getActiveSessionManager().clearSession();
7654
7900
  }
7655
7901
  async getNativeMethods() {
7656
7902
  return this.privyMethods;
@@ -7850,6 +8096,9 @@ exports.logout = logout;
7850
8096
  exports.normalizeSolanaRpcUrl = normalizeSolanaRpcUrl;
7851
8097
  exports.onAuthLoadingChanged = onAuthLoadingChanged;
7852
8098
  exports.onAuthStateChanged = onAuthStateChanged;
8099
+ exports.onramp = onramp;
8100
+ exports.onrampQuote = onrampQuote;
8101
+ exports.onrampStatus = onrampStatus;
7853
8102
  exports.openBoundedWidget = openBoundedWidget;
7854
8103
  exports.requestEip6963Providers = requestEip6963Providers;
7855
8104
  exports.resolveSolanaRpcUrl = resolveSolanaRpcUrl;
@@ -7862,4 +8111,4 @@ exports.startTurnkeyEmailLogin = startTurnkeyEmailLogin;
7862
8111
  exports.useAuth = useAuth;
7863
8112
  exports.useBuildRun = useBuildRun;
7864
8113
  exports.useQuery = useQuery;
7865
- //# sourceMappingURL=index-MKE9tHpl.js.map
8114
+ //# sourceMappingURL=index-iu8vB4ye.js.map