@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
@@ -211,6 +211,36 @@ function setPlatform(adapter) {
211
211
  function getPlatform() {
212
212
  return _platform;
213
213
  }
214
+ // ---------------------------------------------------------------------------
215
+ // Mobile / Android detection helpers
216
+ // ---------------------------------------------------------------------------
217
+ /**
218
+ * Detect mobile device even when Chrome "Request Desktop Site" is active.
219
+ *
220
+ * Desktop mode on Android strips "Android" and "Mobile" from the UA, leaving
221
+ * something like "Linux x86_64 …". The fallback detects this by looking for
222
+ * touch capability + small viewport + a UA that doesn't belong to a known
223
+ * desktop OS (ChromeOS, Windows, macOS).
224
+ */
225
+ function detectMobile() {
226
+ const ua = _platform.getUserAgent();
227
+ if (/Android|iPhone|iPad|iPod/i.test(ua))
228
+ return true;
229
+ if (typeof navigator !== 'undefined' && typeof window !== 'undefined') {
230
+ const hasTouch = navigator.maxTouchPoints > 0;
231
+ // Macs don't have touch screens, so "Macintosh" + touch = iOS desktop mode
232
+ // (also catches iPadOS 13+ which always reports as Macintosh)
233
+ if (/Macintosh/i.test(ua) && hasTouch)
234
+ return true;
235
+ // Android desktop mode: Linux UA + touch + small viewport, excluding
236
+ // known desktop OSes (ChromeOS, Windows, macOS)
237
+ const isSmallViewport = window.innerWidth <= 1024;
238
+ const isDesktopOS = /CrOS|Windows|Macintosh/i.test(ua);
239
+ if (hasTouch && isSmallViewport && !isDesktopOS)
240
+ return true;
241
+ }
242
+ return false;
243
+ }
214
244
 
215
245
  /**
216
246
  * OffchainAuthProvider wraps a real auth provider (Turnkey, Phantom, ...) for the
@@ -5037,9 +5067,18 @@ const OIDC_NO_WALLET = 'This session has no Turnkey wallet. Sign in again to pro
5037
5067
  // same tab (redirect + popup, or two popups) each own a distinct record, so a second
5038
5068
  // attempt cannot clobber the first's verifier/state/redirectUri.
5039
5069
  const PKCE_STORE_PREFIX = 'bounded.oidc.pkce';
5070
+ const STATE_PREFIX = 'bounded.';
5071
+ const LOGIN_TIMEOUT_MS = 10 * 60000;
5072
+ const CALLBACK_PARAMS = ['code', 'state', 'error', 'error_description', 'iss'];
5040
5073
  function pkceKey(state) {
5041
5074
  return `${PKCE_STORE_PREFIX}.${state}`;
5042
5075
  }
5076
+ function removeAttempt(state) {
5077
+ try {
5078
+ sessionStorage.removeItem(pkceKey(state));
5079
+ }
5080
+ catch ( /* Storage may have been disabled mid-login. */_a) { /* Storage may have been disabled mid-login. */ }
5081
+ }
5043
5082
  const ISSUER_SESSION_KIND_KEY = 'bounded_issuer_session_kind';
5044
5083
  // Active session store (Web on web, RN on React Native) — selected by config,
5045
5084
  // not by `typeof window` (truthy on RN). OIDC redirect/popup is web-only, but the
