@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
@@ -1,4 +1,4 @@
1
- import { assertCanonicalBoundedRuntimeWriteCalldata, getActiveSessionManager, getConfig, WebSessionManager, ReactNativeSessionManager, revokeSession, init as init$1, reconnectWithNewAuth, deriveUserIdentityFromIdToken, subscribe, getIdToken as getIdToken$1, convertRemainingAccounts, buildSetDocumentsTransaction, genAuthNonce, genSolanaMessage, createSessionWithSignature } from '@bounded-sh/core';
1
+ import { assertCanonicalBoundedRuntimeWriteCalldata, getActiveSessionManager, getConfig, WebSessionManager, ReactNativeSessionManager, revokeSession, init as init$1, onSessionCleared, reconnectWithNewAuth, deriveUserIdentityFromIdToken, subscribe, getIdToken as getIdToken$1, convertRemainingAccounts, buildSetDocumentsTransaction, genAuthNonce, genSolanaMessage, createSessionWithSignature } from '@bounded-sh/core';
2
2
  import * as React from 'react';
3
3
 
4
4
  /**
@@ -3454,7 +3454,7 @@ const EMBEDDED_WALLET_UNSUPPORTED = 'Unsupported for embedded smart wallets —
3454
3454
  const NO_EMBEDDED_WALLET = 'This login has no embedded wallet. Enable `"auth": { "wallets": true }` in ' +
3455
3455
  'the app policy so email logins get a non-custodial wallet (@user.address), ' +
3456
3456
  'then sign from a user gesture.';
3457
- function hasDOM$4() {
3457
+ function hasDOM$5() {
3458
3458
  return typeof window !== 'undefined' && typeof document !== 'undefined';
3459
3459
  }
3460
3460
  function decodeJwt$1(token) {
@@ -3514,7 +3514,7 @@ let requestSeq$1 = 0;
3514
3514
  * wallet's owner, and validates that the wallet address matches the account.
3515
3515
  */
