@bounded-sh/client 0.0.104 → 0.0.106

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 (56) hide show
  1. package/README.md +24 -3
  2. package/dist/auth/index.d.ts +1 -1
  3. package/dist/auth/oidc-auth.d.ts +17 -13
  4. package/dist/{email-auth-provider-aMa0EDuc.js → email-auth-provider-C_0SoVG4.js} +2 -2
  5. package/dist/{email-auth-provider-aMa0EDuc.js.map → email-auth-provider-C_0SoVG4.js.map} +1 -1
  6. package/dist/{email-auth-provider-CGhDN-8d.js → email-auth-provider-Czo-xM6n.js} +2 -2
  7. package/dist/{email-auth-provider-CGhDN-8d.js.map → email-auth-provider-Czo-xM6n.js.map} +1 -1
  8. package/dist/{email-auth-provider-CdBqgLWw.mjs → email-auth-provider-D9SQu9c1.mjs} +2 -2
  9. package/dist/{email-auth-provider-DA2dPFXt.mjs.map → email-auth-provider-D9SQu9c1.mjs.map} +1 -1
  10. package/dist/{email-auth-provider-DA2dPFXt.mjs → email-auth-provider-DEchcb2r.mjs} +2 -2
  11. package/dist/{email-auth-provider-CdBqgLWw.mjs.map → email-auth-provider-DEchcb2r.mjs.map} +1 -1
  12. package/dist/global.d.ts +2 -0
  13. package/dist/{guest-auth-provider-BiWPelOD.mjs → guest-auth-provider-CF1zahC4.mjs} +2 -2
  14. package/dist/{guest-auth-provider-pDVr5YRg.mjs.map → guest-auth-provider-CF1zahC4.mjs.map} +1 -1
  15. package/dist/{guest-auth-provider-BT9JPOpp.js → guest-auth-provider-DJ5nCGjq.js} +2 -2
  16. package/dist/{guest-auth-provider-BT9JPOpp.js.map → guest-auth-provider-DJ5nCGjq.js.map} +1 -1
  17. package/dist/{guest-auth-provider-Ca50pehK.js → guest-auth-provider-DPZRPch_.js} +2 -2
  18. package/dist/{guest-auth-provider-Ca50pehK.js.map → guest-auth-provider-DPZRPch_.js.map} +1 -1
  19. package/dist/{guest-auth-provider-pDVr5YRg.mjs → guest-auth-provider-DsFTB844.mjs} +2 -2
  20. package/dist/{guest-auth-provider-BiWPelOD.mjs.map → guest-auth-provider-DsFTB844.mjs.map} +1 -1
  21. package/dist/hosted-identity.d.ts +10 -0
  22. package/dist/{index-DKkAO0g-.mjs → index-nVYK3QUO.mjs} +428 -124
  23. package/dist/index-nVYK3QUO.mjs.map +1 -0
  24. package/dist/{index-DTBxbfUl.js → index-ok-Nn6DV.js} +426 -122
  25. package/dist/index-ok-Nn6DV.js.map +1 -0
  26. package/dist/index.d.ts +9 -2
  27. package/dist/index.js +2 -2
  28. package/dist/index.mjs +1 -1
  29. package/dist/{index.native-Bl2W-Wsi.mjs → index.native-BSOqaKIz.mjs} +313 -97
  30. package/dist/index.native-BSOqaKIz.mjs.map +1 -0
  31. package/dist/{index.native-CNW3iAoP.js → index.native-BpcNwWQX.js} +313 -97
  32. package/dist/index.native-BpcNwWQX.js.map +1 -0
  33. package/dist/index.native.d.ts +1 -1
  34. package/dist/index.native.js +2 -2
  35. package/dist/index.native.mjs +1 -1
  36. package/dist/{injected-evm-wallet-provider-B8PWHQdb.js → injected-evm-wallet-provider-C3Z2c4Yl.js} +2 -2
  37. package/dist/{injected-evm-wallet-provider-B8PWHQdb.js.map → injected-evm-wallet-provider-C3Z2c4Yl.js.map} +1 -1
  38. package/dist/{injected-evm-wallet-provider-HbgMQAbf.mjs → injected-evm-wallet-provider-Xb2G-ZGI.mjs} +2 -2
  39. package/dist/{injected-evm-wallet-provider-HbgMQAbf.mjs.map → injected-evm-wallet-provider-Xb2G-ZGI.mjs.map} +1 -1
  40. package/dist/{injected-evm-wallet-provider-Cym2uENX.js → injected-evm-wallet-provider-_UNpc_hM.js} +2 -2
  41. package/dist/{injected-evm-wallet-provider-Cym2uENX.js.map → injected-evm-wallet-provider-_UNpc_hM.js.map} +1 -1
  42. package/dist/{injected-evm-wallet-provider-BPkcFSQX.mjs → injected-evm-wallet-provider-pI1JMr7g.mjs} +2 -2
  43. package/dist/{injected-evm-wallet-provider-BPkcFSQX.mjs.map → injected-evm-wallet-provider-pI1JMr7g.mjs.map} +1 -1
  44. package/dist/{injected-wallet-provider-FY9JLEPR.js → injected-wallet-provider-C7Q0haGY.js} +2 -2
  45. package/dist/{injected-wallet-provider-FY9JLEPR.js.map → injected-wallet-provider-C7Q0haGY.js.map} +1 -1
  46. package/dist/{injected-wallet-provider-oWtACL_7.js → injected-wallet-provider-CEtgcsQl.js} +2 -2
  47. package/dist/{injected-wallet-provider-oWtACL_7.js.map → injected-wallet-provider-CEtgcsQl.js.map} +1 -1
  48. package/dist/{injected-wallet-provider-BGeNaoEY.mjs → injected-wallet-provider-DNZA1IJ9.mjs} +2 -2
  49. package/dist/{injected-wallet-provider-BGeNaoEY.mjs.map → injected-wallet-provider-DNZA1IJ9.mjs.map} +1 -1
  50. package/dist/{injected-wallet-provider-D39pzPTV.mjs → injected-wallet-provider-DZOwvKgd.mjs} +2 -2
  51. package/dist/{injected-wallet-provider-D39pzPTV.mjs.map → injected-wallet-provider-DZOwvKgd.mjs.map} +1 -1
  52. package/package.json +2 -2
  53. package/dist/index-DKkAO0g-.mjs.map +0 -1
  54. package/dist/index-DTBxbfUl.js.map +0 -1
  55. package/dist/index.native-Bl2W-Wsi.mjs.map +0 -1
  56. package/dist/index.native-CNW3iAoP.js.map +0 -1