@@ -5148,6 +5187,165 @@ async function makePkce() {
5148
5187
  function randomState() {
5149
5188
  return b64url(getPlatform().getRandomBytes(16));
5150
5189
  }
5190
+ let pendingCompletion;
5191
+ let loginReturnError;
5192
+ /** The last OAuth return failure in this initialization, or null. Initialization
5193
+ * remains usable so the UI can display this error and offer a fresh sign-in. */
5194
+ function getLoginReturnError() {
5195
+ return (loginReturnError === null || loginReturnError === void 0 ? void 0 : loginReturnError.generation) === getInitializationGeneration() ? loginReturnError.error : null;
5196
+ }
5197
+ // A hosted page can contain both an app bundle and the platform widget's SDK.
5198
+ // They share one code exchange, while adopting the session in their own registries.
5199
+ const COMPLETIONS = Symbol.for('bounded.oidc.completions');
5200
+ function cleanCallbackUrl(url) {
5201
+ const clean = new URL(url);
5202
+ for (const key of CALLBACK_PARAMS)
5203
+ clean.searchParams.delete(key);
5204
+ return clean;
5205
+ }
5206
+ async function validateAttempt(pkce, callback) {
5207
+ const cfg = await core.getConfig();
5208
+ if (!pkce || typeof pkce.verifier !== 'string' || !pkce.verifier ||
5209
+ pkce.appId !== cfg.appId || pkce.issuer !== await issuerBase()) {
5210
+ throw new Error('Login belongs to a different app or auth server. Start sign-in again.');
5211
+ }
5212
+ if (!Number.isFinite(pkce.createdAt) || Date.now() - pkce.createdAt > LOGIN_TIMEOUT_MS || pkce.createdAt > Date.now()) {
5213
+ throw new Error('Login expired. Start sign-in again.');
5214
+ }
5215
+ if (callback && (cleanCallbackUrl(callback).href !== cleanCallbackUrl(new URL(pkce.redirectUri)).href ||
5216
+ (callback.searchParams.has('iss') && callback.searchParams.get('iss') !== pkce.issuer))) {
5217
+ throw new Error('Login returned to a different address or from a different auth server. Start sign-in again.');
5218
+ }
5219
+ }
5220
+ // The receiver acknowledges only after checking origin, window and state. An
5221
+ // absent/disconnected opener must never leave a popup claiming success.
5222
+ async function handOffPopup(url, state) {
5223
+ const cfg = await core.getConfig();
5224
+ const issuer = await issuerBase();
5225
+ const opener = window.opener;
5226
+ if (!opener || opener === window || opener.closed) {
5227
+ throw new Error('The original sign-in window is unavailable. Close this window and sign in again in the original tab.');
5228
+ }
5229
+ return new Promise((resolve, reject) => {
5230
+ const cleanup = () => { clearTimeout(timer); window.removeEventListener('message', onMessage); };
5231
+ const onMessage = (event) => {
5232
+ var _a;
5233
+ if (event.source !== opener || event.origin !== url.origin ||
5234
+ ((_a = event.data) === null || _a === void 0 ? void 0 : _a.type) !== 'bounded-oidc-accepted' || event.data.state !== state)
5235
+ return;
5236
+ cleanup();
5237
+ try {
5238
+ window.close();
5239
+ }
5240
+ catch ( /* The opener also closes its own popup handle. */_b) { /* The opener also closes its own popup handle. */ }
5241
+ resolve();
5242
+ };
5243
+ const timer = setTimeout(() => {
5244
+ cleanup();
5245
+ reject(new Error('Could not reach the original sign-in tab. Close this window and start sign-in again.'));
5246
+ }, 10000);
5247
+ window.addEventListener('message', onMessage);
5248
+ try {
5249
+ opener.postMessage({ type: 'bounded-oidc-code', state, code: url.searchParams.get('code'),
5250
+ error: url.searchParams.get('error'), errorDescription: url.searchParams.get('error_description'),
5251
+ callbackUrl: url.href, appId: cfg.appId, issuer }, url.origin);
5252
+ }
5253
+ catch (error) {
5254
+ cleanup();
5255
+ reject(error);
5256
+ }
5257
+ });
5258
+ }
5259
+ async function finishLoginReturn(url, state) {
5260
+ try {
5261
+ if (!/^bounded\.(popup|redirect)\.[A-Za-z0-9_-]{22}$/.test(state)) {
5262
+ throw new Error('Invalid login state. Start sign-in again.');
5263
+ }
5264
+ if (state.startsWith('bounded.popup.')) {
5265
+ await handOffPopup(url, state);
5266
+ return { handled: true, user: null };
5267
+ }
5268
+ const raw = sessionStorage.getItem(pkceKey(state));
5269
+ if (!raw)
5270
+ throw new Error('Missing login attempt. Start sign-in again in this tab.');
5271
+ const pkce = JSON.parse(raw);
5272
+ if (pkce.state !== state)
5273
+ throw new Error('OAuth state mismatch. Start sign-in again.');
5274
+ await validateAttempt(pkce, url);
5275
+ const error = url.searchParams.get('error');
5276
+ if (error)
5277
+ throw new Error(url.searchParams.get('error_description') || `Login failed: ${error}`);
5278
+ const user = await exchangeCode(url.searchParams.get('code'), pkce);
5279
+ return { handled: true, user, idToken: sessionManager().getIdToken() };
5280
+ }
5281
+ finally {
5282
+ removeAttempt(state);
5283
+ const clean = cleanCallbackUrl(url);
5284
+ if (window.location.href === url.href) {
5285
+ window.history.replaceState(window.history.state, document.title, clean.pathname + clean.search + clean.hash);
5286
+ }
5287
+ }
5288
+ }
5289
+ /** Internal initialization step. Unrelated OAuth callbacks are left untouched. */
5290
+ function completePendingLogin() {
5291
+ if (!hasDOM$2())
5292
+ return Promise.resolve({ handled: false, user: null });
5293
+ const url = new URL(window.location.href);
5294
+ const state = url.searchParams.get('state');
5295
+ if (!(state === null || state === void 0 ? void 0 : state.startsWith(STATE_PREFIX)) || (!url.searchParams.get('code') && !url.searchParams.get('error'))) {
5296
+ return Promise.resolve({ handled: false, user: null });
5297
+ }
5298
+ const generation = getInitializationGeneration();
5299
+ if ((pendingCompletion === null || pendingCompletion === void 0 ? void 0 : pendingCompletion.href) === url.href && pendingCompletion.generation === generation)
5300
+ return pendingCompletion.promise;
5301
+ const promise = finishSharedLoginReturn(url, state).catch((error) => {
5302
+ loginReturnError = { generation, error: error instanceof Error ? error : new Error(String(error)) };
5303
+ throw loginReturnError.error;
5304
+ }).finally(() => {
5305
+ if ((pendingCompletion === null || pendingCompletion === void 0 ? void 0 : pendingCompletion.promise) === promise)
5306
+ pendingCompletion = undefined;
5307
+ });
5308
+ pendingCompletion = { href: url.href, generation, promise };
5309
+ return promise;
5310
+ }
5311
+ async function finishSharedLoginReturn(url, state) {
5312
+ var _a;
5313
+ var _b;
5314
+ const cfg = await core.getConfig();
5315
+ const issuer = await issuerBase();
5316
+ if (state.startsWith('bounded.redirect.')) {
5317
+ const raw = sessionStorage.getItem(pkceKey(state));
5318
+ // A second SDK configured for another app/issuer on this page does not own
5319
+ // this return. Leave its URL and proof available to the initiating app.
5320
+ try {
5321
+ const attempt = raw && JSON.parse(raw);
5322
+ if (attempt && (attempt.appId !== cfg.appId || attempt.issuer !== issuer))
5323
+ return { handled: false, user: null };
5324
+ }
5325
+ catch ( /* The owning completion reports malformed attempt data. */_c) { /* The owning completion reports malformed attempt data. */ }
5326
+ }
5327
+ const shared = (_a = (_b = window)[COMPLETIONS]) !== null && _a !== void 0 ? _a : (_b[COMPLETIONS] = new Map());
5328
+ const key = JSON.stringify([url.href, cfg.appId, issuer]);
5329
+ const existing = shared.get(key);
5330
+ if (!existing) {
5331
+ const completion = finishLoginReturn(url, state);
5332
+ shared.set(key, completion);
5333
+ return completion;
5334
+ }
5335
+ const generation = getInitializationGeneration();
5336
+ const result = await existing;
5337
+ if (result.user) {
5338
+ if (!result.idToken || sessionManager().getIdToken() !== result.idToken) {
5339
+ throw new Error('The completed login session has changed. Start sign-in again.');
5340
+ }
5341
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc',
5342
+ user: Object.assign(Object.assign({}, result.user), { provider: oidcProviderSingleton }), beforeCommit: () => {
5343
+ if (generation !== getInitializationGeneration())
5344
+ throw new Error('Login was superseded by SDK reinitialization. Start sign-in again.');
5345
+ } });
5346
+ }
5347
+ return result;
5348
+ }
5151
5349
  function normalizeMethod(value) {
5152
5350
  const normalized = String(value || '').trim().toLowerCase();
5153
5351
  return /^[a-z0-9_-]{1,40}$/.test(normalized) ? normalized : null;
@@ -5190,7 +5388,8 @@ function appendHostedLoginHints(params, opts) {
5190
5388
  // native (expo-web-browser) path. The caller decides where the PkceState lives
5191
5389
  // (web → sessionStorage across the full-page redirect; native → in-memory across
5192
5390
  // the awaited browser session).
5193
- async function buildAuthorize(opts) {
5391
+ async function buildAuthorize(opts, mode = 'redirect') {
5392
+ loginReturnError = undefined;
5194
5393
  const cfg = await core.getConfig();
5195
5394
  if (!cfg.appId)
5196
5395
  throw new Error('appId is not configured (init the SDK with your appId)');
@@ -5199,15 +5398,23 @@ async function buildAuthorize(opts) {
5199
5398
  const redirectUri = opts.redirectUri || (hasDOM$2() ? window.location.origin + window.location.pathname : undefined);
5200
5399
  if (!redirectUri)
5201
5400
  throw new Error('redirectUri is required on React Native — pass the https universal link the app owner registered.');
5401
+ if (hasDOM$2()) {
5402
+ const redirect = new URL(redirectUri);
5403
+ if (redirect.origin !== window.location.origin || redirect.hash || CALLBACK_PARAMS.some((key) => redirect.searchParams.has(key))) {
5404
+ throw new Error('redirectUri must be on this app origin, without a fragment or OAuth callback parameters.');
5405
+ }
5406
+ }
5202
5407
  const { verifier, challenge } = await makePkce();
5203
- const state = randomState();
5408
+ const state = `${STATE_PREFIX}${mode}.${randomState()}`;
5204
5409
  const base = await issuerBase();
5205
5410
  const params = new URLSearchParams({
5206
5411
  client_id: cfg.appId, redirect_uri: redirectUri, response_type: 'code', scope: 'openid profile email',
5207
5412
  code_challenge: challenge, code_challenge_method: 'S256', state,
5208
5413
  });
5209
5414
  appendHostedLoginHints(params, opts);
5210
- return { url: `${base}/api/auth/oauth2/authorize?${params.toString()}`, pkce: { verifier, state, redirectUri } };
5415
+ return { url: `${base}/api/auth/oauth2/authorize?${params.toString()}`, pkce: {
5416
+ verifier, state, redirectUri, appId: cfg.appId, issuer: base, createdAt: Date.now(),
5417
+ } };
5211
5418
  }
5212
5419
  // Are we on React Native (no DOM)? OIDC dispatch uses this to choose the native
5213
5420
  // expo-web-browser driver vs. the web full-page-redirect/popup driver.
@@ -5256,8 +5463,7 @@ function parseCallbackParams(callbackUrl) {
5256
5463
  }
5257
5464
  // Full native login: open the hosted page in the system auth session, await the
5258
5465
  // universal-link callback, verify state, and run the SAME PKCE code→token exchange
5259
- // as web. Returns the signed-in User (native completes inline — there's no separate
5260
- // completeLoginFromRedirect() round-trip the way the web full-page redirect needs).
5466
+ // as web. Returns the signed-in User inline; web redirects complete on the next init().
5261
5467
  async function runNativeLogin(opts) {
5262
5468
  var _a, _b;
5263
5469
  const { url, pkce } = await buildAuthorize(opts);
@@ -5284,7 +5490,7 @@ async function runNativeLogin(opts) {
5284
5490
  if (!returnedState || returnedState !== pkce.state) {
5285
5491
  throw new Error('OAuth state mismatch — possible CSRF; aborting');
5286
5492
  }
5287
- return await exchangeCode(code, pkce.verifier, pkce.redirectUri);
5493
+ return await exchangeCode(code, pkce);
5288
5494
  }
5289
5495
  finally {
5290
5496
  setAuthLoading(false);
@@ -5293,7 +5499,7 @@ async function runNativeLogin(opts) {
5293
5499
  /** Start hosted login (Google / Apple / email / hosted phone). Works on WEB and REACT NATIVE.
5294
5500
  *
5295
5501
  * WEB: navigates the whole page to the hosted Bounded login and resolves (void) as the page
5296
- * unloads; on return to `redirectUri?code=...` you call completeLoginFromRedirect().
5502
+ * unloads; init() completes the return to redirectUri automatically.
5297
5503
  *
5298
5504
  * REACT NATIVE: opens the hosted page via expo-web-browser, awaits the https universal-link
5299
5505
  * callback, runs the PKCE exchange INLINE, and resolves with the signed-in User. No
@@ -5311,63 +5517,32 @@ async function loginWithRedirect(opts) {
5311
5517
  // is per-tab and cleared on close — appropriate for a short-lived auth handshake. Keyed by
5312
5518
  // this attempt's own state so a second concurrent login cannot overwrite it.
5313
5519
  sessionStorage.setItem(pkceKey(pkce.state), JSON.stringify(pkce));
5314
- setAuthLoading(true); // reflect "signing in…" right up to navigation; the return page's
5315
- window.location.href = url; // completeLoginFromRedirect() keeps it true until the user resolves.
5316
- }
5317
- /** Finish login on the redirect_uri page (WEB ONLY): exchange ?code= for a token (PKCE-verified),
5318
- * store the session, and return the User. Idempotent: returns null if there's no ?code= in the URL.
5319
- *
5320
- * On REACT NATIVE this returns null and is a no-op: native login completes INLINE inside
5321
- * loginWithRedirect() (which resolves with the User), so there is no separate redirect
5322
- * page to finish from. Calling it on RN is harmless. */
5323
- async function completeLoginFromRedirect() {
5324
- if (!hasDOM$2())
5325
- return null;
5326
- const url = new URL(window.location.href);
5327
- const code = url.searchParams.get('code');
5328
- const returnedState = url.searchParams.get('state');
5329
- if (!code)
5330
- return null;
5331
- // POPUP path: if we're the redirect_uri page running INSIDE the loginWithPopup() window,
5332
- // hand the code to the opener and close — no separate completeLoginInPopup() call needed.
5333
- // The opener is the same app/origin, so post to our own origin (the opener's message
5334
- // handler checks ev.origin === redirect_uri origin). One call now finishes BOTH flows.
5335
- if (window.opener && window.opener !== window) {
5336
- try {
5337
- window.opener.postMessage({ type: 'bounded-oidc-code', code, state: returnedState }, window.location.origin);
5338
- }
5339
- catch ( /* ignore */_a) { /* ignore */ }
5340
- try {
5341
- window.close();
5342
- }
5343
- catch ( /* ignore */_b) { /* ignore */ }
5344
- return null;
5345
- }
5520
+ // This promise represents dispatch, not authentication on the next page.
5521
+ // Browsers can cancel navigation (for example at beforeunload) without firing
5522
+ // pageshow. Never leave this document's SDK loading after dispatch settles.
5346
5523
  setAuthLoading(true);
5347
5524
  try {
5348
- // Read back the exact record THIS attempt wrote, keyed by the state the issuer
5349
- // returned, so a concurrent login's record is never consumed by mistake.
5350
- const raw = returnedState ? sessionStorage.getItem(pkceKey(returnedState)) : null;
5351
- if (!raw)
5352
- throw new Error('Missing PKCE state — start login with loginWithRedirect()');
5353
- const pkce = JSON.parse(raw);
5354
- if (!returnedState || returnedState !== pkce.state)
5355
- throw new Error('OAuth state mismatch — possible CSRF; aborting');
5356
- const user = await exchangeCode(code, pkce.verifier, pkce.redirectUri);
5357
- sessionStorage.removeItem(pkceKey(returnedState));
5358
- // Strip code/state from the URL so a reload doesn't re-exchange (codes are single-use anyway).
5359
- url.searchParams.delete('code');
5360
- url.searchParams.delete('state');
5361
- window.history.replaceState({}, document.title, url.pathname + url.search + url.hash);
5362
- return user;
5525
+ window.location.href = url;
5526
+ }
5527
+ catch (error) {
5528
+ removeAttempt(pkce.state);
5529
+ throw error;
5363
5530
  }
5364
5531
  finally {
5365
5532
  setAuthLoading(false);
5366
5533
  }
5367
5534
  }
5535
+ /** Optional explicit completion. Normal apps only await init(), which already finishes login.
5536
+ * Concurrent calls share one exchange. Returns null when no Bounded return is present. */
5537
+ async function completeLoginFromRedirect() {
5538
+ const result = await completePendingLogin();
5539
+ const error = getLoginReturnError();
5540
+ if (error)
5541
+ throw error;
5542
+ return result.user;
5543
+ }
5368
5544
  /** Popup variant: open the hosted login in a popup; resolves with the User once the popup's
5369
- * redirect_uri page posts the code back. The redirect_uri page just calls the usual
5370
- * completeLoginFromRedirect() (which auto-detects the popup) — no separate call needed. */
5545
+ * redirect_uri page initializes the SDK, which hands the code back automatically. */
5371
5546
  async function loginWithPopup(opts) {
5372
5547
  if (!hasDOM$2())
5373
5548
  throw new Error('loginWithPopup is web-only — on React Native use loginWithRedirect()');
@@ -5386,7 +5561,7 @@ async function loginWithPopup(opts) {
5386
5561
  throw new Error('Popup blocked — use loginWithRedirect() instead');
5387
5562
  let authorization;
5388
5563
  try {
5389
- authorization = await buildAuthorize(opts);
5564
+ authorization = await buildAuthorize(opts, 'popup');
5390
5565
  }
5391
5566
  catch (error) {
5392
5567
  try {
@@ -5421,6 +5596,7 @@ async function loginWithPopup(opts) {
5421
5596
  popup.location.href = url;
5422
5597
  }
5423
5598
  catch (_d) {
5599
+ removeAttempt(pkce.state);
5424
5600
  try {
5425
5601
  popup.close();
5426
5602
  }
@@ -5436,17 +5612,21 @@ async function loginWithPopup(opts) {
5436
5612
  let settled = false;
5437
5613
  let timer;
5438
5614
  let navigationTimer;
5615
+ let completionTimer;
5439
5616
  const cleanup = () => {
5440
5617
  settled = true;
5441
5618
  clearInterval(timer);
5442
5619
  clearTimeout(navigationTimer);
5620
+ clearTimeout(completionTimer);
5443
5621
  window.removeEventListener('message', onMsg);
5622
+ removeAttempt(pkce.state);
5444
5623
  };
5445
5624
  const onMsg = async (ev) => {
5446
- if (ev.origin !== expectedOrigin)
5447
- return; // only trust the redirect_uri origin
5625
+ if (settled || ev.origin !== expectedOrigin || ev.source !== popup)
5626
+ return;
5448
5627
  const d = ev.data;
5449
- if (!d || d.type !== 'bounded-oidc-code' || typeof d.code !== 'string')
5628
+ if (!d || d.type !== 'bounded-oidc-code' ||
5629
+ (typeof d.code !== 'string' && typeof d.error !== 'string'))
5450
5630
  return;
5451
5631
  // Validate state BEFORE tearing down this listener, against the record THIS
5452
5632
  // attempt captured in the closure (`pkce`), not a shared sessionStorage reread.
@@ -5454,10 +5634,15 @@ async function loginWithPopup(opts) {
5454
5634
  // stay registered for our own code, instead of cleaning up on another's message.
5455
5635
  if (d.state !== pkce.state)
5456
5636
  return;
5637
+ if (d.appId !== pkce.appId || d.issuer !== pkce.issuer)
5638
+ return;
5457
5639
  cleanup();
5458
5640
  try {
5459
- const user = await exchangeCode(d.code, pkce.verifier, pkce.redirectUri);
5460
- sessionStorage.removeItem(pkceKey(pkce.state));
5641
+ await validateAttempt(pkce, new URL(d.callbackUrl));
5642
+ popup.postMessage({ type: 'bounded-oidc-accepted', state: pkce.state }, expectedOrigin);
5643
+ if (d.error)
5644
+ throw new Error(typeof d.errorDescription === 'string' ? d.errorDescription : `Login failed: ${d.error}`);
5645
+ const user = await exchangeCode(d.code, pkce);
5461
5646
  try {
5462
5647
  popup.close();
5463
5648
  }
@@ -5465,10 +5650,22 @@ async function loginWithPopup(opts) {
5465
5650
  resolve(user);
5466
5651
  }
5467
5652
  catch (e) {
5653
+ try {
5654
+ popup.close();
5655
+ }
5656
+ catch ( /* ignore */_b) { /* ignore */ }
5468
5657
  reject(e);
5469
5658
  }
5470
5659
  };
5471
5660
  window.addEventListener('message', onMsg);
5661
+ completionTimer = setTimeout(() => {
5662
+ cleanup();
5663
+ try {
5664
+ popup.close();
5665
+ }
5666
+ catch ( /* ignore */_a) { /* ignore */ }
5667
+ reject(new Error('Login expired or its popup disconnected. Start sign-in again.'));
5668
+ }, LOGIN_TIMEOUT_MS);
5472
5669
  // Some browser/extension combinations can return a real popup handle but
5473
5670
  // strand the window at about:blank instead of navigating to the hosted
5474
5671
  // authorize URL. The old code then waited forever because the popup was
@@ -5526,25 +5723,6 @@ async function loginWithPopup(opts) {
5526
5723
  setAuthLoading(false);
5527
5724
  }
5528
5725
  }
5529
- /** Call this on the redirect_uri page when login was opened via loginWithPopup(): posts the
5530
- * code back to the opener and closes. (For the redirect variant use completeLoginFromRedirect.) */
5531
- function completeLoginInPopup(openerOrigin) {
5532
- if (!hasDOM$2() || !window.opener)
5533
- return;
5534
- const url = new URL(window.location.href);
5535
- const code = url.searchParams.get('code');
5536
- const state = url.searchParams.get('state');
5537
- if (!code)
5538
- return;
5539
- try {
5540
- window.opener.postMessage({ type: 'bounded-oidc-code', code, state }, openerOrigin);
5541
- }
5542
- catch ( /* ignore */_a) { /* ignore */ }
5543
- try {
5544
- window.close();
5545
- }
5546
- catch ( /* ignore */_b) { /* ignore */ }
5547
- }
5548
5726
  // Establish a Bounded session from a server-minted inline-session response (the same
5549
5727
  // { idToken, accessToken, refreshToken } shape as /session/refresh). Used by the
5550
5728
  // Turnkey-native login path (authMode:"turnkey"), which mints the session server-side
@@ -5587,14 +5765,20 @@ async function inlineAuthEndpoint() {
5587
5765
  const cfg = await core.getConfig();
5588
5766
  return { base: await issuerBase(), appId: cfg.appId };
5589
5767
  }
5590
- async function exchangeCode(code, verifier, redirectUri) {
5591
- const cfg = await core.getConfig();
5592
- const base = await issuerBase();
5768
+ async function exchangeCode(code, pkce) {
5769
+ const generation = getInitializationGeneration();
5770
+ const beforeCommit = () => {
5771
+ if (generation !== getInitializationGeneration())
5772
+ throw new Error('Login was superseded by SDK reinitialization. Start sign-in again.');
5773
+ };
5774
+ await validateAttempt(pkce);
5775
+ beforeCommit();
5776
+ const base = pkce.issuer;
5593
5777
  // OIDC token endpoint: application/x-www-form-urlencoded (the standard). Public client (no
5594
5778
  // secret) authenticated by PKCE. The code is single-use + short-TTL on the server.
5595
5779
  const body = new URLSearchParams({
5596
- grant_type: 'authorization_code', code, redirect_uri: redirectUri,
5597
- client_id: cfg.appId, code_verifier: verifier,
5780
+ grant_type: 'authorization_code', code, redirect_uri: pkce.redirectUri,
5781
+ client_id: pkce.appId, code_verifier: pkce.verifier,
5598
5782
  });
5599
5783
  let res;
5600
5784
  try {
@@ -5618,10 +5802,20 @@ async function exchangeCode(code, verifier, redirectUri) {
5618
5802
  const accessToken = tokenField(data, 'access_token', 'accessToken') || idToken;
5619
5803
  const refreshToken = boundedRefreshTokenField(data);
5620
5804
  const user = userFromIdToken(idToken);
5621
- await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base);
5805
+ // Reinitialization while the request was in flight must not install this
5806
+ // app's result under a different app's session manager/configuration.
5807
+ beforeCommit();
5808
+ const rollback = await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base, beforeCommit);
5622
5809
  // Adopt on EVERY session surface (see establishInlineSession) - hosted
5623
5810
  // popup/redirect completions all land here, so this closes their gap too.
5624
- await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user });
5811
+ try {
5812
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user, beforeCommit });
5813
+ }
5814
+ catch (error) {
5815
+ if (typeof rollback === 'function')
5816
+ await rollback();
5817
+ throw error;
5818
+ }
5625
5819
  // OIDC login does establish the issuer's Better Auth browser cookie. Record
5626
5820
  // that fact separately from the provider name so logout clears the cookie
5627
5821
  // and returns through the registered app origin. Stamped AFTER the adoption
@@ -5987,7 +6181,7 @@ async function getAuthProvider(config) {
5987
6181
  // manager as legacy email sessions. Human login should be started
5988
6182
  // with loginWithRedirect(); this provider is kept as the restore and
5989
6183
  // logout surface for those sessions.
5990
- currentAuthProvider = new (await Promise.resolve().then(function () { return require('./email-auth-provider-aMa0EDuc.js'); })).EmailAuthProvider();
6184
+ currentAuthProvider = new (await Promise.resolve().then(function () { return require('./email-auth-provider-C_0SoVG4.js'); })).EmailAuthProvider();
5991
6185
  break;
5992
6186
  case "phantom": {
5993
6187
  // First-class Solana WALLET LOGIN (the poof/TaroBase-style "connect
@@ -6029,7 +6223,7 @@ async function getAuthProvider(config) {
6029
6223
  // (bounded-login-widget.ts), so registration never counts as
6030
6224
  // evidence the user has a wallet.
6031
6225
  await ensureSolanaMobileWalletRegistered(config, configuredNetwork);
6032
- const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-oWtACL_7.js'); });
6226
+ const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-CEtgcsQl.js'); });
6033
6227
  currentAuthProvider = new InjectedWalletProvider(resolveInjectedWalletConfig(config), rpcUrl, configuredNetwork);
6034
6228
  break;
6035
6229
  }
@@ -6042,7 +6236,7 @@ async function getAuthProvider(config) {
6042
6236
  // Selecting authMethod:'evm-wallet' IS the opt-in — pass evmWalletConfig
6043
6237
  // to point at a specific wallet or set chainId/domain.
6044
6238
  const evmWalletConfig = ((_a = config.evmWalletConfig) !== null && _a !== void 0 ? _a : {});
6045
- const { InjectedEvmWalletProvider } = await Promise.resolve().then(function () { return require('./injected-evm-wallet-provider-B8PWHQdb.js'); });
6239
+ const { InjectedEvmWalletProvider } = await Promise.resolve().then(function () { return require('./injected-evm-wallet-provider-C3Z2c4Yl.js'); });
6046
6240
  currentAuthProvider = new InjectedEvmWalletProvider(evmWalletConfig);
6047
6241
  break;
6048
6242
  }
@@ -6067,7 +6261,7 @@ async function getAuthProvider(config) {
6067
6261
  break;
6068
6262
  case "guest":
6069
6263
  // Anonymous auth: a device-local ed25519 keypair signs the challenge.
6070
- currentAuthProvider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-Ca50pehK.js'); })).GuestAuthProvider();
6264
+ currentAuthProvider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-DPZRPch_.js'); })).GuestAuthProvider();
6071
6265
  break;
6072
6266
  }
6073
6267
  if (!currentAuthProvider) {
@@ -6129,7 +6323,7 @@ async function signInAnonymously() {
6129
6323
  // Lazy-load the guest provider so its ed25519/web3 dependencies stay out of
6130
6324
  // the main bundle for data-only apps. Specifier is a static string literal
6131
6325
  // so bundle-splitting can see it.
6132
- const provider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-Ca50pehK.js'); })).GuestAuthProvider();
6326
+ const provider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-DPZRPch_.js'); })).GuestAuthProvider();
6133
6327
  setAuthLoading(true); // reflect "signing in…" while the guest keypair session is minted
6134
6328
  // adoptSessionProvider awaits only getConfig() (already resolved here) and then
6135
6329
  // runs every write - provider/method/config/marker/user - in one synchronous
@@ -6300,7 +6494,7 @@ function ensureSharedWalletLoginReady() {
6300
6494
  if (!sharedWalletReady) {
6301
6495
  sharedWalletReady = (async () => {
6302
6496
  await core.getConfig();
6303
- await Promise.resolve().then(function () { return require('./injected-wallet-provider-oWtACL_7.js'); });
6497
+ await Promise.resolve().then(function () { return require('./injected-wallet-provider-CEtgcsQl.js'); });
6304
6498
  })().catch((error) => {
6305
6499
  sharedWalletReady = null;
6306
6500
  throw error;
@@ -6403,7 +6597,7 @@ async function loginWithWallet(options) {
6403
6597
  await ensureSharedWalletLoginReady();
6404
6598
  else
6405
6599
  await ensureWalletLoginReady();
6406
- const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-oWtACL_7.js'); });
6600
+ const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-CEtgcsQl.js'); });
6407
6601
  // Keep the RAW wallet provider: the offchain wrapper forwards only a fixed
6408
6602
  // set of methods, so cleanup below must speak to the provider that actually
6409
6603
  // holds the config rather than to whatever is wrapping it.
@@ -6656,13 +6850,14 @@ var index = /*#__PURE__*/Object.freeze({
6656
6850
  WalletConfigError: WalletConfigError,
6657
6851
  clearIncompatibleSession: clearIncompatibleSession,
6658
6852
  completeLoginFromRedirect: completeLoginFromRedirect,
6659
- completeLoginInPopup: completeLoginInPopup,
6853
+ completePendingLogin: completePendingLogin,
6660
6854
  ensureSharedWalletLoginReady: ensureSharedWalletLoginReady,
6661
6855
  ensureWalletLoginReady: ensureWalletLoginReady,
6662
6856
  getAuthProvider: getAuthProvider,
6663
6857
  getConfiguredSolanaNetwork: getConfiguredSolanaNetwork,
6664
6858
  getCurrentAuthMethod: getCurrentAuthMethod,
6665
6859
  getInitConfig: getInitConfig,
6860
+ getLoginReturnError: getLoginReturnError,
6666
6861
  isEmailRequired: isEmailRequired,
6667
6862
  isWalletLoginDisabled: isWalletLoginDisabled,
6668
6863
  isWalletLoginEnabled: isWalletLoginEnabled,
@@ -6739,6 +6934,10 @@ let authLoadingListeners = [];
6739
6934
  let initCompleted = false;
6740
6935
  let isAuthLoading = false;
6741
6936
  let authReadyPromise = null;
6937
+ let initializingConfig = null;
6938
+ let initializationGeneration = 0;
6939
+ /** Internal guard for a login result arriving after the app reconfigured the SDK. */
6940
+ function getInitializationGeneration() { return initializationGeneration; }
6742
6941
  // This file acts as a middleware for the global state of the SDK
6743
6942
  // This mostly involves setting up the AuthProvider and managing the current user
6744
6943
  async function initialize(newConfig) {
@@ -6764,6 +6963,19 @@ async function initialize(newConfig) {
6764
6963
  authProviderInstance = await getAuthProvider(newConfig);
6765
6964
  // Initialize config with auth provider
6766
6965
  await core.init(Object.assign(Object.assign({}, newConfig), { authProvider: authProviderInstance }));
6966
+ // A return establishes its own provider/session, or hands off a popup.
6967
+ // Restoring the old provider afterwards would overwrite the new login.
6968
+ try {
6969
+ if ((await completePendingLogin()).handled) {
6970
+ initCompleted = true;
6971
+ return;
6972
+ }
6973
+ }
6974
+ catch (_a) {
6975
+ // A cancelled/failed return is a login failure, not a broken SDK.
6976
+ // The completion layer keeps it in getLoginReturnError(); restore
6977
+ // any existing session and leave sign-in/readiness usable for retry.
6978
+ }
6767
6979
  // Now that core is initialized, update the auth method on the config
6768
6980
  const authMethod = getCurrentAuthMethod();
6769
6981
  if (authMethod) {
@@ -6795,7 +7007,42 @@ async function initialize(newConfig) {
6795
7007
  setAuthLoading(false);
6796
7008
  }
6797
7009
  }
6798
- function init(newConfig) {
7010
+ // Compare data options by value, but preserve identity for callbacks and class
7011
+ // instances such as providers. Comparing only app/endpoints would silently drop
7012
+ // a concurrent change to wallet policy or provider configuration.
7013
+ function sameConfigValue(a, b, seen = new WeakMap()) {
7014
+ var _a;
7015
+ if (Object.is(a, b))
7016
+ return true;
7017
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object')
7018
+ return false;
7019
+ if (Array.isArray(a) !== Array.isArray(b))
7020
+ return false;
7021
+ if (Array.isArray(a) && a.length !== b.length)
7022
+ return false;
7023
+ if (!Array.isArray(a)) {
7024
+ for (const value of [a, b]) {
7025
+ const prototype = Object.getPrototypeOf(value);
7026
+ if (prototype !== null && ((_a = prototype.constructor) === null || _a === void 0 ? void 0 : _a.name) !== 'Object')
7027
+ return false;
7028
+ }
7029
+ }
7030
+ if (seen.has(a))
7031
+ return seen.get(a) === b;
7032
+ seen.set(a, b);
7033
+ const left = Object.keys(a), right = Object.keys(b);
7034
+ return left.length === right.length && left.every((key) => Object.prototype.hasOwnProperty.call(b, key)
7035
+ && sameConfigValue(a[key], b[key], seen));
7036
+ }
7037
+ function init$1(newConfig) {
7038
+ if (initializingConfig) {
7039
+ if (sameConfigValue(initializingConfig, newConfig)) {
7040
+ return authReadyPromise;
7041
+ }
7042
+ return Promise.reject(new Error('SDK initialization is in progress. Await init() before changing its configuration.'));
7043
+ }
7044
+ initializingConfig = Object.assign({}, newConfig);
7045
+ initializationGeneration++;
6799
7046
  let resolveReady;
6800
7047
  let rejectReady;
6801
7048
  const ready = new Promise((resolve, reject) => {
@@ -6806,7 +7053,7 @@ function init(newConfig) {
6806
7053
  // auth-loading listener to call waitForAuthReady() synchronously when init()
6807
7054
  // announces that session restoration has begun.
6808
7055
  authReadyPromise = ready;
6809
- void initialize(newConfig).then(resolveReady, rejectReady);
7056
+ void initialize(initializingConfig).then(() => { initializingConfig = null; resolveReady(); }, (error) => { initializingConfig = null; rejectReady(error); });
6810
7057
  return ready;
6811
7058
  }
6812
7059
  /**
@@ -6929,6 +7176,38 @@ function setAuthProviderInstance(provider) {
6929
7176
  authProviderInstance = provider;
6930
7177
  }
6931
7178
 
7179
+ /** The text OpenApps templates carried where an app id belongs. Never a real id. */
7180
+ const APP_ID_PLACEHOLDER = 'REPLACE_WITH_MAIN_APP_ID';
7181
+ const NETWORKS = new Set(['bounded', 'bounded-staging', 'bounded-production']);
7182
+ // Bounded hosting states which app a page is: `<meta name="bounded-app-id">`
7183
+ // and `<meta name="bounded-network">`, first in <head> of every page it serves,
7184
+ // so the first tag of each name is the host's (docs/app-identity.md). Outside a
7185
+ // browser there is no page to read.
7186
+ function pageMeta(name) {
7187
+ var _a, _b;
7188
+ if (typeof document === 'undefined' || typeof document.querySelector !== 'function')
7189
+ return undefined;
7190
+ const value = (_b = (_a = document.querySelector(`meta[name="${name}"]`)) === null || _a === void 0 ? void 0 : _a.getAttribute('content')) === null || _b === void 0 ? void 0 : _b.trim();
7191
+ return value || undefined;
7192
+ }
7193
+ /**
7194
+ * The configuration init() runs with. An explicit appId keeps the call exactly
7195
+ * as written. Without one (or with the template placeholder), a page served by
7196
+ * Bounded supplies its app id, and its network unless the call names one, so
7197
+ * code in any framework can call init() without carrying an id in its source.
7198
+ */
7199
+ function withHostedIdentity(config) {
7200
+ if (config.appId && config.appId !== APP_ID_PLACEHOLDER)
7201
+ return config;
7202
+ const appId = pageMeta('bounded-app-id');
7203
+ if (!appId || appId === APP_ID_PLACEHOLDER) {
7204
+ throw new Error('No app ID provided. Pass init({ appId }), or serve this page from Bounded hosting, '
7205
+ + 'which states it in <meta name="bounded-app-id">.');
7206
+ }
7207
+ const network = config.network === undefined ? pageMeta('bounded-network') : undefined;
7208
+ return Object.assign(Object.assign(Object.assign({}, config), { appId }), (network && NETWORKS.has(network) ? { network: network } : {}));
7209
+ }
7210
+
6932
7211
  // bounded-widget-ownership.ts - per-open ownership generation for the singleton
6933
7212
  // login/signing modal (bounded-widget-modal.ts).
6934
7213
  //
@@ -7533,7 +7812,7 @@ function runOtpFrame(run) {
7533
7812
  // Backpack) via loginWithWallet(), run ENTIRELY on the app page (injected
7534
7813
  // providers only exist there, never inside a cross-origin frame).
7535
7814
  //
7536
- // Redirect stays available as the popup-blocked fallback (loginWithRedirect).
7815
+ // Mobile uses a full-page redirect; desktop keeps a popup with redirect fallback.
7537
7816
  // The same modal is reused for Turnkey wallet signing, so login -> first
7538
7817
  // signature is one continuous surface.
7539
7818
  // Palette + type ladder mirror the hosted auth page (login-page.ts): dark editorial
@@ -7776,6 +8055,16 @@ async function prepareAndOpen(opts, pending) {
7776
8055
  var _a, _b, _c, _d, _e, _f;
7777
8056
  // Imported lazily to avoid a static cycle with the auth index barrel.
7778
8057
  const authApi = await untilCancelled(Promise.resolve().then(function () { return index; }), pending);
8058
+ const hostedLogin = (options) => {
8059
+ if (detectMobile())
8060
+ return authApi.loginWithRedirect(options);
8061
+ // Invoke immediately inside the click handler to retain popup activation.
8062
+ return authApi.loginWithPopup(options).catch((error) => {
8063
+ if (shouldFallbackToRedirect(error))
8064
+ return authApi.loginWithRedirect(options);
8065
+ throw error;
8066
+ });
8067
+ };
7779
8068
  // BEFORE any side effect. Registering the mobile wallet from an empty config
7780
8069
  // would take the chainless mainnet default, and that registration cannot be
7781
8070
  // withdrawn - so opening the widget before init() would pin the page to the
@@ -7869,6 +8158,7 @@ async function prepareAndOpen(opts, pending) {
7869
8158
  // asserts rather than fixing a bug - which is exactly why it is worth keeping.
7870
8159
  pendingOpens.delete(pending);
7871
8160
  return new Promise((resolve, reject) => {
8161
+ var _a;
7872
8162
  // Per-open ownership generation (DS3-0402): a later open bumps the modal's
7873
8163
  // generation, so this open's token stops matching and all of its callbacks
7874
8164
  // (backdrop, success, cancel, and the async view swap) no-op instead of
@@ -8087,23 +8377,20 @@ async function prepareAndOpen(opts, pending) {
8087
8377
  loginHint: input.value,
8088
8378
  redirectUri: opts.redirectUri,
8089
8379
  };
8090
- authApi
8380
+ hostedLogin(emailOpts)
8091
8381
  // A hosted login that RESOLVES minted a real session, so it settles
8092
8382
  // the card even if another lane has since taken it - dropping it
8093
8383
  // would leave the app signed in behind a widget that says it is not.
8094
8384
  // Only the failure path belongs to one attempt's UI.
8095
- .loginWithPopup(emailOpts)
8096
- .then((u) => { settleAttempt(attempt); succeed(u); })
8097
- .catch((err) => {
8098
- // Popup unavailable: same-tab redirect instead. It resolves with
8099
- // nothing when it is about to navigate, so the only thing that
8100
- // comes back here is a REJECTION - which must still reach the
8101
- // handler below, or the lane stays locked with nothing running.
8102
- if (shouldFallbackToRedirect(err)) {
8103
- return authApi.loginWithRedirect(emailOpts).then((u) => { if (u)
8104
- succeed(u); });
8385
+ .then((u) => {
8386
+ if (u) {
8387
+ settleAttempt(attempt);
8388
+ succeed(u);
8389
+ }
8390
+ else if (settleAttempt(attempt)) {
8391
+ submit.disabled = false;
8392
+ submit.textContent = "Continue";
8105
8393
  }
8106
- throw err;
8107
8394
  })
8108
8395
  .catch((err) => {
8109
8396
  if (!settleAttempt(attempt))
@@ -8140,15 +8427,16 @@ async function prepareAndOpen(opts, pending) {
8140
8427
  }
8141
8428
  setBusy(b, "Opening secure sign-in\u2026");
8142
8429
  const socialOpts = { provider: method, redirectUri: opts.redirectUri };
8143
- authApi
8144
- .loginWithPopup(socialOpts)
8145
- .then((u) => { settleAttempt(attempt); succeed(u); })
8146
- .catch((err) => {
8147
- if (shouldFallbackToRedirect(err)) {
8148
- return authApi.loginWithRedirect(socialOpts).then((u) => { if (u)
8149
- succeed(u); });
8430
+ hostedLogin(socialOpts)
8431
+ .then((u) => {
8432
+ if (u) {
8433
+ settleAttempt(attempt);
8434
+ succeed(u);
8435
+ }
8436
+ else if (settleAttempt(attempt)) {
8437
+ b.disabled = false;
8438
+ setLabel(b, method);
8150
8439
  }
8151
- throw err;
8152
8440
  })
8153
8441
  .catch((err) => {
8154
8442
  if (!settleAttempt(attempt))
@@ -8412,7 +8700,7 @@ async function prepareAndOpen(opts, pending) {
8412
8700
  fitPanelToContent(m);
8413
8701
  });
8414
8702
  };
8415
- swap(renderChoose());
8703
+ swap(renderChoose((_a = authApi.getLoginReturnError()) === null || _a === void 0 ? void 0 : _a.message));
8416
8704
  showModal(m);
8417
8705
  });
8418
8706
  }
@@ -10706,6 +10994,22 @@ async function connectServiceSession(ticket, options) {
10706
10994
  }
10707
10995
  }
10708
10996
 
10997
+ /**
10998
+ * Initialize the SDK. A page served by Bounded already states which app it is,
10999
+ * so `init()` there needs no `appId`; pass one to run anywhere else (another
11000
+ * host, local development). An explicit `appId` is used exactly as given.
11001
+ */
11002
+ function init(config = {}) {
11003
+ let resolved;
11004
+ try {
11005
+ resolved = withHostedIdentity(config);
11006
+ }
11007
+ catch (error) {
11008
+ return Promise.reject(error);
11009
+ }
11010
+ return init$1(resolved);
11011
+ }
11012
+
10709
11013
  exports.BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA = BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA;
10710
11014
  exports.BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA = BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA;
10711
11015
  exports.BUILD_REVIEW_CAPABILITY_SCHEMA = BUILD_REVIEW_CAPABILITY_SCHEMA;
@@ -10723,7 +11027,6 @@ exports.bs58 = bs58;
10723
11027
  exports.bufferExports = bufferExports;
10724
11028
  exports.builds = builds;
10725
11029
  exports.completeLoginFromRedirect = completeLoginFromRedirect;
10726
- exports.completeLoginInPopup = completeLoginInPopup;
10727
11030
  exports.confirmAndCheckTransaction = confirmAndCheckTransaction;
10728
11031
  exports.connectBroadcast = connectBroadcast;
10729
11032
  exports.connectServiceSession = connectServiceSession;
@@ -10737,6 +11040,7 @@ exports.getAuthLoading = getAuthLoading;
10737
11040
  exports.getAuthProvider = getAuthProvider;
10738
11041
  exports.getCurrentUser = getCurrentUser;
10739
11042
  exports.getIdToken = getIdToken;
11043
+ exports.getLoginReturnError = getLoginReturnError;
10740
11044
  exports.getOrCreateTurnkeyWallet = getOrCreateTurnkeyWallet;
10741
11045
  exports.getPlatform = getPlatform;
10742
11046
  exports.getTurnkeyWalletRecoveryUrl = getTurnkeyWalletRecoveryUrl;
@@ -10777,4 +11081,4 @@ exports.useAuth = useAuth;
10777
11081
  exports.useBuildRun = useBuildRun;
10778
11082
  exports.useQuery = useQuery;
10779
11083
  exports.waitForAuthReady = waitForAuthReady;
10780
- //# sourceMappingURL=index-DTBxbfUl.js.map
11084
+ //# sourceMappingURL=index-ok-Nn6DV.js.map