3516
3516
  async function signViaSignerPopup(action) {
3517
- if (!hasDOM$4()) {
3517
+ if (!hasDOM$5()) {
3518
3518
  throw new Error('Embedded-wallet signing is only available in the browser (popup-based).');
3519
3519
  }
3520
3520
  const isEvm = action.kind === 'evmTransaction';
@@ -3690,7 +3690,7 @@ const STORED_AUTH_METHOD_KEY$1 = 'bounded_last_auth_method';
3690
3690
  // not by `typeof window` (truthy on RN). OIDC redirect/popup is web-only, but the
3691
3691
  // session it mints is read back through this same selector everywhere.
3692
3692
  const sessionManager = getActiveSessionManager;
3693
- function hasDOM$3() {
3693
+ function hasDOM$4() {
3694
3694
  return typeof window !== 'undefined' && typeof document !== 'undefined';
3695
3695
  }
3696
3696
  function decodeJwt(token) {
@@ -3721,7 +3721,7 @@ const oidcProviderSingleton = {
3721
3721
  const idToken = sessionManager().getIdToken();
3722
3722
  return idToken ? userFromIdToken(idToken) : null;
3723
3723
  },
3724
- async logout() { sessionManager().clearSession(); },
3724
+ async logout() { await sessionManager().clearSession(); },
3725
3725
  async signMessage(_message) {
3726
3726
  throw new Error(hasEmbeddedWallet() ? EMBEDDED_WALLET_UNSUPPORTED : OIDC_NO_WALLET);
3727
3727
  },
@@ -3830,7 +3830,7 @@ async function buildAuthorize(opts) {
3830
3830
  throw new Error('appId is not configured (init the SDK with your appId)');
3831
3831
  // Web: default to the current page so callers don't have to pass redirectUri. Native has no
3832
3832
  // window — it MUST pass an https universal link (the system auth session needs a real URL).
3833
- const redirectUri = opts.redirectUri || (hasDOM$3() ? window.location.origin + window.location.pathname : undefined);
3833
+ const redirectUri = opts.redirectUri || (hasDOM$4() ? window.location.origin + window.location.pathname : undefined);
3834
3834
  if (!redirectUri)
3835
3835
  throw new Error('redirectUri is required on React Native — pass the https universal link the app owner registered.');
3836
3836
  const { verifier, challenge } = await makePkce();
@@ -3938,7 +3938,7 @@ async function runNativeLogin(opts) {
3938
3938
  async function loginWithRedirect(opts) {
3939
3939
  if (isNative())
3940
3940
  return runNativeLogin(opts);
3941
- if (!hasDOM$3())
3941
+ if (!hasDOM$4())
3942
3942
  throw new Error('loginWithRedirect requires a browser or a configured React Native platform');
3943
3943
  const { url, pkce } = await buildAuthorize(opts);
3944
3944
  // Stash the verifier + state so the redirect_uri page can finish the exchange. sessionStorage
@@ -3954,7 +3954,7 @@ async function loginWithRedirect(opts) {
3954
3954
  * loginWithRedirect() (which resolves with the User), so there is no separate redirect
3955
3955
  * page to finish from. Calling it on RN is harmless. */
3956
3956
  async function completeLoginFromRedirect() {
3957
- if (!hasDOM$3())
3957
+ if (!hasDOM$4())
3958
3958
  return null;
3959
3959
  const url = new URL(window.location.href);
3960
3960
  const code = url.searchParams.get('code');
@@ -4000,7 +4000,7 @@ async function completeLoginFromRedirect() {
4000
4000
  * redirect_uri page posts the code back. The redirect_uri page just calls the usual
4001
4001
  * completeLoginFromRedirect() (which auto-detects the popup) — no separate call needed. */
4002
4002
  async function loginWithPopup(opts) {
4003
- if (!hasDOM$3())
4003
+ if (!hasDOM$4())
4004
4004
  throw new Error('loginWithPopup is web-only — on React Native use loginWithRedirect()');
4005
4005
  // Open synchronously, before PKCE/config awaits consume the click's user-
4006
4006
  // activation window. Opening only after buildAuthorize() intermittently
@@ -4140,7 +4140,7 @@ async function loginWithPopup(opts) {
4140
4140
  /** Call this on the redirect_uri page when login was opened via loginWithPopup(): posts the
4141
4141
  * code back to the opener and closes. (For the redirect variant use completeLoginFromRedirect.) */
4142
4142
  function completeLoginInPopup(openerOrigin) {
4143
- if (!hasDOM$3() || !window.opener)
4143
+ if (!hasDOM$4() || !window.opener)
4144
4144
  return;
4145
4145
  const url = new URL(window.location.href);
4146
4146
  const code = url.searchParams.get('code');
@@ -4369,15 +4369,20 @@ function authProviderConfigKey(config) {
4369
4369
  * so both web (WebSessionManager) and RN (ReactNativeSessionManager) sessions
4370
4370
  * are properly cleared.
4371
4371
  */
4372
- function clearIncompatibleSession() {
4372
+ async function clearIncompatibleSession() {
4373
4373
  if (getStoredAuthMethod() === currentAuthMethod) {
4374
4374
  return false;
4375
4375
  }
4376
4376
  // Clear both managers: on web the RN manager is a no-op (not configured),
4377
4377
  // and on RN the web manager is a no-op (no localStorage).
4378
- WebSessionManager.clearSession();
4378
+ //
4379
+ // AWAITED. Session removal is serialized through a cross-tab lock now, so a
4380
+ // fire-and-forget call would let init() reach setCurrentUser while the old
4381
+ // credential was still readable — and a request issued in that gap would
4382
+ // authenticate as the session we are in the middle of discarding.
4383
+ await WebSessionManager.clearSession();
4379
4384
  try {
4380
- ReactNativeSessionManager.clearSession();
4385
+ await ReactNativeSessionManager.clearSession();
4381
4386
  }
4382
4387
  catch ( /* not configured on web */_a) { /* not configured on web */ }
4383
4388
  setStoredAuthMethod(null);
@@ -4510,7 +4515,7 @@ async function getAuthProvider(config) {
4510
4515
  // manager as legacy email sessions. Human login should be started
4511
4516
  // with loginWithRedirect(); this provider is kept as the restore and
4512
4517
  // logout surface for those sessions.
4513
- currentAuthProvider = new (await import('./email-auth-provider-CX7foEhA.mjs')).EmailAuthProvider();
4518
+ currentAuthProvider = new (await import('./email-auth-provider-nBv7mW9q.mjs')).EmailAuthProvider();
4514
4519
  break;
4515
4520
  case "wallet":
4516
4521
  case "phantom": {
@@ -4529,7 +4534,7 @@ async function getAuthProvider(config) {
4529
4534
  if (!isWalletLoginEnabled(config)) {
4530
4535
  throw walletLoginNotEnabledError(authMethod);
4531
4536
  }
4532
- const { InjectedWalletProvider } = await import('./injected-wallet-provider-Dg3MieTq.mjs');
4537
+ const { InjectedWalletProvider } = await import('./injected-wallet-provider-kx8tcMKM.mjs');
4533
4538
  currentAuthProvider = new InjectedWalletProvider(resolveInjectedWalletConfig(config), rpcUrl, configuredNetwork);
4534
4539
  break;
4535
4540
  }
@@ -4542,7 +4547,7 @@ async function getAuthProvider(config) {
4542
4547
  // Selecting authMethod:'evm-wallet' IS the opt-in — pass evmWalletConfig
4543
4548
  // to point at a specific wallet or set chainId/domain.
4544
4549
  const evmWalletConfig = ((_a = config.evmWalletConfig) !== null && _a !== void 0 ? _a : {});
4545
- const { InjectedEvmWalletProvider } = await import('./injected-evm-wallet-provider-CnJRLE8P.mjs');
4550
+ const { InjectedEvmWalletProvider } = await import('./injected-evm-wallet-provider-D8lYGcM2.mjs');
4546
4551
  currentAuthProvider = new InjectedEvmWalletProvider(evmWalletConfig);
4547
4552
  break;
4548
4553
  }
@@ -4570,7 +4575,7 @@ async function getAuthProvider(config) {
4570
4575
  break;
4571
4576
  case "guest":
4572
4577
  // Anonymous auth: a device-local ed25519 keypair signs the challenge.
4573
- currentAuthProvider = new (await import('./guest-auth-provider-DJmuReVa.mjs')).GuestAuthProvider();
4578
+ currentAuthProvider = new (await import('./guest-auth-provider-DGvQgIwm.mjs')).GuestAuthProvider();
4574
4579
  break;
4575
4580
  }
4576
4581
  if (!currentAuthProvider) {
@@ -4626,7 +4631,7 @@ async function signInAnonymously() {
4626
4631
  // Lazy-load the guest provider so its ed25519/web3 dependencies stay out of
4627
4632
  // the main bundle for data-only apps. Specifier is a static string literal
4628
4633
  // so bundle-splitting can see it.
4629
- const provider = new (await import('./guest-auth-provider-DJmuReVa.mjs')).GuestAuthProvider();
4634
+ const provider = new (await import('./guest-auth-provider-DGvQgIwm.mjs')).GuestAuthProvider();
4630
4635
  setAuthLoading(true); // reflect "signing in…" while the guest keypair session is minted
4631
4636
  const commit = (user) => {
4632
4637
  // No await/interleaving inside this block: provider/method/config/marker
@@ -4668,7 +4673,7 @@ async function signInAnonymously() {
4668
4673
  }
4669
4674
  catch (_b) {
4670
4675
  try {
4671
- getActiveSessionManager().clearSession();
4676
+ await getActiveSessionManager().clearSession();
4672
4677
  }
4673
4678
  catch ( /* fail closed below */_c) { /* fail closed below */ }
4674
4679
  }
@@ -4777,7 +4782,7 @@ async function loginWithWallet(options) {
4777
4782
  ? Object.assign(Object.assign({}, base), { getProvider: options.getProvider }) : base;
4778
4783
  const rpcUrl = normalizeSolanaRpcUrl(cfg.rpcUrl);
4779
4784
  const network = getConfiguredSolanaNetwork(cfg);
4780
- const { InjectedWalletProvider } = await import('./injected-wallet-provider-Dg3MieTq.mjs');
4785
+ const { InjectedWalletProvider } = await import('./injected-wallet-provider-kx8tcMKM.mjs');
4781
4786
  let provider = new InjectedWalletProvider(injectedConfig, rpcUrl, network);
4782
4787
  if (coreConfig.chain === 'offchain')
4783
4788
  provider = new OffchainAuthProvider(provider);
@@ -4849,7 +4854,7 @@ function getCurrentAuthMethod() {
4849
4854
  // allowlist here: on a trusted origin we end the issuer session with a
4850
4855
  // top-level bounce; anywhere else (custom domains, http dev) we keep the
4851
4856
  // local-only logout instead of stranding the user on the issuer origin.
4852
- function issuerLogoutBounceUrl(issuerBase) {
4857
+ function issuerLogoutBounceUrl(issuerBase, idTokenHint) {
4853
4858
  if (typeof window === 'undefined' || typeof document === 'undefined')
4854
4859
  return null; // RN/Node: no shared cookie jar to clear this way
4855
4860
  try {
@@ -4872,12 +4877,27 @@ function issuerLogoutBounceUrl(issuerBase) {
4872
4877
  if (!trusted)
4873
4878
  return null;
4874
4879
  const base = issuerBase.replace(/\/$/, '');
4875
- return `${base}/logout?redirect_uri=${encodeURIComponent(here.href)}`;
4880
+ // #228: send the id_token_hint so the issuer can bind the post-logout redirect to this
4881
+ // session + app + registered origin. Without it the issuer safely falls back to its own
4882
+ // origin (it no longer trusts a bare suffix match), so the return-to-app bounce is a no-op.
4883
+ const hint = idTokenHint ? `&id_token_hint=${encodeURIComponent(idTokenHint)}` : '';
4884
+ return `${base}/logout?redirect_uri=${encodeURIComponent(here.href)}${hint}`;
4876
4885
  }
4877
4886
  catch (_b) {
4878
4887
  return null;
4879
4888
  }
4880
4889
  }
4890
+ // #228 helper, deliberately OUTSIDE logout(): the logout body is source-guarded
4891
+ // against any `catch` so a revokeSession failure can never be swallowed, and a
4892
+ // missing id token is the one benign absence worth tolerating here.
4893
+ function idTokenHintOrNull(sm) {
4894
+ try {
4895
+ return sm.getIdToken();
4896
+ }
4897
+ catch (_a) {
4898
+ return null; /* no id token to hint with */
4899
+ }
4900
+ }
4881
4901
  async function logout$1(options) {
4882
4902
  // Re-initialize the provider if it was cleared by a prior logout — the exact mirror
4883
4903
  // of login()'s self-heal above. A hosted popup/redirect login (loginWithPopup /
@@ -4902,6 +4922,9 @@ async function logout$1(options) {
4902
4922
  const sm = getActiveSessionManager();
4903
4923
  const refreshToken = sm.getRefreshToken();
4904
4924
  const sessionIssuer = sm.getIssuer();
4925
+ // #228: capture the id token NOW, before currentAuthProvider.logout() clears the session, so we
4926
+ // can hand it to the issuer /logout as an id_token_hint (which binds the post-logout redirect).
4927
+ const idTokenHint = idTokenHintOrNull(sm);
4905
4928
  if (refreshToken)
4906
4929
  await revokeSession(refreshToken, sessionIssuer !== null && sessionIssuer !== void 0 ? sessionIssuer : undefined);
4907
4930
  // Hosted sessions ('email' covers the whole hosted surface: email OTP +
@@ -4922,7 +4945,7 @@ async function logout$1(options) {
4922
4945
  const issuerBase = sessionIssuer
4923
4946
  || (await getConfig()).humanAuthApiUrl
4924
4947
  || 'https://auth.bounded.sh';
4925
- const bounce = issuerLogoutBounceUrl(issuerBase);
4948
+ const bounce = issuerLogoutBounceUrl(issuerBase, idTokenHint);
4926
4949
  // Local state is already cleared, so even if the navigation is stopped
4927
4950
  // (tab close, extension) the app-side logout has fully happened.
4928
4951
  if (bounce)
@@ -4951,6 +4974,22 @@ var index = /*#__PURE__*/Object.freeze({
4951
4974
  signInAnonymously: signInAnonymously
4952
4975
  });
4953
4976
 
4977
+ /**
4978
+ * Subscribed once per process, not per init(): re-registering on every init would
4979
+ * stack duplicate handlers on repeated initialization.
4980
+ */
4981
+ let sessionClearedBridgeRegistered = false;
4982
+ function registerSessionClearedBridge() {
4983
+ if (sessionClearedBridgeRegistered)
4984
+ return;
4985
+ sessionClearedBridgeRegistered = true;
4986
+ onSessionCleared(() => {
4987
+ // Core fires this only when storage was ACTUALLY cleared, never on a
4988
+ // transient refresh failure that preserved the session — so this cannot
4989
+ // sign anyone out over a network blip.
4990
+ setCurrentUser(null);
4991
+ });
4992
+ }
4954
4993
  /**
4955
4994
  * Read the active session's idToken synchronously from whichever store is live
4956
4995
  * for this runtime (WebSessionManager on web, ReactNativeSessionManager on RN —
@@ -5008,6 +5047,15 @@ async function init(newConfig) {
5008
5047
  // setting it here makes "no flash" the SDK's default behavior for every consumer.
5009
5048
  setAuthLoading(true);
5010
5049
  try {
5050
+ // Bridge core's "the stored session is gone" signal into SDK auth state.
5051
+ //
5052
+ // Core cannot import this package, and a session can die deep inside a
5053
+ // session manager (dead refresh token, rejected refresh, app-id mismatch).
5054
+ // Before this, setCurrentUser(null) was only ever called by logout() and
5055
+ // init(), so storage could be wiped while `currentUser` stayed populated —
5056
+ // onAuthStateChanged never fired, and every consumer UI went on showing a
5057
+ // connected wallet for a session the server no longer knew about.
5058
+ registerSessionClearedBridge();
5011
5059
  // Get auth provider first
5012
5060
  authProviderInstance = await getAuthProvider(newConfig);
5013
5061
  // Initialize config with auth provider
@@ -5024,7 +5072,12 @@ async function init(newConfig) {
5024
5072
  let user = await authProviderInstance.restoreSession();
5025
5073
  // If a session exists but was created before auth method tracking (legacy session),
5026
5074
  // clear it to avoid mismatches when the app switches auth methods (e.g. email → phantom)
5027
- if (user && clearIncompatibleSession()) {
5075
+ //
5076
+ // AWAITED: the clear is serialized through a cross-tab lock now, and an
5077
+ // unawaited call would return a Promise (always truthy) — nulling the user
5078
+ // unconditionally AND letting setCurrentUser run while the old credential
5079
+ // was still in storage.
5080
+ if (user && await clearIncompatibleSession()) {
5028
5081
  user = null;
5029
5082
  }
5030
5083
  setCurrentUser(user);
@@ -5142,7 +5195,7 @@ function setAuthProviderInstance(provider) {
5142
5195
  // The modal is a warm singleton: it stays mounted (hidden) between uses so a
5143
5196
  // second open - or a signature right after login - is instant and, when it is
5144
5197
  // already open, a clean in-place view swap rather than a reopen.
5145
- function hasDOM$2() {
5198
+ function hasDOM$3() {
5146
5199
  return typeof window !== "undefined" && typeof document !== "undefined";
5147
5200
  }
5148
5201
  const OVERLAY_CSS = `
@@ -5364,7 +5417,7 @@ function detachIframe(m) {
5364
5417
  // no-op if already present or off-DOM. Fonts loaded here match the hosted auth page.
5365
5418
  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";
5366
5419
  function ensureFonts() {
5367
- if (!hasDOM$2())
5420
+ if (!hasDOM$3())
5368
5421
  return;
5369
5422
  if (document.querySelector('link[data-bounded-fonts="1"]'))
5370
5423
  return;
@@ -5670,7 +5723,7 @@ function discoverStandardWallets() {
5670
5723
  return out;
5671
5724
  }
5672
5725
 
5673
- function hasDOM$1() {
5726
+ function hasDOM$2() {
5674
5727
  return typeof window !== 'undefined' && typeof document !== 'undefined';
5675
5728
  }
5676
5729
  async function signerOrigin() {
@@ -5747,7 +5800,7 @@ function sealOtpInIframe(code, otpEncryptionTargetBundle) {
5747
5800
  * step needs the signer iframe); on React Native use loginWithRedirect() instead.
5748
5801
  */
5749
5802
  async function startTurnkeyEmailLogin(email) {
5750
- if (!hasDOM$1()) {
5803
+ if (!hasDOM$2()) {
5751
5804
  throw new Error('Turnkey-native email login is web-only - on React Native use loginWithRedirect()');
5752
5805
  }
5753
5806
  const trimmed = email.trim();
@@ -5936,7 +5989,7 @@ function el(tag, cls, text) {
5936
5989
  */
5937
5990
  async function openBoundedWidget(opts = {}) {
5938
5991
  var _a, _b, _c, _d, _e, _f, _g, _h;
5939
- if (!hasDOM$2()) {
5992
+ if (!hasDOM$3()) {
5940
5993
  throw new Error("openBoundedWidget is web-only - on React Native use loginWithRedirect()");
5941
5994
  }
5942
5995
  // Imported lazily to avoid a static cycle with the auth index barrel.
@@ -7043,7 +7096,7 @@ async function deserializeTransaction(tx) {
7043
7096
  * (Safari cross-origin create) or cannot be mounted.
7044
7097
  */
7045
7098
  async function runWidget(url, signerOrigin, ex, popupFallback) {
7046
- if (!hasDOM$2())
7099
+ if (!hasDOM$3())
7047
7100
  return popupFallback();
7048
7101
  const m = getModal();
7049
7102
  const requestId = String(ex.request.requestId);
@@ -7152,7 +7205,7 @@ async function runWidget(url, signerOrigin, ex, popupFallback) {
7152
7205
  // ONE ORIGIN, ONE PASSKEY: a WebAuthn passkey is bound to a single origin, so ALL
7153
7206
  // Bounded apps (any subdomain / custom domain) sign through this one origin - giving
7154
7207
  // every user one wallet + one passkey that roams across every app.
7155
- function hasDOM() {
7208
+ function hasDOM$1() {
7156
7209
  return typeof window !== 'undefined' && typeof document !== 'undefined';
7157
7210
  }
7158
7211
  // Safari (incl. iOS WebViews) is unreliable at running a WebAuthn CREATE inside a
@@ -7161,7 +7214,7 @@ function hasDOM() {
7161
7214
  // There we pick the popup up front, from the original click, so enrollment is solid.
7162
7215
  // Everywhere else the inline widget is used (buttery in-app modal).
7163
7216
  function preferPopup() {
7164
- if (!hasDOM() || typeof navigator === 'undefined')
7217
+ if (!hasDOM$1() || typeof navigator === 'undefined')
7165
7218
  return true;
7166
7219
  const ua = navigator.userAgent || '';
7167
7220
  return /^((?!chrome|android|crios|fxios|edg|edga|opr).)*safari/i.test(ua);
@@ -7278,7 +7331,7 @@ async function runPopup(url, signerOrigin, ex) {
7278
7331
  * Use this to learn the fee-payer address BEFORE building a transaction to sign.
7279
7332
  */
7280
7333
  async function getOrCreateTurnkeyWallet() {
7281
- if (!hasDOM())
7334
+ if (!hasDOM$1())
7282
7335
  throw new Error('Turnkey embedded wallets are only available in the browser (popup-based).');
7283
7336
  const { url, origin } = await turnkeySignerPage();
7284
7337
  const requestId = `tkcreate_${Date.now()}_${++requestSeq}`;
@@ -7302,7 +7355,7 @@ async function getOrCreateTurnkeyWallet() {
7302
7355
  * signatures are just a passkey approval.
7303
7356
  */
7304
7357
  async function signSolanaMessageViaTurnkey(message) {
7305
- if (!hasDOM()) {
7358
+ if (!hasDOM$1()) {
7306
7359
  throw new Error('Turnkey embedded-wallet signing is only available in the browser (popup-based).');
7307
7360
  }
7308
7361
  const { url, origin } = await turnkeySignerPage();
@@ -7319,6 +7372,199 @@ async function signSolanaMessageViaTurnkey(message) {
7319
7372
  });
7320
7373
  }
7321
7374
 
7375
+ // bounded.onramp() - fund the signed-in user's wallet with Coinbase Onramp.
7376
+ //
7377
+ // Web-only. Opens Coinbase's hosted checkout (pay.coinbase.com) in a popup
7378
+ // (new-tab fallback) using a single-use session token minted by the Bounded
7379
+ // platform at POST <origin>/__bounded/onramp/session on the app's own origin.
7380
+ // The platform holds the Coinbase credential; the app never sees it. The
7381
+ // session is bound server-side to the caller's verified Bounded JWT, and the
7382
+ // destination defaults to that user's embedded wallet.
7383
+ //
7384
+ // Amount semantics:
7385
+ // amountUsd - exact fiat in; the fee/output breakdown comes back as `quote`.
7386
+ // minAmountOut - a FLOOR on crypto received. The platform quotes Coinbase's
7387
+ // worst-fee payment method (card) and pads by `slippagePad` (default 1%),
7388
+ // so cheaper methods overshoot the floor rather than undershooting it. The
7389
+ // floor holds unless the market moves more than the pad while the user sits
7390
+ // on the checkout screen - which is why completion is VERIFIED: after the
7391
+ // popup closes we poll the platform's status leg and resolve with the
7392
+ // actual filled amount when Coinbase reports it.
7393
+ //
7394
+ // Works on Bounded-served origins (<app>.bounded.page / custom domains). On a
7395
+ // local dev origin the session endpoint does not exist; we fail with a clear
7396
+ // error rather than guessing a host.
7397
+ function hasDOM() {
7398
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
7399
+ }
7400
+ async function authedFetch(path, init) {
7401
+ const idToken = await getIdToken();
7402
+ if (!idToken)
7403
+ throw new Error('onramp: no active session - call login() first');
7404
+ 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}` }) }));
7405
+ }
7406
+ async function readError(res) {
7407
+ var _a;
7408
+ try {
7409
+ return (_a = (await res.json()).error) !== null && _a !== void 0 ? _a : '';
7410
+ }
7411
+ catch (_b) {
7412
+ return '';
7413
+ }
7414
+ }
7415
+ function sessionBody(options, extra) {
7416
+ 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));
7417
+ }
7418
+ function resolveAddress(options) {
7419
+ var _a, _b;
7420
+ const user = getCurrentUser();
7421
+ 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;
7422
+ if (!address) {
7423
+ throw new Error(user
7424
+ ? 'onramp: the signed-in account has no wallet address - pass { address } explicitly'
7425
+ : 'onramp: no signed-in user - call login() first or pass { address }');
7426
+ }
7427
+ return address;
7428
+ }
7429
+ /**
7430
+ * Preview a quote (fees + exact/floor output) WITHOUT opening any window or
7431
+ * consuming a session. Show this to the user, then call onramp() to check out.
7432
+ */
7433
+ async function onrampQuote(options = {}) {
7434
+ if (!hasDOM())
7435
+ throw new Error('onrampQuote is web-only');
7436
+ resolveAddress(options);
7437
+ const res = await authedFetch('/__bounded/onramp/session', {
7438
+ method: 'POST',
7439
+ headers: { 'content-type': 'application/json' },
7440
+ body: sessionBody(options, { quoteOnly: true }),
7441
+ });
7442
+ if (!res.ok)
7443
+ throw new Error(`onrampQuote: failed (${res.status}${await readError(res).then((e) => (e ? `: ${e}` : ''))})`);
7444
+ return (await res.json());
7445
+ }
7446
+ /** The signed-in user's recent onramp transactions (newest first). */
7447
+ async function onrampStatus(sinceMs) {
7448
+ if (!hasDOM())
7449
+ throw new Error('onrampStatus is web-only');
7450
+ const res = await authedFetch(`/__bounded/onramp/status${sinceMs ? `?since=${Math.floor(sinceMs)}` : ''}`);
7451
+ if (!res.ok)
7452
+ throw new Error(`onrampStatus: failed (${res.status})`);
7453
+ return (await res.json()).transactions;
7454
+ }
7455
+ function isSuccess(t) {
7456
+ return /SUCCESS|COMPLETED/i.test(t.status);
7457
+ }
7458
+ function isPending(t) {
7459
+ return /PROGRESS|PENDING|CREATED/i.test(t.status);
7460
+ }
7461
+ async function verifyCompletion(openedAt, asset, timeoutMs) {
7462
+ const deadline = Date.now() + timeoutMs;
7463
+ // 60s of clock-skew slack on `since`: Coinbase stamps server-side.
7464
+ const since = openedAt - 60000;
7465
+ for (;;) {
7466
+ let sawPending = false;
7467
+ try {
7468
+ const txs = await onrampStatus(since);
7469
+ const done = txs.find((t) => isSuccess(t) && (t.asset === null || t.asset === asset));
7470
+ if (done)
7471
+ return done;
7472
+ sawPending = txs.some(isPending);
7473
+ }
7474
+ catch ( /* transient - keep polling until the deadline */_a) { /* transient - keep polling until the deadline */ }
7475
+ // No completed fill and nothing in flight: one grace poll cycle still
7476
+ // runs (Coinbase's record can lag the redirect), but don't burn the
7477
+ // full window when the user plainly abandoned checkout.
7478
+ if (Date.now() >= deadline)
7479
+ return null;
7480
+ if (!sawPending && Date.now() >= openedAt + 12000 && Date.now() - openedAt > timeoutMs / 2)
7481
+ return null;
7482
+ await new Promise((r) => setTimeout(r, 3000));
7483
+ }
7484
+ }
7485
+ /**
7486
+ * Open Coinbase Onramp for the signed-in user. Resolves after the window
7487
+ * closes, with completion VERIFIED against Coinbase when possible.
7488
+ * Call from a click handler - popup blockers require user activation.
7489
+ */
7490
+ async function onramp(options = {}) {
7491
+ var _a;
7492
+ if (!hasDOM())
7493
+ throw new Error('onramp() is web-only');
7494
+ resolveAddress(options);
7495
+ // Open the window synchronously so the click's user-activation window is not
7496
+ // consumed by the fetch below (same hard-won pattern as loginWithPopup).
7497
+ let popup = null;
7498
+ if (!options.newTab) {
7499
+ const w = options.width || 460;
7500
+ const h = options.height || 720;
7501
+ const left = window.screenX + (window.outerWidth - w) / 2;
7502
+ const top = window.screenY + (window.outerHeight - h) / 2;
7503
+ popup = window.open('', 'bounded-onramp', `width=${w},height=${h},left=${left},top=${top}`);
7504
+ }
7505
+ let session;
7506
+ try {
7507
+ const res = await authedFetch('/__bounded/onramp/session', {
7508
+ method: 'POST',
7509
+ headers: { 'content-type': 'application/json' },
7510
+ body: sessionBody(options),
7511
+ });
7512
+ if (!res.ok) {
7513
+ const detail = await readError(res);
7514
+ if (res.status === 404) {
7515
+ throw new Error('onramp: session endpoint not found - onramp works on Bounded-served origins (your deployed app URL), not local dev servers');
7516
+ }
7517
+ throw new Error(`onramp: session mint failed (${res.status}${detail ? `: ${detail}` : ''})`);
7518
+ }
7519
+ session = (await res.json());
7520
+ }
7521
+ catch (error) {
7522
+ try {
7523
+ popup === null || popup === void 0 ? void 0 : popup.close();
7524
+ }
7525
+ catch ( /* ignore */_b) { /* ignore */ }
7526
+ throw error;
7527
+ }
7528
+ const openedAt = Date.now();
7529
+ const base = Object.assign({ address: session.address, asset: session.asset }, (session.quote ? { quote: session.quote } : {}));
7530
+ if (popup) {
7531
+ try {
7532
+ popup.location.replace(session.url);
7533
+ }
7534
+ catch (_c) {
7535
+ try {
7536
+ popup.location.href = session.url;
7537
+ }
7538
+ catch (_d) {
7539
+ try {
7540
+ popup.close();
7541
+ }
7542
+ catch ( /* ignore */_e) { /* ignore */ }
7543
+ throw new Error('onramp: popup failed to navigate - retry with { newTab: true }');
7544
+ }
7545
+ }
7546
+ await new Promise((resolve) => {
7547
+ const timer = setInterval(() => {
7548
+ if (popup.closed) {
7549
+ clearInterval(timer);
7550
+ resolve();
7551
+ }
7552
+ }, 500);
7553
+ });
7554
+ const fill = await verifyCompletion(openedAt, session.asset, (_a = options.completionTimeoutMs) !== null && _a !== void 0 ? _a : 24000);
7555
+ if (fill) {
7556
+ return Object.assign(Object.assign(Object.assign(Object.assign({}, base), { status: 'completed' }), (fill.amountOut !== null ? { actualOut: fill.amountOut } : {})), (fill.txHash ? { txHash: fill.txHash } : {}));
7557
+ }
7558
+ return Object.assign(Object.assign({}, base), { status: 'closed' });
7559
+ }
7560
+ // New-tab path (popup was blocked or explicitly requested): cross-origin tab
7561
+ // close is not observable, so resolve immediately; verify via onrampStatus().
7562
+ const tab = window.open(session.url, '_blank');
7563
+ if (!tab)
7564
+ throw new Error('onramp: window.open was blocked - call onramp() from a click handler');
7565
+ return Object.assign(Object.assign({}, base), { status: 'opened' });
7566
+ }
7567
+
7322
7568
  /**
7323
7569
  * Extracts a human-readable error message from Solana transaction logs.
7324
7570
  */
@@ -7448,7 +7694,7 @@ class PrivyExpoProvider {
7448
7694
  return { provider: this, address: session.address };
7449
7695
  }
7450
7696
  // Stale session — clear and continue to fresh login flow
7451
- getActiveSessionManager().clearSession();
7697
+ await getActiveSessionManager().clearSession();
7452
7698
  }
7453
7699
  // Trigger Privy login
7454
7700
  const privyUser = await this.privyMethods.login();
@@ -7629,7 +7875,7 @@ class PrivyExpoProvider {
7629
7875
  if ((_a = this.privyMethods) === null || _a === void 0 ? void 0 : _a.isAuthenticated) {
7630
7876
  await this.privyMethods.logout();
7631
7877
  }
7632
- getActiveSessionManager().clearSession();
7878
+ await getActiveSessionManager().clearSession();
7633
7879
  }
7634
7880
  async getNativeMethods() {
7635
7881
  return this.privyMethods;
@@ -7787,5 +8033,5 @@ function defaultInjectedEvmProvider() {
7787
8033
  return null;
7788
8034
  }
7789
8035
 
7790
- export { BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA as $, completeLoginFromRedirect as A, loginWithPopup as B, completeLoginInPopup as C, openBoundedWidget as D, EMBEDDED_WALLET_UNSUPPORTED as E, useAuth as F, useQuery as G, useBuildRun as H, apps as I, deserializeTransaction as J, signSolanaMessageViaTurnkey as K, getOrCreateTurnkeyWallet as L, startTurnkeyEmailLogin as M, getIdToken as N, setPlatform as O, OffchainAuthProvider as P, PrivyExpoProvider as Q, requestEip6963Providers as R, builds as S, BuildsApiError as T, NON_TERMINAL_STATES as U, TERMINAL_STATES as V, RUN_STATES as W, PARK_REASONS as X, FAILURE_REASONS as Y, BUILD_REVIEW_CAPABILITY_SCHEMA as Z, BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA as _, hasEmbeddedEvmWallet as a, embeddedSignAndSubmit as b, setAuthLoading as c, defaultInjectedProvider as d, embeddedRunEvmTransaction as e, bufferExports as f, getPlatform as g, hasEmbeddedWallet as h, bs58 as i, confirmAndCheckTransaction as j, defaultInjectedEvmProvider as k, init as l, getCurrentUser as m, normalizeSolanaRpcUrl as n, onAuthStateChanged as o, onAuthLoadingChanged as p, getAuthLoading as q, resolveSolanaRpcUrl as r, setCurrentUser as s, login as t, logout as u, getAuthProvider as v, signInAnonymously as w, loginWithPrivy as x, loginWithWallet as y, loginWithRedirect as z };
7791
- //# sourceMappingURL=index-DWodlTFG.mjs.map
8036
+ export { FAILURE_REASONS as $, completeLoginFromRedirect as A, loginWithPopup as B, completeLoginInPopup as C, openBoundedWidget as D, EMBEDDED_WALLET_UNSUPPORTED as E, useAuth as F, useQuery as G, useBuildRun as H, apps as I, deserializeTransaction as J, signSolanaMessageViaTurnkey as K, getOrCreateTurnkeyWallet as L, startTurnkeyEmailLogin as M, getIdToken as N, onramp as O, onrampQuote as P, onrampStatus as Q, setPlatform as R, OffchainAuthProvider as S, PrivyExpoProvider as T, requestEip6963Providers as U, builds as V, BuildsApiError as W, NON_TERMINAL_STATES as X, TERMINAL_STATES as Y, RUN_STATES as Z, PARK_REASONS as _, hasEmbeddedEvmWallet as a, BUILD_REVIEW_CAPABILITY_SCHEMA as a0, BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA as a1, BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA as a2, embeddedSignAndSubmit as b, setAuthLoading as c, defaultInjectedProvider as d, embeddedRunEvmTransaction as e, bufferExports as f, getPlatform as g, hasEmbeddedWallet as h, bs58 as i, confirmAndCheckTransaction as j, defaultInjectedEvmProvider as k, init as l, getCurrentUser as m, normalizeSolanaRpcUrl as n, onAuthStateChanged as o, onAuthLoadingChanged as p, getAuthLoading as q, resolveSolanaRpcUrl as r, setCurrentUser as s, login as t, logout as u, getAuthProvider as v, signInAnonymously as w, loginWithPrivy as x, loginWithWallet as y, loginWithRedirect as z };
8037
+ //# sourceMappingURL=index-CQtGkKVd.mjs.map