@@ -4830,9 +4830,18 @@ const OIDC_NO_WALLET = 'This session has no Turnkey wallet. Sign in again to pro
4830
4830
  // same tab (redirect + popup, or two popups) each own a distinct record, so a second
4831
4831
  // attempt cannot clobber the first's verifier/state/redirectUri.
4832
4832
  const PKCE_STORE_PREFIX = 'bounded.oidc.pkce';
4833
+ const STATE_PREFIX = 'bounded.';
4834
+ const LOGIN_TIMEOUT_MS = 10 * 60000;
4835
+ const CALLBACK_PARAMS = ['code', 'state', 'error', 'error_description', 'iss'];
4833
4836
  function pkceKey(state) {
4834
4837
  return `${PKCE_STORE_PREFIX}.${state}`;
4835
4838
  }
4839
+ function removeAttempt(state) {
4840
+ try {
4841
+ sessionStorage.removeItem(pkceKey(state));
4842
+ }
4843
+ catch ( /* Storage may have been disabled mid-login. */_a) { /* Storage may have been disabled mid-login. */ }
4844
+ }
4836
4845
  const ISSUER_SESSION_KIND_KEY = 'bounded_issuer_session_kind';
4837
4846
  // Active session store (Web on web, RN on React Native) — selected by config,
4838
4847
  // not by `typeof window` (truthy on RN). OIDC redirect/popup is web-only, but the
