@bounded-sh/client 0.0.104 → 0.0.105

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 (55) hide show
  1. package/README.md +15 -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-CB5ZBjJL.js} +2 -2
  5. package/dist/{email-auth-provider-aMa0EDuc.js.map → email-auth-provider-CB5ZBjJL.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-sA_9RTRl.mjs} +2 -2
  11. package/dist/{email-auth-provider-CdBqgLWw.mjs.map → email-auth-provider-sA_9RTRl.mjs.map} +1 -1
  12. package/dist/global.d.ts +2 -0
  13. package/dist/{guest-auth-provider-BiWPelOD.mjs → guest-auth-provider-1mrlNF2G.mjs} +2 -2
  14. package/dist/{guest-auth-provider-pDVr5YRg.mjs.map → guest-auth-provider-1mrlNF2G.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-DXNCZ0R6.js} +2 -2
  18. package/dist/{guest-auth-provider-Ca50pehK.js.map → guest-auth-provider-DXNCZ0R6.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/{index-DKkAO0g-.mjs → index-Bu6vnwxM.mjs} +377 -121
  22. package/dist/index-Bu6vnwxM.mjs.map +1 -0
  23. package/dist/{index-DTBxbfUl.js → index-yb-jFt5B.js} +377 -121
  24. package/dist/index-yb-jFt5B.js.map +1 -0
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +2 -2
  27. package/dist/index.mjs +1 -1
  28. package/dist/{index.native-Bl2W-Wsi.mjs → index.native-BSOqaKIz.mjs} +313 -97
  29. package/dist/index.native-BSOqaKIz.mjs.map +1 -0
  30. package/dist/{index.native-CNW3iAoP.js → index.native-BpcNwWQX.js} +313 -97
  31. package/dist/index.native-BpcNwWQX.js.map +1 -0
  32. package/dist/index.native.d.ts +1 -1
  33. package/dist/index.native.js +2 -2
  34. package/dist/index.native.mjs +1 -1
  35. package/dist/{injected-evm-wallet-provider-BPkcFSQX.mjs → injected-evm-wallet-provider-BuDISWSq.mjs} +2 -2
  36. package/dist/{injected-evm-wallet-provider-BPkcFSQX.mjs.map → injected-evm-wallet-provider-BuDISWSq.mjs.map} +1 -1
  37. package/dist/{injected-evm-wallet-provider-B8PWHQdb.js → injected-evm-wallet-provider-D7tQrDaO.js} +2 -2
  38. package/dist/{injected-evm-wallet-provider-B8PWHQdb.js.map → injected-evm-wallet-provider-D7tQrDaO.js.map} +1 -1
  39. package/dist/{injected-evm-wallet-provider-HbgMQAbf.mjs → injected-evm-wallet-provider-Xb2G-ZGI.mjs} +2 -2
  40. package/dist/{injected-evm-wallet-provider-HbgMQAbf.mjs.map → injected-evm-wallet-provider-Xb2G-ZGI.mjs.map} +1 -1
  41. package/dist/{injected-evm-wallet-provider-Cym2uENX.js → injected-evm-wallet-provider-_UNpc_hM.js} +2 -2
  42. package/dist/{injected-evm-wallet-provider-Cym2uENX.js.map → injected-evm-wallet-provider-_UNpc_hM.js.map} +1 -1
  43. package/dist/{injected-wallet-provider-BGeNaoEY.mjs → injected-wallet-provider-BDCLkdR_.mjs} +2 -2
  44. package/dist/{injected-wallet-provider-BGeNaoEY.mjs.map → injected-wallet-provider-BDCLkdR_.mjs.map} +1 -1
  45. package/dist/{injected-wallet-provider-FY9JLEPR.js → injected-wallet-provider-C7Q0haGY.js} +2 -2
  46. package/dist/{injected-wallet-provider-FY9JLEPR.js.map → injected-wallet-provider-C7Q0haGY.js.map} +1 -1
  47. package/dist/{injected-wallet-provider-oWtACL_7.js → injected-wallet-provider-CFwJYxh2.js} +2 -2
  48. package/dist/{injected-wallet-provider-oWtACL_7.js.map → injected-wallet-provider-CFwJYxh2.js.map} +1 -1
  49. package/dist/{injected-wallet-provider-D39pzPTV.mjs → injected-wallet-provider-DZOwvKgd.mjs} +2 -2
  50. package/dist/{injected-wallet-provider-D39pzPTV.mjs.map → injected-wallet-provider-DZOwvKgd.mjs.map} +1 -1
  51. package/package.json +2 -2
  52. package/dist/index-DKkAO0g-.mjs.map +0 -1
  53. package/dist/index-DTBxbfUl.js.map +0 -1
  54. package/dist/index.native-Bl2W-Wsi.mjs.map +0 -1
  55. package/dist/index.native-CNW3iAoP.js.map +0 -1
@@ -4809,9 +4809,18 @@ const OIDC_NO_WALLET = 'This session has no Turnkey wallet. Sign in again to pro
4809
4809
  // same tab (redirect + popup, or two popups) each own a distinct record, so a second
4810
4810
  // attempt cannot clobber the first's verifier/state/redirectUri.
4811
4811
  const PKCE_STORE_PREFIX = 'bounded.oidc.pkce';
4812
+ const STATE_PREFIX = 'bounded.';
4813
+ const LOGIN_TIMEOUT_MS = 10 * 60000;
4814
+ const CALLBACK_PARAMS = ['code', 'state', 'error', 'error_description', 'iss'];
4812
4815
  function pkceKey(state) {
4813
4816
  return `${PKCE_STORE_PREFIX}.${state}`;
4814
4817
  }
4818
+ function removeAttempt(state) {
4819
+ try {
4820
+ sessionStorage.removeItem(pkceKey(state));
4821
+ }
4822
+ catch ( /* Storage may have been disabled mid-login. */_a) { /* Storage may have been disabled mid-login. */ }
4823
+ }
4815
4824
  const ISSUER_SESSION_KIND_KEY = 'bounded_issuer_session_kind';
4816
4825
  // Active session store (Web on web, RN on React Native) — selected by config,
4817
4826
  // not by `typeof window` (truthy on RN). OIDC redirect/popup is web-only, but the
@@ -4920,6 +4929,165 @@ async function makePkce() {
4920
4929
  function randomState() {
4921
4930
  return b64url(getPlatform().getRandomBytes(16));
4922
4931
  }
4932
+ let pendingCompletion;
4933
+ let loginReturnError;
4934
+ /** The last OAuth return failure in this initialization, or null. Initialization
4935
+ * remains usable so the UI can display this error and offer a fresh sign-in. */
4936
+ function getLoginReturnError() {
4937
+ return (loginReturnError === null || loginReturnError === void 0 ? void 0 : loginReturnError.generation) === getInitializationGeneration() ? loginReturnError.error : null;
4938
+ }
4939
+ // A hosted page can contain both an app bundle and the platform widget's SDK.
4940
+ // They share one code exchange, while adopting the session in their own registries.
4941
+ const COMPLETIONS = Symbol.for('bounded.oidc.completions');
4942
+ function cleanCallbackUrl(url) {
4943
+ const clean = new URL(url);
4944
+ for (const key of CALLBACK_PARAMS)
4945
+ clean.searchParams.delete(key);
4946
+ return clean;
4947
+ }
4948
+ async function validateAttempt(pkce, callback) {
4949
+ const cfg = await getConfig();
4950
+ if (!pkce || typeof pkce.verifier !== 'string' || !pkce.verifier ||
4951
+ pkce.appId !== cfg.appId || pkce.issuer !== await issuerBase()) {
4952
+ throw new Error('Login belongs to a different app or auth server. Start sign-in again.');
4953
+ }
4954
+ if (!Number.isFinite(pkce.createdAt) || Date.now() - pkce.createdAt > LOGIN_TIMEOUT_MS || pkce.createdAt > Date.now()) {
4955
+ throw new Error('Login expired. Start sign-in again.');
4956
+ }
4957
+ if (callback && (cleanCallbackUrl(callback).href !== cleanCallbackUrl(new URL(pkce.redirectUri)).href ||
4958
+ (callback.searchParams.has('iss') && callback.searchParams.get('iss') !== pkce.issuer))) {
4959
+ throw new Error('Login returned to a different address or from a different auth server. Start sign-in again.');
4960
+ }
4961
+ }
4962
+ // The receiver acknowledges only after checking origin, window and state. An
4963
+ // absent/disconnected opener must never leave a popup claiming success.
4964
+ async function handOffPopup(url, state) {
4965
+ const cfg = await getConfig();
4966
+ const issuer = await issuerBase();
4967
+ const opener = window.opener;
4968
+ if (!opener || opener === window || opener.closed) {
4969
+ throw new Error('The original sign-in window is unavailable. Close this window and sign in again in the original tab.');
4970
+ }
4971
+ return new Promise((resolve, reject) => {
4972
+ const cleanup = () => { clearTimeout(timer); window.removeEventListener('message', onMessage); };
4973
+ const onMessage = (event) => {
4974
+ var _a;
4975
+ if (event.source !== opener || event.origin !== url.origin ||
4976
+ ((_a = event.data) === null || _a === void 0 ? void 0 : _a.type) !== 'bounded-oidc-accepted' || event.data.state !== state)
4977
+ return;
4978
+ cleanup();
4979
+ try {
4980
+ window.close();
4981
+ }
4982
+ catch ( /* The opener also closes its own popup handle. */_b) { /* The opener also closes its own popup handle. */ }
4983
+ resolve();
4984
+ };
4985
+ const timer = setTimeout(() => {
4986
+ cleanup();
4987
+ reject(new Error('Could not reach the original sign-in tab. Close this window and start sign-in again.'));
4988
+ }, 10000);
4989
+ window.addEventListener('message', onMessage);
4990
+ try {
4991
+ opener.postMessage({ type: 'bounded-oidc-code', state, code: url.searchParams.get('code'),
4992
+ error: url.searchParams.get('error'), errorDescription: url.searchParams.get('error_description'),
4993
+ callbackUrl: url.href, appId: cfg.appId, issuer }, url.origin);
4994
+ }
4995
+ catch (error) {
4996
+ cleanup();
4997
+ reject(error);
4998
+ }
4999
+ });
5000
+ }
5001
+ async function finishLoginReturn(url, state) {
5002
+ try {
5003
+ if (!/^bounded\.(popup|redirect)\.[A-Za-z0-9_-]{22}$/.test(state)) {
5004
+ throw new Error('Invalid login state. Start sign-in again.');
5005
+ }
5006
+ if (state.startsWith('bounded.popup.')) {
5007
+ await handOffPopup(url, state);
5008
+ return { handled: true, user: null };
5009
+ }
5010
+ const raw = sessionStorage.getItem(pkceKey(state));
5011
+ if (!raw)
5012
+ throw new Error('Missing login attempt. Start sign-in again in this tab.');
5013
+ const pkce = JSON.parse(raw);
5014
+ if (pkce.state !== state)
5015
+ throw new Error('OAuth state mismatch. Start sign-in again.');
5016
+ await validateAttempt(pkce, url);
5017
+ const error = url.searchParams.get('error');
5018
+ if (error)
5019
+ throw new Error(url.searchParams.get('error_description') || `Login failed: ${error}`);
5020
+ const user = await exchangeCode(url.searchParams.get('code'), pkce);
5021
+ return { handled: true, user, idToken: sessionManager().getIdToken() };
5022
+ }
5023
+ finally {
5024
+ removeAttempt(state);
5025
+ const clean = cleanCallbackUrl(url);
5026
+ if (window.location.href === url.href) {
5027
+ window.history.replaceState(window.history.state, document.title, clean.pathname + clean.search + clean.hash);
5028
+ }
5029
+ }
5030
+ }
5031
+ /** Internal initialization step. Unrelated OAuth callbacks are left untouched. */
5032
+ function completePendingLogin() {
5033
+ if (!hasDOM$1())
5034
+ return Promise.resolve({ handled: false, user: null });
5035
+ const url = new URL(window.location.href);
5036
+ const state = url.searchParams.get('state');
5037
+ if (!(state === null || state === void 0 ? void 0 : state.startsWith(STATE_PREFIX)) || (!url.searchParams.get('code') && !url.searchParams.get('error'))) {
5038
+ return Promise.resolve({ handled: false, user: null });
5039
+ }
5040
+ const generation = getInitializationGeneration();
5041
+ if ((pendingCompletion === null || pendingCompletion === void 0 ? void 0 : pendingCompletion.href) === url.href && pendingCompletion.generation === generation)
5042
+ return pendingCompletion.promise;
5043
+ const promise = finishSharedLoginReturn(url, state).catch((error) => {
5044
+ loginReturnError = { generation, error: error instanceof Error ? error : new Error(String(error)) };
5045
+ throw loginReturnError.error;
5046
+ }).finally(() => {
5047
+ if ((pendingCompletion === null || pendingCompletion === void 0 ? void 0 : pendingCompletion.promise) === promise)
5048
+ pendingCompletion = undefined;
5049
+ });
5050
+ pendingCompletion = { href: url.href, generation, promise };
5051
+ return promise;
5052
+ }
5053
+ async function finishSharedLoginReturn(url, state) {
5054
+ var _a;
5055
+ var _b;
5056
+ const cfg = await getConfig();
5057
+ const issuer = await issuerBase();
5058
+ if (state.startsWith('bounded.redirect.')) {
5059
+ const raw = sessionStorage.getItem(pkceKey(state));
5060
+ // A second SDK configured for another app/issuer on this page does not own
5061
+ // this return. Leave its URL and proof available to the initiating app.
5062
+ try {
5063
+ const attempt = raw && JSON.parse(raw);
5064
+ if (attempt && (attempt.appId !== cfg.appId || attempt.issuer !== issuer))
5065
+ return { handled: false, user: null };
5066
+ }
5067
+ catch ( /* The owning completion reports malformed attempt data. */_c) { /* The owning completion reports malformed attempt data. */ }
5068
+ }
5069
+ const shared = (_a = (_b = window)[COMPLETIONS]) !== null && _a !== void 0 ? _a : (_b[COMPLETIONS] = new Map());
5070
+ const key = JSON.stringify([url.href, cfg.appId, issuer]);
5071
+ const existing = shared.get(key);
5072
+ if (!existing) {
5073
+ const completion = finishLoginReturn(url, state);
5074
+ shared.set(key, completion);
5075
+ return completion;
5076
+ }
5077
+ const generation = getInitializationGeneration();
5078
+ const result = await existing;
5079
+ if (result.user) {
5080
+ if (!result.idToken || sessionManager().getIdToken() !== result.idToken) {
5081
+ throw new Error('The completed login session has changed. Start sign-in again.');
5082
+ }
5083
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc',
5084
+ user: Object.assign(Object.assign({}, result.user), { provider: oidcProviderSingleton }), beforeCommit: () => {
5085
+ if (generation !== getInitializationGeneration())
5086
+ throw new Error('Login was superseded by SDK reinitialization. Start sign-in again.');
5087
+ } });
5088
+ }
5089
+ return result;
5090
+ }
4923
5091
  function normalizeMethod(value) {
4924
5092
  const normalized = String(value || '').trim().toLowerCase();
4925
5093
  return /^[a-z0-9_-]{1,40}$/.test(normalized) ? normalized : null;
@@ -4962,7 +5130,8 @@ function appendHostedLoginHints(params, opts) {
4962
5130
  // native (expo-web-browser) path. The caller decides where the PkceState lives
4963
5131
  // (web → sessionStorage across the full-page redirect; native → in-memory across
4964
5132
  // the awaited browser session).
4965
- async function buildAuthorize(opts) {
5133
+ async function buildAuthorize(opts, mode = 'redirect') {
5134
+ loginReturnError = undefined;
4966
5135
  const cfg = await getConfig();
4967
5136
  if (!cfg.appId)
4968
5137
  throw new Error('appId is not configured (init the SDK with your appId)');
@@ -4971,15 +5140,23 @@ async function buildAuthorize(opts) {
4971
5140
  const redirectUri = opts.redirectUri || (hasDOM$1() ? window.location.origin + window.location.pathname : undefined);
4972
5141
  if (!redirectUri)
4973
5142
  throw new Error('redirectUri is required on React Native — pass the https universal link the app owner registered.');
5143
+ if (hasDOM$1()) {
5144
+ const redirect = new URL(redirectUri);
5145
+ if (redirect.origin !== window.location.origin || redirect.hash || CALLBACK_PARAMS.some((key) => redirect.searchParams.has(key))) {
5146
+ throw new Error('redirectUri must be on this app origin, without a fragment or OAuth callback parameters.');
5147
+ }
5148
+ }
4974
5149
  const { verifier, challenge } = await makePkce();
4975
- const state = randomState();
5150
+ const state = `${STATE_PREFIX}${mode}.${randomState()}`;
4976
5151
  const base = await issuerBase();
4977
5152
  const params = new URLSearchParams({
4978
5153
  client_id: cfg.appId, redirect_uri: redirectUri, response_type: 'code', scope: 'openid profile email',
4979
5154
  code_challenge: challenge, code_challenge_method: 'S256', state,
4980
5155
  });
4981
5156
  appendHostedLoginHints(params, opts);
4982
- return { url: `${base}/api/auth/oauth2/authorize?${params.toString()}`, pkce: { verifier, state, redirectUri } };
5157
+ return { url: `${base}/api/auth/oauth2/authorize?${params.toString()}`, pkce: {
5158
+ verifier, state, redirectUri, appId: cfg.appId, issuer: base, createdAt: Date.now(),
5159
+ } };
4983
5160
  }
4984
5161
  // Are we on React Native (no DOM)? OIDC dispatch uses this to choose the native
4985
5162
  // expo-web-browser driver vs. the web full-page-redirect/popup driver.
@@ -5028,8 +5205,7 @@ function parseCallbackParams(callbackUrl) {
5028
5205
  }
5029
5206
  // Full native login: open the hosted page in the system auth session, await the
5030
5207
  // universal-link callback, verify state, and run the SAME PKCE code→token exchange
5031
- // as web. Returns the signed-in User (native completes inline — there's no separate
5032
- // completeLoginFromRedirect() round-trip the way the web full-page redirect needs).
5208
+ // as web. Returns the signed-in User inline; web redirects complete on the next init().
5033
5209
  async function runNativeLogin(opts) {
5034
5210
  var _a, _b;
5035
5211
  const { url, pkce } = await buildAuthorize(opts);
@@ -5056,7 +5232,7 @@ async function runNativeLogin(opts) {
5056
5232
  if (!returnedState || returnedState !== pkce.state) {
5057
5233
  throw new Error('OAuth state mismatch — possible CSRF; aborting');
5058
5234
  }
5059
- return await exchangeCode(code, pkce.verifier, pkce.redirectUri);
5235
+ return await exchangeCode(code, pkce);
5060
5236
  }
5061
5237
  finally {
5062
5238
  setAuthLoading(false);
@@ -5065,7 +5241,7 @@ async function runNativeLogin(opts) {
5065
5241
  /** Start hosted login (Google / Apple / email / hosted phone). Works on WEB and REACT NATIVE.
5066
5242
  *
5067
5243
  * WEB: navigates the whole page to the hosted Bounded login and resolves (void) as the page
5068
- * unloads; on return to `redirectUri?code=...` you call completeLoginFromRedirect().
5244
+ * unloads; init() completes the return to redirectUri automatically.
5069
5245
  *
5070
5246
  * REACT NATIVE: opens the hosted page via expo-web-browser, awaits the https universal-link
5071
5247
  * callback, runs the PKCE exchange INLINE, and resolves with the signed-in User. No
@@ -5083,63 +5259,32 @@ async function loginWithRedirect(opts) {
5083
5259
  // is per-tab and cleared on close — appropriate for a short-lived auth handshake. Keyed by
5084
5260
  // this attempt's own state so a second concurrent login cannot overwrite it.
5085
5261
  sessionStorage.setItem(pkceKey(pkce.state), JSON.stringify(pkce));
5086
- setAuthLoading(true); // reflect "signing in…" right up to navigation; the return page's
5087
- window.location.href = url; // completeLoginFromRedirect() keeps it true until the user resolves.
5088
- }
5089
- /** Finish login on the redirect_uri page (WEB ONLY): exchange ?code= for a token (PKCE-verified),
5090
- * store the session, and return the User. Idempotent: returns null if there's no ?code= in the URL.
5091
- *
5092
- * On REACT NATIVE this returns null and is a no-op: native login completes INLINE inside
5093
- * loginWithRedirect() (which resolves with the User), so there is no separate redirect
5094
- * page to finish from. Calling it on RN is harmless. */
5095
- async function completeLoginFromRedirect() {
5096
- if (!hasDOM$1())
5097
- return null;
5098
- const url = new URL(window.location.href);
5099
- const code = url.searchParams.get('code');
5100
- const returnedState = url.searchParams.get('state');
5101
- if (!code)
5102
- return null;
5103
- // POPUP path: if we're the redirect_uri page running INSIDE the loginWithPopup() window,
5104
- // hand the code to the opener and close — no separate completeLoginInPopup() call needed.
5105
- // The opener is the same app/origin, so post to our own origin (the opener's message
5106
- // handler checks ev.origin === redirect_uri origin). One call now finishes BOTH flows.
5107
- if (window.opener && window.opener !== window) {
5108
- try {
5109
- window.opener.postMessage({ type: 'bounded-oidc-code', code, state: returnedState }, window.location.origin);
5110
- }
5111
- catch ( /* ignore */_a) { /* ignore */ }
5112
- try {
5113
- window.close();
5114
- }
5115
- catch ( /* ignore */_b) { /* ignore */ }
5116
- return null;
5117
- }
5262
+ // This promise represents dispatch, not authentication on the next page.
5263
+ // Browsers can cancel navigation (for example at beforeunload) without firing
5264
+ // pageshow. Never leave this document's SDK loading after dispatch settles.
5118
5265
  setAuthLoading(true);
5119
5266
  try {
5120
- // Read back the exact record THIS attempt wrote, keyed by the state the issuer
5121
- // returned, so a concurrent login's record is never consumed by mistake.
5122
- const raw = returnedState ? sessionStorage.getItem(pkceKey(returnedState)) : null;
5123
- if (!raw)
5124
- throw new Error('Missing PKCE state — start login with loginWithRedirect()');
5125
- const pkce = JSON.parse(raw);
5126
- if (!returnedState || returnedState !== pkce.state)
5127
- throw new Error('OAuth state mismatch — possible CSRF; aborting');
5128
- const user = await exchangeCode(code, pkce.verifier, pkce.redirectUri);
5129
- sessionStorage.removeItem(pkceKey(returnedState));
5130
- // Strip code/state from the URL so a reload doesn't re-exchange (codes are single-use anyway).
5131
- url.searchParams.delete('code');
5132
- url.searchParams.delete('state');
5133
- window.history.replaceState({}, document.title, url.pathname + url.search + url.hash);
5134
- return user;
5267
+ window.location.href = url;
5268
+ }
5269
+ catch (error) {
5270
+ removeAttempt(pkce.state);
5271
+ throw error;
5135
5272
  }
5136
5273
  finally {
5137
5274
  setAuthLoading(false);
5138
5275
  }
5139
5276
  }
5277
+ /** Optional explicit completion. Normal apps only await init(), which already finishes login.
5278
+ * Concurrent calls share one exchange. Returns null when no Bounded return is present. */
5279
+ async function completeLoginFromRedirect() {
5280
+ const result = await completePendingLogin();
5281
+ const error = getLoginReturnError();
5282
+ if (error)
5283
+ throw error;
5284
+ return result.user;
5285
+ }
5140
5286
  /** Popup variant: open the hosted login in a popup; resolves with the User once the popup's
5141
- * redirect_uri page posts the code back. The redirect_uri page just calls the usual
5142
- * completeLoginFromRedirect() (which auto-detects the popup) — no separate call needed. */
5287
+ * redirect_uri page initializes the SDK, which hands the code back automatically. */
5143
5288
  async function loginWithPopup(opts) {
5144
5289
  if (!hasDOM$1())
5145
5290
  throw new Error('loginWithPopup is web-only — on React Native use loginWithRedirect()');
@@ -5158,7 +5303,7 @@ async function loginWithPopup(opts) {
5158
5303
  throw new Error('Popup blocked — use loginWithRedirect() instead');
5159
5304
  let authorization;
5160
5305
  try {
5161
- authorization = await buildAuthorize(opts);
5306
+ authorization = await buildAuthorize(opts, 'popup');
5162
5307
  }
5163
5308
  catch (error) {
5164
5309
  try {
@@ -5193,6 +5338,7 @@ async function loginWithPopup(opts) {
5193
5338
  popup.location.href = url;
5194
5339
  }
5195
5340
  catch (_d) {
5341
+ removeAttempt(pkce.state);
5196
5342
  try {
5197
5343
  popup.close();
5198
5344
  }
@@ -5208,17 +5354,21 @@ async function loginWithPopup(opts) {
5208
5354
  let settled = false;
5209
5355
  let timer;
5210
5356
  let navigationTimer;
5357
+ let completionTimer;
5211
5358
  const cleanup = () => {
5212
5359
  settled = true;
5213
5360
  clearInterval(timer);
5214
5361
  clearTimeout(navigationTimer);
5362
+ clearTimeout(completionTimer);
5215
5363
  window.removeEventListener('message', onMsg);
5364
+ removeAttempt(pkce.state);
5216
5365
  };
5217
5366
  const onMsg = async (ev) => {
5218
- if (ev.origin !== expectedOrigin)
5219
- return; // only trust the redirect_uri origin
5367
+ if (settled || ev.origin !== expectedOrigin || ev.source !== popup)
5368
+ return;
5220
5369
  const d = ev.data;
5221
- if (!d || d.type !== 'bounded-oidc-code' || typeof d.code !== 'string')
5370
+ if (!d || d.type !== 'bounded-oidc-code' ||
5371
+ (typeof d.code !== 'string' && typeof d.error !== 'string'))
5222
5372
  return;
5223
5373
  // Validate state BEFORE tearing down this listener, against the record THIS
5224
5374
  // attempt captured in the closure (`pkce`), not a shared sessionStorage reread.
@@ -5226,10 +5376,15 @@ async function loginWithPopup(opts) {
5226
5376
  // stay registered for our own code, instead of cleaning up on another's message.
5227
5377
  if (d.state !== pkce.state)
5228
5378
  return;
5379
+ if (d.appId !== pkce.appId || d.issuer !== pkce.issuer)
5380
+ return;
5229
5381
  cleanup();
5230
5382
  try {
5231
- const user = await exchangeCode(d.code, pkce.verifier, pkce.redirectUri);
5232
- sessionStorage.removeItem(pkceKey(pkce.state));
5383
+ await validateAttempt(pkce, new URL(d.callbackUrl));
5384
+ popup.postMessage({ type: 'bounded-oidc-accepted', state: pkce.state }, expectedOrigin);
5385
+ if (d.error)
5386
+ throw new Error(typeof d.errorDescription === 'string' ? d.errorDescription : `Login failed: ${d.error}`);
5387
+ const user = await exchangeCode(d.code, pkce);
5233
5388
  try {
5234
5389
  popup.close();
5235
5390
  }
@@ -5237,10 +5392,22 @@ async function loginWithPopup(opts) {
5237
5392
  resolve(user);
5238
5393
  }
5239
5394
  catch (e) {
5395
+ try {
5396
+ popup.close();
5397
+ }
5398
+ catch ( /* ignore */_b) { /* ignore */ }
5240
5399
  reject(e);
5241
5400
  }
5242
5401
  };
5243
5402
  window.addEventListener('message', onMsg);
5403
+ completionTimer = setTimeout(() => {
5404
+ cleanup();
5405
+ try {
5406
+ popup.close();
5407
+ }
5408
+ catch ( /* ignore */_a) { /* ignore */ }
5409
+ reject(new Error('Login expired or its popup disconnected. Start sign-in again.'));
5410
+ }, LOGIN_TIMEOUT_MS);
5244
5411
  // Some browser/extension combinations can return a real popup handle but
5245
5412
  // strand the window at about:blank instead of navigating to the hosted
5246
5413
  // authorize URL. The old code then waited forever because the popup was
@@ -5298,33 +5465,20 @@ async function loginWithPopup(opts) {
5298
5465
  setAuthLoading(false);
5299
5466
  }
5300
5467
  }
5301
- /** Call this on the redirect_uri page when login was opened via loginWithPopup(): posts the
5302
- * code back to the opener and closes. (For the redirect variant use completeLoginFromRedirect.) */
5303
- function completeLoginInPopup(openerOrigin) {
5304
- if (!hasDOM$1() || !window.opener)
5305
- return;
5306
- const url = new URL(window.location.href);
5307
- const code = url.searchParams.get('code');
5308
- const state = url.searchParams.get('state');
5309
- if (!code)
5310
- return;
5311
- try {
5312
- window.opener.postMessage({ type: 'bounded-oidc-code', code, state }, openerOrigin);
5313
- }
5314
- catch ( /* ignore */_a) { /* ignore */ }
5315
- try {
5316
- window.close();
5317
- }
5318
- catch ( /* ignore */_b) { /* ignore */ }
5319
- }
5320
- async function exchangeCode(code, verifier, redirectUri) {
5321
- const cfg = await getConfig();
5322
- const base = await issuerBase();
5468
+ async function exchangeCode(code, pkce) {
5469
+ const generation = getInitializationGeneration();
5470
+ const beforeCommit = () => {
5471
+ if (generation !== getInitializationGeneration())
5472
+ throw new Error('Login was superseded by SDK reinitialization. Start sign-in again.');
5473
+ };
5474
+ await validateAttempt(pkce);
5475
+ beforeCommit();
5476
+ const base = pkce.issuer;
5323
5477
  // OIDC token endpoint: application/x-www-form-urlencoded (the standard). Public client (no
5324
5478
  // secret) authenticated by PKCE. The code is single-use + short-TTL on the server.
5325
5479
  const body = new URLSearchParams({
5326
- grant_type: 'authorization_code', code, redirect_uri: redirectUri,
5327
- client_id: cfg.appId, code_verifier: verifier,
5480
+ grant_type: 'authorization_code', code, redirect_uri: pkce.redirectUri,
5481
+ client_id: pkce.appId, code_verifier: pkce.verifier,
5328
5482
  });
5329
5483
  let res;
5330
5484
  try {
@@ -5348,10 +5502,20 @@ async function exchangeCode(code, verifier, redirectUri) {
5348
5502
  const accessToken = tokenField(data, 'access_token', 'accessToken') || idToken;
5349
5503
  const refreshToken = boundedRefreshTokenField(data);
5350
5504
  const user = userFromIdToken(idToken);
5351
- await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base);
5505
+ // Reinitialization while the request was in flight must not install this
5506
+ // app's result under a different app's session manager/configuration.
5507
+ beforeCommit();
5508
+ const rollback = await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base, beforeCommit);
5352
5509
  // Adopt on EVERY session surface (see establishInlineSession) - hosted
5353
5510
  // popup/redirect completions all land here, so this closes their gap too.
5354
- await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user });
5511
+ try {
5512
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user, beforeCommit });
5513
+ }
5514
+ catch (error) {
5515
+ if (typeof rollback === 'function')
5516
+ await rollback();
5517
+ throw error;
5518
+ }
5355
5519
  // OIDC login does establish the issuer's Better Auth browser cookie. Record
5356
5520
  // that fact separately from the provider name so logout clears the cookie
5357
5521
  // and returns through the registered app origin. Stamped AFTER the adoption
@@ -5702,7 +5866,7 @@ async function getAuthProvider(config) {
5702
5866
  // manager as legacy email sessions. Human login should be started
5703
5867
  // with loginWithRedirect(); this provider is kept as the restore and
5704
5868
  // logout surface for those sessions.
5705
- currentAuthProvider = new (await import('./email-auth-provider-CdBqgLWw.mjs')).EmailAuthProvider();
5869
+ currentAuthProvider = new (await import('./email-auth-provider-D9SQu9c1.mjs')).EmailAuthProvider();
5706
5870
  break;
5707
5871
  case "phantom": {
5708
5872
  // First-class Solana WALLET LOGIN (the poof/TaroBase-style "connect
@@ -5744,7 +5908,7 @@ async function getAuthProvider(config) {
5744
5908
  // (bounded-login-widget.ts), so registration never counts as
5745
5909
  // evidence the user has a wallet.
5746
5910
  await ensureSolanaMobileWalletRegistered(config, configuredNetwork);
5747
- const { InjectedWalletProvider } = await import('./injected-wallet-provider-D39pzPTV.mjs');
5911
+ const { InjectedWalletProvider } = await import('./injected-wallet-provider-DZOwvKgd.mjs');
5748
5912
  currentAuthProvider = new InjectedWalletProvider(resolveInjectedWalletConfig(config), rpcUrl, configuredNetwork);
5749
5913
  break;
5750
5914
  }
@@ -5757,7 +5921,7 @@ async function getAuthProvider(config) {
5757
5921
  // Selecting authMethod:'evm-wallet' IS the opt-in — pass evmWalletConfig
5758
5922
  // to point at a specific wallet or set chainId/domain.
5759
5923
  const evmWalletConfig = ((_a = config.evmWalletConfig) !== null && _a !== void 0 ? _a : {});
5760
- const { InjectedEvmWalletProvider } = await import('./injected-evm-wallet-provider-HbgMQAbf.mjs');
5924
+ const { InjectedEvmWalletProvider } = await import('./injected-evm-wallet-provider-Xb2G-ZGI.mjs');
5761
5925
  currentAuthProvider = new InjectedEvmWalletProvider(evmWalletConfig);
5762
5926
  break;
5763
5927
  }
@@ -5782,7 +5946,7 @@ async function getAuthProvider(config) {
5782
5946
  break;
5783
5947
  case "guest":
5784
5948
  // Anonymous auth: a device-local ed25519 keypair signs the challenge.
5785
- currentAuthProvider = new (await import('./guest-auth-provider-pDVr5YRg.mjs')).GuestAuthProvider();
5949
+ currentAuthProvider = new (await import('./guest-auth-provider-DsFTB844.mjs')).GuestAuthProvider();
5786
5950
  break;
5787
5951
  }
5788
5952
  if (!currentAuthProvider) {
@@ -5844,7 +6008,7 @@ async function signInAnonymously() {
5844
6008
  // Lazy-load the guest provider so its ed25519/web3 dependencies stay out of
5845
6009
  // the main bundle for data-only apps. Specifier is a static string literal
5846
6010
  // so bundle-splitting can see it.
5847
- const provider = new (await import('./guest-auth-provider-pDVr5YRg.mjs')).GuestAuthProvider();
6011
+ const provider = new (await import('./guest-auth-provider-DsFTB844.mjs')).GuestAuthProvider();
5848
6012
  setAuthLoading(true); // reflect "signing in…" while the guest keypair session is minted
5849
6013
  // adoptSessionProvider awaits only getConfig() (already resolved here) and then
5850
6014
  // runs every write - provider/method/config/marker/user - in one synchronous
@@ -6015,7 +6179,7 @@ function ensureSharedWalletLoginReady() {
6015
6179
  if (!sharedWalletReady) {
6016
6180
  sharedWalletReady = (async () => {
6017
6181
  await getConfig();
6018
- await import('./injected-wallet-provider-D39pzPTV.mjs');
6182
+ await import('./injected-wallet-provider-DZOwvKgd.mjs');
6019
6183
  })().catch((error) => {
6020
6184
  sharedWalletReady = null;
6021
6185
  throw error;
@@ -6298,6 +6462,10 @@ let authLoadingListeners = [];
6298
6462
  let initCompleted = false;
6299
6463
  let isAuthLoading = false;
6300
6464
  let authReadyPromise = null;
6465
+ let initializingConfig = null;
6466
+ let initializationGeneration = 0;
6467
+ /** Internal guard for a login result arriving after the app reconfigured the SDK. */
6468
+ function getInitializationGeneration() { return initializationGeneration; }
6301
6469
  // This file acts as a middleware for the global state of the SDK
6302
6470
  // This mostly involves setting up the AuthProvider and managing the current user
6303
6471
  async function initialize(newConfig) {
@@ -6323,6 +6491,19 @@ async function initialize(newConfig) {
6323
6491
  authProviderInstance = await getAuthProvider(newConfig);
6324
6492
  // Initialize config with auth provider
6325
6493
  await init$1(Object.assign(Object.assign({}, newConfig), { authProvider: authProviderInstance }));
6494
+ // A return establishes its own provider/session, or hands off a popup.
6495
+ // Restoring the old provider afterwards would overwrite the new login.
6496
+ try {
6497
+ if ((await completePendingLogin()).handled) {
6498
+ initCompleted = true;
6499
+ return;
6500
+ }
6501
+ }
6502
+ catch (_a) {
6503
+ // A cancelled/failed return is a login failure, not a broken SDK.
6504
+ // The completion layer keeps it in getLoginReturnError(); restore
6505
+ // any existing session and leave sign-in/readiness usable for retry.
6506
+ }
6326
6507
  // Now that core is initialized, update the auth method on the config
6327
6508
  const authMethod = getCurrentAuthMethod();
6328
6509
  if (authMethod) {
@@ -6354,7 +6535,42 @@ async function initialize(newConfig) {
6354
6535
  setAuthLoading(false);
6355
6536
  }
6356
6537
  }
6538
+ // Compare data options by value, but preserve identity for callbacks and class
6539
+ // instances such as providers. Comparing only app/endpoints would silently drop
6540
+ // a concurrent change to wallet policy or provider configuration.
6541
+ function sameConfigValue(a, b, seen = new WeakMap()) {
6542
+ var _a;
6543
+ if (Object.is(a, b))
6544
+ return true;
6545
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object')
6546
+ return false;
6547
+ if (Array.isArray(a) !== Array.isArray(b))
6548
+ return false;
6549
+ if (Array.isArray(a) && a.length !== b.length)
6550
+ return false;
6551
+ if (!Array.isArray(a)) {
6552
+ for (const value of [a, b]) {
6553
+ const prototype = Object.getPrototypeOf(value);
6554
+ if (prototype !== null && ((_a = prototype.constructor) === null || _a === void 0 ? void 0 : _a.name) !== 'Object')
6555
+ return false;
6556
+ }
6557
+ }
6558
+ if (seen.has(a))
6559
+ return seen.get(a) === b;
6560
+ seen.set(a, b);
6561
+ const left = Object.keys(a), right = Object.keys(b);
6562
+ return left.length === right.length && left.every((key) => Object.prototype.hasOwnProperty.call(b, key)
6563
+ && sameConfigValue(a[key], b[key], seen));
6564
+ }
6357
6565
  function init(newConfig) {
6566
+ if (initializingConfig) {
6567
+ if (sameConfigValue(initializingConfig, newConfig)) {
6568
+ return authReadyPromise;
6569
+ }
6570
+ return Promise.reject(new Error('SDK initialization is in progress. Await init() before changing its configuration.'));
6571
+ }
6572
+ initializingConfig = Object.assign({}, newConfig);
6573
+ initializationGeneration++;
6358
6574
  let resolveReady;
6359
6575
  let rejectReady;
6360
6576
  const ready = new Promise((resolve, reject) => {
@@ -6365,7 +6581,7 @@ function init(newConfig) {
6365
6581
  // auth-loading listener to call waitForAuthReady() synchronously when init()
6366
6582
  // announces that session restoration has begun.
6367
6583
  authReadyPromise = ready;
6368
- void initialize(newConfig).then(resolveReady, rejectReady);
6584
+ void initialize(initializingConfig).then(() => { initializingConfig = null; resolveReady(); }, (error) => { initializingConfig = null; rejectReady(error); });
6369
6585
  return ready;
6370
6586
  }
6371
6587
  /**
@@ -8343,5 +8559,5 @@ class PrivyExpoProvider {
8343
8559
  }
8344
8560
  }
8345
8561
 
8346
- export { BUILD_REVIEW_CAPABILITY_SCHEMA as $, logout as A, getAuthProvider as B, signInAnonymously as C, loginWithPrivy as D, loginWithRedirect as E, completeLoginFromRedirect as F, loginWithPopup as G, completeLoginInPopup as H, useAuth as I, useQuery as J, useBuildRun as K, apps as L, getIdToken as M, onramp as N, onrampQuote as O, onrampStatus as P, setPlatform as Q, OffchainAuthProvider as R, PrivyExpoProvider as S, builds as T, BuildsApiError as U, mediaAgentTools as V, NON_TERMINAL_STATES as W, TERMINAL_STATES as X, RUN_STATES as Y, PARK_REASONS as Z, FAILURE_REASONS as _, signMessageWithTurnkey as a, BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA as a0, BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA as a1, signTransactionWithTurnkey as b, signTransactionV1WithTurnkey as c, signAndSubmitTransactionWithTurnkey as d, bs58 as e, getWallets as f, getPlatform as g, hasTurnkeyWallet as h, isSolanaMobileWallet as i, solanaMobileWalletLeavesPage as j, setAuthLoading as k, bufferExports as l, setStoredAuthMethod as m, normalizeSolanaRpcUrl as n, confirmAndCheckTransaction as o, resolveSolanaRpcUrl as p, init as q, resolveSolanaWalletStandardChain as r, setCurrentUser as s, transactionHasSignature as t, getCurrentUser as u, onAuthStateChanged as v, onAuthLoadingChanged as w, getAuthLoading as x, waitForAuthReady as y, login as z };
8347
- //# sourceMappingURL=index.native-Bl2W-Wsi.mjs.map
8562
+ export { BUILD_REVIEW_CAPABILITY_SCHEMA as $, logout as A, getAuthProvider as B, signInAnonymously as C, loginWithPrivy as D, loginWithRedirect as E, completeLoginFromRedirect as F, getLoginReturnError as G, loginWithPopup as H, useAuth as I, useQuery as J, useBuildRun as K, apps as L, getIdToken as M, onramp as N, onrampQuote as O, onrampStatus as P, setPlatform as Q, OffchainAuthProvider as R, PrivyExpoProvider as S, builds as T, BuildsApiError as U, mediaAgentTools as V, NON_TERMINAL_STATES as W, TERMINAL_STATES as X, RUN_STATES as Y, PARK_REASONS as Z, FAILURE_REASONS as _, signMessageWithTurnkey as a, BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA as a0, BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA as a1, signTransactionWithTurnkey as b, signTransactionV1WithTurnkey as c, signAndSubmitTransactionWithTurnkey as d, bs58 as e, getWallets as f, getPlatform as g, hasTurnkeyWallet as h, isSolanaMobileWallet as i, solanaMobileWalletLeavesPage as j, setAuthLoading as k, bufferExports as l, setStoredAuthMethod as m, normalizeSolanaRpcUrl as n, confirmAndCheckTransaction as o, resolveSolanaRpcUrl as p, init as q, resolveSolanaWalletStandardChain as r, setCurrentUser as s, transactionHasSignature as t, getCurrentUser as u, onAuthStateChanged as v, onAuthLoadingChanged as w, getAuthLoading as x, waitForAuthReady as y, login as z };
8563
+ //# sourceMappingURL=index.native-BSOqaKIz.mjs.map