@@ -4941,6 +4950,165 @@ async function makePkce() {
4941
4950
  function randomState() {
4942
4951
  return b64url(getPlatform().getRandomBytes(16));
4943
4952
  }
4953
+ let pendingCompletion;
4954
+ let loginReturnError;
4955
+ /** The last OAuth return failure in this initialization, or null. Initialization
4956
+ * remains usable so the UI can display this error and offer a fresh sign-in. */
4957
+ function getLoginReturnError() {
4958
+ return (loginReturnError === null || loginReturnError === void 0 ? void 0 : loginReturnError.generation) === getInitializationGeneration() ? loginReturnError.error : null;
4959
+ }
4960
+ // A hosted page can contain both an app bundle and the platform widget's SDK.
4961
+ // They share one code exchange, while adopting the session in their own registries.
4962
+ const COMPLETIONS = Symbol.for('bounded.oidc.completions');
4963
+ function cleanCallbackUrl(url) {
4964
+ const clean = new URL(url);
4965
+ for (const key of CALLBACK_PARAMS)
4966
+ clean.searchParams.delete(key);
4967
+ return clean;
4968
+ }
4969
+ async function validateAttempt(pkce, callback) {
4970
+ const cfg = await core.getConfig();
4971
+ if (!pkce || typeof pkce.verifier !== 'string' || !pkce.verifier ||
4972
+ pkce.appId !== cfg.appId || pkce.issuer !== await issuerBase()) {
4973
+ throw new Error('Login belongs to a different app or auth server. Start sign-in again.');
4974
+ }
4975
+ if (!Number.isFinite(pkce.createdAt) || Date.now() - pkce.createdAt > LOGIN_TIMEOUT_MS || pkce.createdAt > Date.now()) {
4976
+ throw new Error('Login expired. Start sign-in again.');
4977
+ }
4978
+ if (callback && (cleanCallbackUrl(callback).href !== cleanCallbackUrl(new URL(pkce.redirectUri)).href ||
4979
+ (callback.searchParams.has('iss') && callback.searchParams.get('iss') !== pkce.issuer))) {
4980
+ throw new Error('Login returned to a different address or from a different auth server. Start sign-in again.');
4981
+ }
4982
+ }
4983
+ // The receiver acknowledges only after checking origin, window and state. An
4984
+ // absent/disconnected opener must never leave a popup claiming success.
4985
+ async function handOffPopup(url, state) {
4986
+ const cfg = await core.getConfig();
4987
+ const issuer = await issuerBase();
4988
+ const opener = window.opener;
4989
+ if (!opener || opener === window || opener.closed) {
4990
+ throw new Error('The original sign-in window is unavailable. Close this window and sign in again in the original tab.');
4991
+ }
4992
+ return new Promise((resolve, reject) => {
4993
+ const cleanup = () => { clearTimeout(timer); window.removeEventListener('message', onMessage); };
4994
+ const onMessage = (event) => {
4995
+ var _a;
4996
+ if (event.source !== opener || event.origin !== url.origin ||
4997
+ ((_a = event.data) === null || _a === void 0 ? void 0 : _a.type) !== 'bounded-oidc-accepted' || event.data.state !== state)
4998
+ return;
4999
+ cleanup();
5000
+ try {
5001
+ window.close();
5002
+ }
5003
+ catch ( /* The opener also closes its own popup handle. */_b) { /* The opener also closes its own popup handle. */ }
5004
+ resolve();
5005
+ };
5006
+ const timer = setTimeout(() => {
5007
+ cleanup();
5008
+ reject(new Error('Could not reach the original sign-in tab. Close this window and start sign-in again.'));
5009
+ }, 10000);
5010
+ window.addEventListener('message', onMessage);
5011
+ try {
5012
+ opener.postMessage({ type: 'bounded-oidc-code', state, code: url.searchParams.get('code'),
5013
+ error: url.searchParams.get('error'), errorDescription: url.searchParams.get('error_description'),
5014
+ callbackUrl: url.href, appId: cfg.appId, issuer }, url.origin);
5015
+ }
5016
+ catch (error) {
5017
+ cleanup();
5018
+ reject(error);
5019
+ }
5020
+ });
5021
+ }
5022
+ async function finishLoginReturn(url, state) {
5023
+ try {
5024
+ if (!/^bounded\.(popup|redirect)\.[A-Za-z0-9_-]{22}$/.test(state)) {
5025
+ throw new Error('Invalid login state. Start sign-in again.');
5026
+ }
5027
+ if (state.startsWith('bounded.popup.')) {
5028
+ await handOffPopup(url, state);
5029
+ return { handled: true, user: null };
5030
+ }
5031
+ const raw = sessionStorage.getItem(pkceKey(state));
5032
+ if (!raw)
5033
+ throw new Error('Missing login attempt. Start sign-in again in this tab.');
5034
+ const pkce = JSON.parse(raw);
5035
+ if (pkce.state !== state)
5036
+ throw new Error('OAuth state mismatch. Start sign-in again.');
5037
+ await validateAttempt(pkce, url);
5038
+ const error = url.searchParams.get('error');
5039
+ if (error)
5040
+ throw new Error(url.searchParams.get('error_description') || `Login failed: ${error}`);
5041
+ const user = await exchangeCode(url.searchParams.get('code'), pkce);
5042
+ return { handled: true, user, idToken: sessionManager().getIdToken() };
5043
+ }
5044
+ finally {
5045
+ removeAttempt(state);
5046
+ const clean = cleanCallbackUrl(url);
5047
+ if (window.location.href === url.href) {
5048
+ window.history.replaceState(window.history.state, document.title, clean.pathname + clean.search + clean.hash);
5049
+ }
5050
+ }
5051
+ }
5052
+ /** Internal initialization step. Unrelated OAuth callbacks are left untouched. */
5053
+ function completePendingLogin() {
5054
+ if (!hasDOM$1())
5055
+ return Promise.resolve({ handled: false, user: null });
5056
+ const url = new URL(window.location.href);
5057
+ const state = url.searchParams.get('state');
5058
+ if (!(state === null || state === void 0 ? void 0 : state.startsWith(STATE_PREFIX)) || (!url.searchParams.get('code') && !url.searchParams.get('error'))) {
5059
+ return Promise.resolve({ handled: false, user: null });
5060
+ }
5061
+ const generation = getInitializationGeneration();
5062
+ if ((pendingCompletion === null || pendingCompletion === void 0 ? void 0 : pendingCompletion.href) === url.href && pendingCompletion.generation === generation)
5063
+ return pendingCompletion.promise;
5064
+ const promise = finishSharedLoginReturn(url, state).catch((error) => {
5065
+ loginReturnError = { generation, error: error instanceof Error ? error : new Error(String(error)) };
5066
+ throw loginReturnError.error;
5067
+ }).finally(() => {
5068
+ if ((pendingCompletion === null || pendingCompletion === void 0 ? void 0 : pendingCompletion.promise) === promise)
5069
+ pendingCompletion = undefined;
5070
+ });
5071
+ pendingCompletion = { href: url.href, generation, promise };
5072
+ return promise;
5073
+ }
5074
+ async function finishSharedLoginReturn(url, state) {
5075
+ var _a;
5076
+ var _b;
5077
+ const cfg = await core.getConfig();
5078
+ const issuer = await issuerBase();
5079
+ if (state.startsWith('bounded.redirect.')) {
5080
+ const raw = sessionStorage.getItem(pkceKey(state));
5081
+ // A second SDK configured for another app/issuer on this page does not own
5082
+ // this return. Leave its URL and proof available to the initiating app.
5083
+ try {
5084
+ const attempt = raw && JSON.parse(raw);
5085
+ if (attempt && (attempt.appId !== cfg.appId || attempt.issuer !== issuer))
5086
+ return { handled: false, user: null };
5087
+ }
5088
+ catch ( /* The owning completion reports malformed attempt data. */_c) { /* The owning completion reports malformed attempt data. */ }
5089
+ }
5090
+ const shared = (_a = (_b = window)[COMPLETIONS]) !== null && _a !== void 0 ? _a : (_b[COMPLETIONS] = new Map());
5091
+ const key = JSON.stringify([url.href, cfg.appId, issuer]);
5092
+ const existing = shared.get(key);
5093
+ if (!existing) {
5094
+ const completion = finishLoginReturn(url, state);
5095
+ shared.set(key, completion);
5096
+ return completion;
5097
+ }
5098
+ const generation = getInitializationGeneration();
5099
+ const result = await existing;
5100
+ if (result.user) {
5101
+ if (!result.idToken || sessionManager().getIdToken() !== result.idToken) {
5102
+ throw new Error('The completed login session has changed. Start sign-in again.');
5103
+ }
5104
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc',
5105
+ user: Object.assign(Object.assign({}, result.user), { provider: oidcProviderSingleton }), beforeCommit: () => {
5106
+ if (generation !== getInitializationGeneration())
5107
+ throw new Error('Login was superseded by SDK reinitialization. Start sign-in again.');
5108
+ } });
5109
+ }
5110
+ return result;
5111
+ }
4944
5112
  function normalizeMethod(value) {
4945
5113
  const normalized = String(value || '').trim().toLowerCase();
4946
5114
  return /^[a-z0-9_-]{1,40}$/.test(normalized) ? normalized : null;
@@ -4983,7 +5151,8 @@ function appendHostedLoginHints(params, opts) {
4983
5151
  // native (expo-web-browser) path. The caller decides where the PkceState lives
4984
5152
  // (web → sessionStorage across the full-page redirect; native → in-memory across
4985
5153
  // the awaited browser session).
4986
- async function buildAuthorize(opts) {
5154
+ async function buildAuthorize(opts, mode = 'redirect') {
5155
+ loginReturnError = undefined;
4987
5156
  const cfg = await core.getConfig();
4988
5157
  if (!cfg.appId)
4989
5158
  throw new Error('appId is not configured (init the SDK with your appId)');
@@ -4992,15 +5161,23 @@ async function buildAuthorize(opts) {
4992
5161
  const redirectUri = opts.redirectUri || (hasDOM$1() ? window.location.origin + window.location.pathname : undefined);
4993
5162
  if (!redirectUri)
4994
5163
  throw new Error('redirectUri is required on React Native — pass the https universal link the app owner registered.');
5164
+ if (hasDOM$1()) {
5165
+ const redirect = new URL(redirectUri);
5166
+ if (redirect.origin !== window.location.origin || redirect.hash || CALLBACK_PARAMS.some((key) => redirect.searchParams.has(key))) {
5167
+ throw new Error('redirectUri must be on this app origin, without a fragment or OAuth callback parameters.');
5168
+ }
5169
+ }
4995
5170
  const { verifier, challenge } = await makePkce();
4996
- const state = randomState();
5171
+ const state = `${STATE_PREFIX}${mode}.${randomState()}`;
4997
5172
  const base = await issuerBase();
4998
5173
  const params = new URLSearchParams({
4999
5174
  client_id: cfg.appId, redirect_uri: redirectUri, response_type: 'code', scope: 'openid profile email',
5000
5175
  code_challenge: challenge, code_challenge_method: 'S256', state,
5001
5176
  });
5002
5177
  appendHostedLoginHints(params, opts);
5003
- return { url: `${base}/api/auth/oauth2/authorize?${params.toString()}`, pkce: { verifier, state, redirectUri } };
5178
+ return { url: `${base}/api/auth/oauth2/authorize?${params.toString()}`, pkce: {
5179
+ verifier, state, redirectUri, appId: cfg.appId, issuer: base, createdAt: Date.now(),
5180
+ } };
5004
5181
  }
5005
5182
  // Are we on React Native (no DOM)? OIDC dispatch uses this to choose the native
5006
5183
  // expo-web-browser driver vs. the web full-page-redirect/popup driver.
@@ -5049,8 +5226,7 @@ function parseCallbackParams(callbackUrl) {
5049
5226
  }
5050
5227
  // Full native login: open the hosted page in the system auth session, await the
5051
5228
  // universal-link callback, verify state, and run the SAME PKCE code→token exchange
5052
- // as web. Returns the signed-in User (native completes inline — there's no separate
5053
- // completeLoginFromRedirect() round-trip the way the web full-page redirect needs).
5229
+ // as web. Returns the signed-in User inline; web redirects complete on the next init().
5054
5230
  async function runNativeLogin(opts) {
5055
5231
  var _a, _b;
5056
5232
  const { url, pkce } = await buildAuthorize(opts);
@@ -5077,7 +5253,7 @@ async function runNativeLogin(opts) {
5077
5253
  if (!returnedState || returnedState !== pkce.state) {
5078
5254
  throw new Error('OAuth state mismatch — possible CSRF; aborting');
5079
5255
  }
5080
- return await exchangeCode(code, pkce.verifier, pkce.redirectUri);
5256
+ return await exchangeCode(code, pkce);
5081
5257
  }
5082
5258
  finally {
5083
5259
  setAuthLoading(false);
@@ -5086,7 +5262,7 @@ async function runNativeLogin(opts) {
5086
5262
  /** Start hosted login (Google / Apple / email / hosted phone). Works on WEB and REACT NATIVE.
5087
5263
  *
5088
5264
  * WEB: navigates the whole page to the hosted Bounded login and resolves (void) as the page
5089
- * unloads; on return to `redirectUri?code=...` you call completeLoginFromRedirect().
5265
+ * unloads; init() completes the return to redirectUri automatically.
5090
5266
  *
5091
5267
  * REACT NATIVE: opens the hosted page via expo-web-browser, awaits the https universal-link
5092
5268
  * callback, runs the PKCE exchange INLINE, and resolves with the signed-in User. No
@@ -5104,63 +5280,32 @@ async function loginWithRedirect(opts) {
5104
5280
  // is per-tab and cleared on close — appropriate for a short-lived auth handshake. Keyed by
5105
5281
  // this attempt's own state so a second concurrent login cannot overwrite it.
5106
5282
  sessionStorage.setItem(pkceKey(pkce.state), JSON.stringify(pkce));
5107
- setAuthLoading(true); // reflect "signing in…" right up to navigation; the return page's
5108
- window.location.href = url; // completeLoginFromRedirect() keeps it true until the user resolves.
5109
- }
5110
- /** Finish login on the redirect_uri page (WEB ONLY): exchange ?code= for a token (PKCE-verified),
5111
- * store the session, and return the User. Idempotent: returns null if there's no ?code= in the URL.
5112
- *
5113
- * On REACT NATIVE this returns null and is a no-op: native login completes INLINE inside
5114
- * loginWithRedirect() (which resolves with the User), so there is no separate redirect
5115
- * page to finish from. Calling it on RN is harmless. */
5116
- async function completeLoginFromRedirect() {
5117
- if (!hasDOM$1())
5118
- return null;
5119
- const url = new URL(window.location.href);
5120
- const code = url.searchParams.get('code');
5121
- const returnedState = url.searchParams.get('state');
5122
- if (!code)
5123
- return null;
5124
- // POPUP path: if we're the redirect_uri page running INSIDE the loginWithPopup() window,
5125
- // hand the code to the opener and close — no separate completeLoginInPopup() call needed.
5126
- // The opener is the same app/origin, so post to our own origin (the opener's message
5127
- // handler checks ev.origin === redirect_uri origin). One call now finishes BOTH flows.
5128
- if (window.opener && window.opener !== window) {
5129
- try {
5130
- window.opener.postMessage({ type: 'bounded-oidc-code', code, state: returnedState }, window.location.origin);
5131
- }
5132
- catch ( /* ignore */_a) { /* ignore */ }
5133
- try {
5134
- window.close();
5135
- }
5136
- catch ( /* ignore */_b) { /* ignore */ }
5137
- return null;
5138
- }
5283
+ // This promise represents dispatch, not authentication on the next page.
5284
+ // Browsers can cancel navigation (for example at beforeunload) without firing
5285
+ // pageshow. Never leave this document's SDK loading after dispatch settles.
5139
5286
  setAuthLoading(true);
5140
5287
  try {
5141
- // Read back the exact record THIS attempt wrote, keyed by the state the issuer
5142
- // returned, so a concurrent login's record is never consumed by mistake.
5143
- const raw = returnedState ? sessionStorage.getItem(pkceKey(returnedState)) : null;
5144
- if (!raw)
5145
- throw new Error('Missing PKCE state — start login with loginWithRedirect()');
5146
- const pkce = JSON.parse(raw);
5147
- if (!returnedState || returnedState !== pkce.state)
5148
- throw new Error('OAuth state mismatch — possible CSRF; aborting');
5149
- const user = await exchangeCode(code, pkce.verifier, pkce.redirectUri);
5150
- sessionStorage.removeItem(pkceKey(returnedState));
5151
- // Strip code/state from the URL so a reload doesn't re-exchange (codes are single-use anyway).
5152
- url.searchParams.delete('code');
5153
- url.searchParams.delete('state');
5154
- window.history.replaceState({}, document.title, url.pathname + url.search + url.hash);
5155
- return user;
5288
+ window.location.href = url;
5289
+ }
5290
+ catch (error) {
5291
+ removeAttempt(pkce.state);
5292
+ throw error;
5156
5293
  }
5157
5294
  finally {
5158
5295
  setAuthLoading(false);
5159
5296
  }
5160
5297
  }
5298
+ /** Optional explicit completion. Normal apps only await init(), which already finishes login.
5299
+ * Concurrent calls share one exchange. Returns null when no Bounded return is present. */
5300
+ async function completeLoginFromRedirect() {
5301
+ const result = await completePendingLogin();
5302
+ const error = getLoginReturnError();
5303
+ if (error)
5304
+ throw error;
5305
+ return result.user;
5306
+ }
5161
5307
  /** Popup variant: open the hosted login in a popup; resolves with the User once the popup's
5162
- * redirect_uri page posts the code back. The redirect_uri page just calls the usual
5163
- * completeLoginFromRedirect() (which auto-detects the popup) — no separate call needed. */
5308
+ * redirect_uri page initializes the SDK, which hands the code back automatically. */
5164
5309
  async function loginWithPopup(opts) {
5165
5310
  if (!hasDOM$1())
5166
5311
  throw new Error('loginWithPopup is web-only — on React Native use loginWithRedirect()');
@@ -5179,7 +5324,7 @@ async function loginWithPopup(opts) {
5179
5324
  throw new Error('Popup blocked — use loginWithRedirect() instead');
5180
5325
  let authorization;
5181
5326
  try {
5182
- authorization = await buildAuthorize(opts);
5327
+ authorization = await buildAuthorize(opts, 'popup');
5183
5328
  }
5184
5329
  catch (error) {
5185
5330
  try {
@@ -5214,6 +5359,7 @@ async function loginWithPopup(opts) {
5214
5359
  popup.location.href = url;
5215
5360
  }
5216
5361
  catch (_d) {
5362
+ removeAttempt(pkce.state);
5217
5363
  try {
5218
5364
  popup.close();
5219
5365
  }
@@ -5229,17 +5375,21 @@ async function loginWithPopup(opts) {
5229
5375
  let settled = false;
5230
5376
  let timer;
5231
5377
  let navigationTimer;
5378
+ let completionTimer;
5232
5379
  const cleanup = () => {
5233
5380
  settled = true;
5234
5381
  clearInterval(timer);
5235
5382
  clearTimeout(navigationTimer);
5383
+ clearTimeout(completionTimer);
5236
5384
  window.removeEventListener('message', onMsg);
5385
+ removeAttempt(pkce.state);
5237
5386
  };
5238
5387
  const onMsg = async (ev) => {
5239
- if (ev.origin !== expectedOrigin)
5240
- return; // only trust the redirect_uri origin
5388
+ if (settled || ev.origin !== expectedOrigin || ev.source !== popup)
5389
+ return;
5241
5390
  const d = ev.data;
5242
- if (!d || d.type !== 'bounded-oidc-code' || typeof d.code !== 'string')
5391
+ if (!d || d.type !== 'bounded-oidc-code' ||
5392
+ (typeof d.code !== 'string' && typeof d.error !== 'string'))
5243
5393
  return;
5244
5394
  // Validate state BEFORE tearing down this listener, against the record THIS
5245
5395
  // attempt captured in the closure (`pkce`), not a shared sessionStorage reread.
@@ -5247,10 +5397,15 @@ async function loginWithPopup(opts) {
5247
5397
  // stay registered for our own code, instead of cleaning up on another's message.
5248
5398
  if (d.state !== pkce.state)
5249
5399
  return;
5400
+ if (d.appId !== pkce.appId || d.issuer !== pkce.issuer)
5401
+ return;
5250
5402
  cleanup();
5251
5403
  try {
5252
- const user = await exchangeCode(d.code, pkce.verifier, pkce.redirectUri);
5253
- sessionStorage.removeItem(pkceKey(pkce.state));
5404
+ await validateAttempt(pkce, new URL(d.callbackUrl));
5405
+ popup.postMessage({ type: 'bounded-oidc-accepted', state: pkce.state }, expectedOrigin);
5406
+ if (d.error)
5407
+ throw new Error(typeof d.errorDescription === 'string' ? d.errorDescription : `Login failed: ${d.error}`);
5408
+ const user = await exchangeCode(d.code, pkce);
5254
5409
  try {
5255
5410
  popup.close();
5256
5411
  }
@@ -5258,10 +5413,22 @@ async function loginWithPopup(opts) {
5258
5413
  resolve(user);
5259
5414
  }
5260
5415
  catch (e) {
5416
+ try {
5417
+ popup.close();
5418
+ }
5419
+ catch ( /* ignore */_b) { /* ignore */ }
5261
5420
  reject(e);
5262
5421
  }
5263
5422
  };
5264
5423
  window.addEventListener('message', onMsg);
5424
+ completionTimer = setTimeout(() => {
5425
+ cleanup();
5426
+ try {
5427
+ popup.close();
5428
+ }
5429
+ catch ( /* ignore */_a) { /* ignore */ }
5430
+ reject(new Error('Login expired or its popup disconnected. Start sign-in again.'));
5431
+ }, LOGIN_TIMEOUT_MS);
5265
5432
  // Some browser/extension combinations can return a real popup handle but
5266
5433
  // strand the window at about:blank instead of navigating to the hosted
5267
5434
  // authorize URL. The old code then waited forever because the popup was
@@ -5319,33 +5486,20 @@ async function loginWithPopup(opts) {
5319
5486
  setAuthLoading(false);
5320
5487
  }
5321
5488
  }
5322
- /** Call this on the redirect_uri page when login was opened via loginWithPopup(): posts the
5323
- * code back to the opener and closes. (For the redirect variant use completeLoginFromRedirect.) */
5324
- function completeLoginInPopup(openerOrigin) {
5325
- if (!hasDOM$1() || !window.opener)
5326
- return;
5327
- const url = new URL(window.location.href);
5328
- const code = url.searchParams.get('code');
5329
- const state = url.searchParams.get('state');
5330
- if (!code)
5331
- return;
5332
- try {
5333
- window.opener.postMessage({ type: 'bounded-oidc-code', code, state }, openerOrigin);
5334
- }
5335
- catch ( /* ignore */_a) { /* ignore */ }
5336
- try {
5337
- window.close();
5338
- }
5339
- catch ( /* ignore */_b) { /* ignore */ }
5340
- }
5341
- async function exchangeCode(code, verifier, redirectUri) {
5342
- const cfg = await core.getConfig();
5343
- const base = await issuerBase();
5489
+ async function exchangeCode(code, pkce) {
5490
+ const generation = getInitializationGeneration();
5491
+ const beforeCommit = () => {
5492
+ if (generation !== getInitializationGeneration())
5493
+ throw new Error('Login was superseded by SDK reinitialization. Start sign-in again.');
5494
+ };
5495
+ await validateAttempt(pkce);
5496
+ beforeCommit();
5497
+ const base = pkce.issuer;
5344
5498
  // OIDC token endpoint: application/x-www-form-urlencoded (the standard). Public client (no
5345
5499
  // secret) authenticated by PKCE. The code is single-use + short-TTL on the server.
5346
5500
  const body = new URLSearchParams({
5347
- grant_type: 'authorization_code', code, redirect_uri: redirectUri,
5348
- client_id: cfg.appId, code_verifier: verifier,
5501
+ grant_type: 'authorization_code', code, redirect_uri: pkce.redirectUri,
5502
+ client_id: pkce.appId, code_verifier: pkce.verifier,
5349
5503
  });
5350
5504
  let res;
5351
5505
  try {
@@ -5369,10 +5523,20 @@ async function exchangeCode(code, verifier, redirectUri) {
5369
5523
  const accessToken = tokenField(data, 'access_token', 'accessToken') || idToken;
5370
5524
  const refreshToken = boundedRefreshTokenField(data);
5371
5525
  const user = userFromIdToken(idToken);
5372
- await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base);
5526
+ // Reinitialization while the request was in flight must not install this
5527
+ // app's result under a different app's session manager/configuration.
5528
+ beforeCommit();
5529
+ const rollback = await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base, beforeCommit);
5373
5530
  // Adopt on EVERY session surface (see establishInlineSession) - hosted
5374
5531
  // popup/redirect completions all land here, so this closes their gap too.
5375
- await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user });
5532
+ try {
5533
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user, beforeCommit });
5534
+ }
5535
+ catch (error) {
5536
+ if (typeof rollback === 'function')
5537
+ await rollback();
5538
+ throw error;
5539
+ }
5376
5540
  // OIDC login does establish the issuer's Better Auth browser cookie. Record
5377
5541
  // that fact separately from the provider name so logout clears the cookie
5378
5542
  // and returns through the registered app origin. Stamped AFTER the adoption
@@ -5723,7 +5887,7 @@ async function getAuthProvider(config) {
5723
5887
  // manager as legacy email sessions. Human login should be started
5724
5888
  // with loginWithRedirect(); this provider is kept as the restore and
5725
5889
  // logout surface for those sessions.
5726
- currentAuthProvider = new (await Promise.resolve().then(function () { return require('./email-auth-provider-CGhDN-8d.js'); })).EmailAuthProvider();
5890
+ currentAuthProvider = new (await Promise.resolve().then(function () { return require('./email-auth-provider-Czo-xM6n.js'); })).EmailAuthProvider();
5727
5891
  break;
5728
5892
  case "phantom": {
5729
5893
  // First-class Solana WALLET LOGIN (the poof/TaroBase-style "connect
@@ -5765,7 +5929,7 @@ async function getAuthProvider(config) {
5765
5929
  // (bounded-login-widget.ts), so registration never counts as
5766
5930
  // evidence the user has a wallet.
5767
5931
  await ensureSolanaMobileWalletRegistered(config, configuredNetwork);
5768
- const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-FY9JLEPR.js'); });
5932
+ const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-C7Q0haGY.js'); });
5769
5933
  currentAuthProvider = new InjectedWalletProvider(resolveInjectedWalletConfig(config), rpcUrl, configuredNetwork);
5770
5934
  break;
5771
5935
  }
@@ -5778,7 +5942,7 @@ async function getAuthProvider(config) {
5778
5942
  // Selecting authMethod:'evm-wallet' IS the opt-in — pass evmWalletConfig
5779
5943
  // to point at a specific wallet or set chainId/domain.
5780
5944
  const evmWalletConfig = ((_a = config.evmWalletConfig) !== null && _a !== void 0 ? _a : {});
5781
- const { InjectedEvmWalletProvider } = await Promise.resolve().then(function () { return require('./injected-evm-wallet-provider-Cym2uENX.js'); });
5945
+ const { InjectedEvmWalletProvider } = await Promise.resolve().then(function () { return require('./injected-evm-wallet-provider-_UNpc_hM.js'); });
5782
5946
  currentAuthProvider = new InjectedEvmWalletProvider(evmWalletConfig);
5783
5947
  break;
5784
5948
  }
@@ -5803,7 +5967,7 @@ async function getAuthProvider(config) {
5803
5967
  break;
5804
5968
  case "guest":
5805
5969
  // Anonymous auth: a device-local ed25519 keypair signs the challenge.
5806
- currentAuthProvider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-BT9JPOpp.js'); })).GuestAuthProvider();
5970
+ currentAuthProvider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-DJ5nCGjq.js'); })).GuestAuthProvider();
5807
5971
  break;
5808
5972
  }
5809
5973
  if (!currentAuthProvider) {
@@ -5865,7 +6029,7 @@ async function signInAnonymously() {
5865
6029
  // Lazy-load the guest provider so its ed25519/web3 dependencies stay out of
5866
6030
  // the main bundle for data-only apps. Specifier is a static string literal
5867
6031
  // so bundle-splitting can see it.
5868
- const provider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-BT9JPOpp.js'); })).GuestAuthProvider();
6032
+ const provider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-DJ5nCGjq.js'); })).GuestAuthProvider();
5869
6033
  setAuthLoading(true); // reflect "signing in…" while the guest keypair session is minted
5870
6034
  // adoptSessionProvider awaits only getConfig() (already resolved here) and then
5871
6035
  // runs every write - provider/method/config/marker/user - in one synchronous
@@ -6036,7 +6200,7 @@ function ensureSharedWalletLoginReady() {
6036
6200
  if (!sharedWalletReady) {
6037
6201
  sharedWalletReady = (async () => {
6038
6202
  await core.getConfig();
6039
- await Promise.resolve().then(function () { return require('./injected-wallet-provider-FY9JLEPR.js'); });
6203
+ await Promise.resolve().then(function () { return require('./injected-wallet-provider-C7Q0haGY.js'); });
6040
6204
  })().catch((error) => {
6041
6205
  sharedWalletReady = null;
6042
6206
  throw error;
@@ -6319,6 +6483,10 @@ let authLoadingListeners = [];
6319
6483
  let initCompleted = false;
6320
6484
  let isAuthLoading = false;
6321
6485
  let authReadyPromise = null;
6486
+ let initializingConfig = null;
6487
+ let initializationGeneration = 0;
6488
+ /** Internal guard for a login result arriving after the app reconfigured the SDK. */
6489
+ function getInitializationGeneration() { return initializationGeneration; }
6322
6490
  // This file acts as a middleware for the global state of the SDK
6323
6491
  // This mostly involves setting up the AuthProvider and managing the current user
6324
6492
  async function initialize(newConfig) {
@@ -6344,6 +6512,19 @@ async function initialize(newConfig) {
6344
6512
  authProviderInstance = await getAuthProvider(newConfig);
6345
6513
  // Initialize config with auth provider
6346
6514
  await core.init(Object.assign(Object.assign({}, newConfig), { authProvider: authProviderInstance }));
6515
+ // A return establishes its own provider/session, or hands off a popup.
6516
+ // Restoring the old provider afterwards would overwrite the new login.
6517
+ try {
6518
+ if ((await completePendingLogin()).handled) {
6519
+ initCompleted = true;
6520
+ return;
6521
+ }
6522
+ }
6523
+ catch (_a) {
6524
+ // A cancelled/failed return is a login failure, not a broken SDK.
6525
+ // The completion layer keeps it in getLoginReturnError(); restore
6526
+ // any existing session and leave sign-in/readiness usable for retry.
6527
+ }
6347
6528
  // Now that core is initialized, update the auth method on the config
6348
6529
  const authMethod = getCurrentAuthMethod();
6349
6530
  if (authMethod) {
@@ -6375,7 +6556,42 @@ async function initialize(newConfig) {
6375
6556
  setAuthLoading(false);
6376
6557
  }
6377
6558
  }
6559
+ // Compare data options by value, but preserve identity for callbacks and class
6560
+ // instances such as providers. Comparing only app/endpoints would silently drop
6561
+ // a concurrent change to wallet policy or provider configuration.
6562
+ function sameConfigValue(a, b, seen = new WeakMap()) {
6563
+ var _a;
6564
+ if (Object.is(a, b))
6565
+ return true;
6566
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object')
6567
+ return false;
6568
+ if (Array.isArray(a) !== Array.isArray(b))
6569
+ return false;
6570
+ if (Array.isArray(a) && a.length !== b.length)
6571
+ return false;
6572
+ if (!Array.isArray(a)) {
6573
+ for (const value of [a, b]) {
6574
+ const prototype = Object.getPrototypeOf(value);
6575
+ if (prototype !== null && ((_a = prototype.constructor) === null || _a === void 0 ? void 0 : _a.name) !== 'Object')
6576
+ return false;
6577
+ }
6578
+ }
6579
+ if (seen.has(a))
6580
+ return seen.get(a) === b;
6581
+ seen.set(a, b);
6582
+ const left = Object.keys(a), right = Object.keys(b);
6583
+ return left.length === right.length && left.every((key) => Object.prototype.hasOwnProperty.call(b, key)
6584
+ && sameConfigValue(a[key], b[key], seen));
6585
+ }
6378
6586
  function init(newConfig) {
6587
+ if (initializingConfig) {
6588
+ if (sameConfigValue(initializingConfig, newConfig)) {
6589
+ return authReadyPromise;
6590
+ }
6591
+ return Promise.reject(new Error('SDK initialization is in progress. Await init() before changing its configuration.'));
6592
+ }
6593
+ initializingConfig = Object.assign({}, newConfig);
6594
+ initializationGeneration++;
6379
6595
  let resolveReady;
6380
6596
  let rejectReady;
6381
6597
  const ready = new Promise((resolve, reject) => {
@@ -6386,7 +6602,7 @@ function init(newConfig) {
6386
6602
  // auth-loading listener to call waitForAuthReady() synchronously when init()
6387
6603
  // announces that session restoration has begun.
6388
6604
  authReadyPromise = ready;
6389
- void initialize(newConfig).then(resolveReady, rejectReady);
6605
+ void initialize(initializingConfig).then(() => { initializingConfig = null; resolveReady(); }, (error) => { initializingConfig = null; rejectReady(error); });
6390
6606
  return ready;
6391
6607
  }
6392
6608
  /**
@@ -8380,12 +8596,12 @@ exports.bs58 = bs58;
8380
8596
  exports.bufferExports = bufferExports;
8381
8597
  exports.builds = builds;
8382
8598
  exports.completeLoginFromRedirect = completeLoginFromRedirect;
8383
- exports.completeLoginInPopup = completeLoginInPopup;
8384
8599
  exports.confirmAndCheckTransaction = confirmAndCheckTransaction;
8385
8600
  exports.getAuthLoading = getAuthLoading;
8386
8601
  exports.getAuthProvider = getAuthProvider;
8387
8602
  exports.getCurrentUser = getCurrentUser;
8388
8603
  exports.getIdToken = getIdToken;
8604
+ exports.getLoginReturnError = getLoginReturnError;
8389
8605
  exports.getPlatform = getPlatform;
8390
8606
  exports.getWallets = getWallets;
8391
8607
  exports.hasTurnkeyWallet = hasTurnkeyWallet;
@@ -8420,4 +8636,4 @@ exports.useAuth = useAuth;
8420
8636
  exports.useBuildRun = useBuildRun;
8421
8637
  exports.useQuery = useQuery;
8422
8638
  exports.waitForAuthReady = waitForAuthReady;
8423
- //# sourceMappingURL=index.native-CNW3iAoP.js.map
8639
+ //# sourceMappingURL=index.native-BpcNwWQX.js.map