@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
@@ -1,4 +1,4 @@
1
- import { getConfig, getActiveSessionManager, WebSessionManager, ReactNativeSessionManager, revokeSession, init as init$1, reconnectWithNewAuth, onSessionCleared, deriveUserIdentityFromIdToken, subscribe, getFreshAuthToken, getSessionGeneration, getIdToken as getIdToken$1, genAuthNonce, genSolanaMessage, createSessionWithSignature } from '@bounded-sh/core';
1
+ import { getConfig, getActiveSessionManager, WebSessionManager, ReactNativeSessionManager, revokeSession, reconnectWithNewAuth, init as init$2, deriveUserIdentityFromIdToken, onSessionCleared, subscribe, getFreshAuthToken, getSessionGeneration, getIdToken as getIdToken$1, genAuthNonce, genSolanaMessage, createSessionWithSignature } from '@bounded-sh/core';
2
2
  import * as React from 'react';
3
3
 
4
4
  function walletContextFromClaims(claims, baseUrl, expectedAppId, tenantOrigin) {
@@ -190,6 +190,36 @@ function setPlatform(adapter) {
190
190
  function getPlatform() {
191
191
  return _platform;
192
192
  }
193
+ // ---------------------------------------------------------------------------
194
+ // Mobile / Android detection helpers
195
+ // ---------------------------------------------------------------------------
196
+ /**
197
+ * Detect mobile device even when Chrome "Request Desktop Site" is active.
198
+ *
199
+ * Desktop mode on Android strips "Android" and "Mobile" from the UA, leaving
200
+ * something like "Linux x86_64 …". The fallback detects this by looking for
201
+ * touch capability + small viewport + a UA that doesn't belong to a known
202
+ * desktop OS (ChromeOS, Windows, macOS).
203
+ */
204
+ function detectMobile() {
205
+ const ua = _platform.getUserAgent();
206
+ if (/Android|iPhone|iPad|iPod/i.test(ua))
207
+ return true;
208
+ if (typeof navigator !== 'undefined' && typeof window !== 'undefined') {
209
+ const hasTouch = navigator.maxTouchPoints > 0;
210
+ // Macs don't have touch screens, so "Macintosh" + touch = iOS desktop mode
211
+ // (also catches iPadOS 13+ which always reports as Macintosh)
212
+ if (/Macintosh/i.test(ua) && hasTouch)
213
+ return true;
214
+ // Android desktop mode: Linux UA + touch + small viewport, excluding
215
+ // known desktop OSes (ChromeOS, Windows, macOS)
216
+ const isSmallViewport = window.innerWidth <= 1024;
217
+ const isDesktopOS = /CrOS|Windows|Macintosh/i.test(ua);
218
+ if (hasTouch && isSmallViewport && !isDesktopOS)
219
+ return true;
220
+ }
221
+ return false;
222
+ }
193
223
 
194
224
  /**
195
225
  * OffchainAuthProvider wraps a real auth provider (Turnkey, Phantom, ...) for the
@@ -5016,9 +5046,18 @@ const OIDC_NO_WALLET = 'This session has no Turnkey wallet. Sign in again to pro
5016
5046
  // same tab (redirect + popup, or two popups) each own a distinct record, so a second
5017
5047
  // attempt cannot clobber the first's verifier/state/redirectUri.
5018
5048
  const PKCE_STORE_PREFIX = 'bounded.oidc.pkce';
5049
+ const STATE_PREFIX = 'bounded.';
5050
+ const LOGIN_TIMEOUT_MS = 10 * 60000;
5051
+ const CALLBACK_PARAMS = ['code', 'state', 'error', 'error_description', 'iss'];
5019
5052
  function pkceKey(state) {
5020
5053
  return `${PKCE_STORE_PREFIX}.${state}`;
5021
5054
  }
5055
+ function removeAttempt(state) {
5056
+ try {
5057
+ sessionStorage.removeItem(pkceKey(state));
5058
+ }
5059
+ catch ( /* Storage may have been disabled mid-login. */_a) { /* Storage may have been disabled mid-login. */ }
5060
+ }
5022
5061
  const ISSUER_SESSION_KIND_KEY = 'bounded_issuer_session_kind';
5023
5062
  // Active session store (Web on web, RN on React Native) — selected by config,
5024
5063
  // not by `typeof window` (truthy on RN). OIDC redirect/popup is web-only, but the
@@ -5127,6 +5166,165 @@ async function makePkce() {
5127
5166
  function randomState() {
5128
5167
  return b64url(getPlatform().getRandomBytes(16));
5129
5168
  }
5169
+ let pendingCompletion;
5170
+ let loginReturnError;
5171
+ /** The last OAuth return failure in this initialization, or null. Initialization
5172
+ * remains usable so the UI can display this error and offer a fresh sign-in. */
5173
+ function getLoginReturnError() {
5174
+ return (loginReturnError === null || loginReturnError === void 0 ? void 0 : loginReturnError.generation) === getInitializationGeneration() ? loginReturnError.error : null;
5175
+ }
5176
+ // A hosted page can contain both an app bundle and the platform widget's SDK.
5177
+ // They share one code exchange, while adopting the session in their own registries.
5178
+ const COMPLETIONS = Symbol.for('bounded.oidc.completions');
5179
+ function cleanCallbackUrl(url) {
5180
+ const clean = new URL(url);
5181
+ for (const key of CALLBACK_PARAMS)
5182
+ clean.searchParams.delete(key);
5183
+ return clean;
5184
+ }
5185
+ async function validateAttempt(pkce, callback) {
5186
+ const cfg = await getConfig();
5187
+ if (!pkce || typeof pkce.verifier !== 'string' || !pkce.verifier ||
5188
+ pkce.appId !== cfg.appId || pkce.issuer !== await issuerBase()) {
5189
+ throw new Error('Login belongs to a different app or auth server. Start sign-in again.');
5190
+ }
5191
+ if (!Number.isFinite(pkce.createdAt) || Date.now() - pkce.createdAt > LOGIN_TIMEOUT_MS || pkce.createdAt > Date.now()) {
5192
+ throw new Error('Login expired. Start sign-in again.');
5193
+ }
5194
+ if (callback && (cleanCallbackUrl(callback).href !== cleanCallbackUrl(new URL(pkce.redirectUri)).href ||
5195
+ (callback.searchParams.has('iss') && callback.searchParams.get('iss') !== pkce.issuer))) {
5196
+ throw new Error('Login returned to a different address or from a different auth server. Start sign-in again.');
5197
+ }
5198
+ }
5199
+ // The receiver acknowledges only after checking origin, window and state. An
5200
+ // absent/disconnected opener must never leave a popup claiming success.
5201
+ async function handOffPopup(url, state) {
5202
+ const cfg = await getConfig();
5203
+ const issuer = await issuerBase();
5204
+ const opener = window.opener;
5205
+ if (!opener || opener === window || opener.closed) {
5206
+ throw new Error('The original sign-in window is unavailable. Close this window and sign in again in the original tab.');
5207
+ }
5208
+ return new Promise((resolve, reject) => {
5209
+ const cleanup = () => { clearTimeout(timer); window.removeEventListener('message', onMessage); };
5210
+ const onMessage = (event) => {
5211
+ var _a;
5212
+ if (event.source !== opener || event.origin !== url.origin ||
5213
+ ((_a = event.data) === null || _a === void 0 ? void 0 : _a.type) !== 'bounded-oidc-accepted' || event.data.state !== state)
5214
+ return;
5215
+ cleanup();
5216
+ try {
5217
+ window.close();
5218
+ }
5219
+ catch ( /* The opener also closes its own popup handle. */_b) { /* The opener also closes its own popup handle. */ }
5220
+ resolve();
5221
+ };
5222
+ const timer = setTimeout(() => {
5223
+ cleanup();
5224
+ reject(new Error('Could not reach the original sign-in tab. Close this window and start sign-in again.'));
5225
+ }, 10000);
5226
+ window.addEventListener('message', onMessage);
5227
+ try {
5228
+ opener.postMessage({ type: 'bounded-oidc-code', state, code: url.searchParams.get('code'),
5229
+ error: url.searchParams.get('error'), errorDescription: url.searchParams.get('error_description'),
5230
+ callbackUrl: url.href, appId: cfg.appId, issuer }, url.origin);
5231
+ }
5232
+ catch (error) {
5233
+ cleanup();
5234
+ reject(error);
5235
+ }
5236
+ });
5237
+ }
5238
+ async function finishLoginReturn(url, state) {
5239
+ try {
5240
+ if (!/^bounded\.(popup|redirect)\.[A-Za-z0-9_-]{22}$/.test(state)) {
5241
+ throw new Error('Invalid login state. Start sign-in again.');
5242
+ }
5243
+ if (state.startsWith('bounded.popup.')) {
5244
+ await handOffPopup(url, state);
5245
+ return { handled: true, user: null };
5246
+ }
5247
+ const raw = sessionStorage.getItem(pkceKey(state));
5248
+ if (!raw)
5249
+ throw new Error('Missing login attempt. Start sign-in again in this tab.');
5250
+ const pkce = JSON.parse(raw);
5251
+ if (pkce.state !== state)
5252
+ throw new Error('OAuth state mismatch. Start sign-in again.');
5253
+ await validateAttempt(pkce, url);
5254
+ const error = url.searchParams.get('error');
5255
+ if (error)
5256
+ throw new Error(url.searchParams.get('error_description') || `Login failed: ${error}`);
5257
+ const user = await exchangeCode(url.searchParams.get('code'), pkce);
5258
+ return { handled: true, user, idToken: sessionManager().getIdToken() };
5259
+ }
5260
+ finally {
5261
+ removeAttempt(state);
5262
+ const clean = cleanCallbackUrl(url);
5263
+ if (window.location.href === url.href) {
5264
+ window.history.replaceState(window.history.state, document.title, clean.pathname + clean.search + clean.hash);
5265
+ }
5266
+ }
5267
+ }
5268
+ /** Internal initialization step. Unrelated OAuth callbacks are left untouched. */
5269
+ function completePendingLogin() {
5270
+ if (!hasDOM$2())
5271
+ return Promise.resolve({ handled: false, user: null });
5272
+ const url = new URL(window.location.href);
5273
+ const state = url.searchParams.get('state');
5274
+ if (!(state === null || state === void 0 ? void 0 : state.startsWith(STATE_PREFIX)) || (!url.searchParams.get('code') && !url.searchParams.get('error'))) {
5275
+ return Promise.resolve({ handled: false, user: null });
5276
+ }
5277
+ const generation = getInitializationGeneration();
5278
+ if ((pendingCompletion === null || pendingCompletion === void 0 ? void 0 : pendingCompletion.href) === url.href && pendingCompletion.generation === generation)
5279
+ return pendingCompletion.promise;
5280
+ const promise = finishSharedLoginReturn(url, state).catch((error) => {
5281
+ loginReturnError = { generation, error: error instanceof Error ? error : new Error(String(error)) };
5282
+ throw loginReturnError.error;
5283
+ }).finally(() => {
5284
+ if ((pendingCompletion === null || pendingCompletion === void 0 ? void 0 : pendingCompletion.promise) === promise)
5285
+ pendingCompletion = undefined;
5286
+ });
5287
+ pendingCompletion = { href: url.href, generation, promise };
5288
+ return promise;
5289
+ }
5290
+ async function finishSharedLoginReturn(url, state) {
5291
+ var _a;
5292
+ var _b;
5293
+ const cfg = await getConfig();
5294
+ const issuer = await issuerBase();
5295
+ if (state.startsWith('bounded.redirect.')) {
5296
+ const raw = sessionStorage.getItem(pkceKey(state));
5297
+ // A second SDK configured for another app/issuer on this page does not own
5298
+ // this return. Leave its URL and proof available to the initiating app.
5299
+ try {
5300
+ const attempt = raw && JSON.parse(raw);
5301
+ if (attempt && (attempt.appId !== cfg.appId || attempt.issuer !== issuer))
5302
+ return { handled: false, user: null };
5303
+ }
5304
+ catch ( /* The owning completion reports malformed attempt data. */_c) { /* The owning completion reports malformed attempt data. */ }
5305
+ }
5306
+ const shared = (_a = (_b = window)[COMPLETIONS]) !== null && _a !== void 0 ? _a : (_b[COMPLETIONS] = new Map());
5307
+ const key = JSON.stringify([url.href, cfg.appId, issuer]);
5308
+ const existing = shared.get(key);
5309
+ if (!existing) {
5310
+ const completion = finishLoginReturn(url, state);
5311
+ shared.set(key, completion);
5312
+ return completion;
5313
+ }
5314
+ const generation = getInitializationGeneration();
5315
+ const result = await existing;
5316
+ if (result.user) {
5317
+ if (!result.idToken || sessionManager().getIdToken() !== result.idToken) {
5318
+ throw new Error('The completed login session has changed. Start sign-in again.');
5319
+ }
5320
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc',
5321
+ user: Object.assign(Object.assign({}, result.user), { provider: oidcProviderSingleton }), beforeCommit: () => {
5322
+ if (generation !== getInitializationGeneration())
5323
+ throw new Error('Login was superseded by SDK reinitialization. Start sign-in again.');
5324
+ } });
5325
+ }
5326
+ return result;
5327
+ }
5130
5328
  function normalizeMethod(value) {
5131
5329
  const normalized = String(value || '').trim().toLowerCase();
5132
5330
  return /^[a-z0-9_-]{1,40}$/.test(normalized) ? normalized : null;
@@ -5169,7 +5367,8 @@ function appendHostedLoginHints(params, opts) {
5169
5367
  // native (expo-web-browser) path. The caller decides where the PkceState lives
5170
5368
  // (web → sessionStorage across the full-page redirect; native → in-memory across
5171
5369
  // the awaited browser session).
5172
- async function buildAuthorize(opts) {
5370
+ async function buildAuthorize(opts, mode = 'redirect') {
5371
+ loginReturnError = undefined;
5173
5372
  const cfg = await getConfig();
5174
5373
  if (!cfg.appId)
5175
5374
  throw new Error('appId is not configured (init the SDK with your appId)');
@@ -5178,15 +5377,23 @@ async function buildAuthorize(opts) {
5178
5377
  const redirectUri = opts.redirectUri || (hasDOM$2() ? window.location.origin + window.location.pathname : undefined);
5179
5378
  if (!redirectUri)
5180
5379
  throw new Error('redirectUri is required on React Native — pass the https universal link the app owner registered.');
5380
+ if (hasDOM$2()) {
5381
+ const redirect = new URL(redirectUri);
5382
+ if (redirect.origin !== window.location.origin || redirect.hash || CALLBACK_PARAMS.some((key) => redirect.searchParams.has(key))) {
5383
+ throw new Error('redirectUri must be on this app origin, without a fragment or OAuth callback parameters.');
5384
+ }
5385
+ }
5181
5386
  const { verifier, challenge } = await makePkce();
5182
- const state = randomState();
5387
+ const state = `${STATE_PREFIX}${mode}.${randomState()}`;
5183
5388
  const base = await issuerBase();
5184
5389
  const params = new URLSearchParams({
5185
5390
  client_id: cfg.appId, redirect_uri: redirectUri, response_type: 'code', scope: 'openid profile email',
5186
5391
  code_challenge: challenge, code_challenge_method: 'S256', state,
5187
5392
  });
5188
5393
  appendHostedLoginHints(params, opts);
5189
- return { url: `${base}/api/auth/oauth2/authorize?${params.toString()}`, pkce: { verifier, state, redirectUri } };
5394
+ return { url: `${base}/api/auth/oauth2/authorize?${params.toString()}`, pkce: {
5395
+ verifier, state, redirectUri, appId: cfg.appId, issuer: base, createdAt: Date.now(),
5396
+ } };
5190
5397
  }
5191
5398
  // Are we on React Native (no DOM)? OIDC dispatch uses this to choose the native
5192
5399
  // expo-web-browser driver vs. the web full-page-redirect/popup driver.
@@ -5235,8 +5442,7 @@ function parseCallbackParams(callbackUrl) {
5235
5442
  }
5236
5443
  // Full native login: open the hosted page in the system auth session, await the
5237
5444
  // universal-link callback, verify state, and run the SAME PKCE code→token exchange
5238
- // as web. Returns the signed-in User (native completes inline — there's no separate
5239
- // completeLoginFromRedirect() round-trip the way the web full-page redirect needs).
5445
+ // as web. Returns the signed-in User inline; web redirects complete on the next init().
5240
5446
  async function runNativeLogin(opts) {
5241
5447
  var _a, _b;
5242
5448
  const { url, pkce } = await buildAuthorize(opts);
@@ -5263,7 +5469,7 @@ async function runNativeLogin(opts) {
5263
5469
  if (!returnedState || returnedState !== pkce.state) {
5264
5470
  throw new Error('OAuth state mismatch — possible CSRF; aborting');
5265
5471
  }
5266
- return await exchangeCode(code, pkce.verifier, pkce.redirectUri);
5472
+ return await exchangeCode(code, pkce);
5267
5473
  }
5268
5474
  finally {
5269
5475
  setAuthLoading(false);
@@ -5272,7 +5478,7 @@ async function runNativeLogin(opts) {
5272
5478
  /** Start hosted login (Google / Apple / email / hosted phone). Works on WEB and REACT NATIVE.
5273
5479
  *
5274
5480
  * WEB: navigates the whole page to the hosted Bounded login and resolves (void) as the page
5275
- * unloads; on return to `redirectUri?code=...` you call completeLoginFromRedirect().
5481
+ * unloads; init() completes the return to redirectUri automatically.
5276
5482
  *
5277
5483
  * REACT NATIVE: opens the hosted page via expo-web-browser, awaits the https universal-link
5278
5484
  * callback, runs the PKCE exchange INLINE, and resolves with the signed-in User. No
@@ -5290,63 +5496,32 @@ async function loginWithRedirect(opts) {
5290
5496
  // is per-tab and cleared on close — appropriate for a short-lived auth handshake. Keyed by
5291
5497
  // this attempt's own state so a second concurrent login cannot overwrite it.
5292
5498
  sessionStorage.setItem(pkceKey(pkce.state), JSON.stringify(pkce));
5293
- setAuthLoading(true); // reflect "signing in…" right up to navigation; the return page's
5294
- window.location.href = url; // completeLoginFromRedirect() keeps it true until the user resolves.
5295
- }
5296
- /** Finish login on the redirect_uri page (WEB ONLY): exchange ?code= for a token (PKCE-verified),
5297
- * store the session, and return the User. Idempotent: returns null if there's no ?code= in the URL.
5298
- *
5299
- * On REACT NATIVE this returns null and is a no-op: native login completes INLINE inside
5300
- * loginWithRedirect() (which resolves with the User), so there is no separate redirect
5301
- * page to finish from. Calling it on RN is harmless. */
5302
- async function completeLoginFromRedirect() {
5303
- if (!hasDOM$2())
5304
- return null;
5305
- const url = new URL(window.location.href);
5306
- const code = url.searchParams.get('code');
5307
- const returnedState = url.searchParams.get('state');
5308
- if (!code)
5309
- return null;
5310
- // POPUP path: if we're the redirect_uri page running INSIDE the loginWithPopup() window,
5311
- // hand the code to the opener and close — no separate completeLoginInPopup() call needed.
5312
- // The opener is the same app/origin, so post to our own origin (the opener's message
5313
- // handler checks ev.origin === redirect_uri origin). One call now finishes BOTH flows.
5314
- if (window.opener && window.opener !== window) {
5315
- try {
5316
- window.opener.postMessage({ type: 'bounded-oidc-code', code, state: returnedState }, window.location.origin);
5317
- }
5318
- catch ( /* ignore */_a) { /* ignore */ }
5319
- try {
5320
- window.close();
5321
- }
5322
- catch ( /* ignore */_b) { /* ignore */ }
5323
- return null;
5324
- }
5499
+ // This promise represents dispatch, not authentication on the next page.
5500
+ // Browsers can cancel navigation (for example at beforeunload) without firing
5501
+ // pageshow. Never leave this document's SDK loading after dispatch settles.
5325
5502
  setAuthLoading(true);
5326
5503
  try {
5327
- // Read back the exact record THIS attempt wrote, keyed by the state the issuer
5328
- // returned, so a concurrent login's record is never consumed by mistake.
5329
- const raw = returnedState ? sessionStorage.getItem(pkceKey(returnedState)) : null;
5330
- if (!raw)
5331
- throw new Error('Missing PKCE state — start login with loginWithRedirect()');
5332
- const pkce = JSON.parse(raw);
5333
- if (!returnedState || returnedState !== pkce.state)
5334
- throw new Error('OAuth state mismatch — possible CSRF; aborting');
5335
- const user = await exchangeCode(code, pkce.verifier, pkce.redirectUri);
5336
- sessionStorage.removeItem(pkceKey(returnedState));
5337
- // Strip code/state from the URL so a reload doesn't re-exchange (codes are single-use anyway).
5338
- url.searchParams.delete('code');
5339
- url.searchParams.delete('state');
5340
- window.history.replaceState({}, document.title, url.pathname + url.search + url.hash);
5341
- return user;
5504
+ window.location.href = url;
5505
+ }
5506
+ catch (error) {
5507
+ removeAttempt(pkce.state);
5508
+ throw error;
5342
5509
  }
5343
5510
  finally {
5344
5511
  setAuthLoading(false);
5345
5512
  }
5346
5513
  }
5514
+ /** Optional explicit completion. Normal apps only await init(), which already finishes login.
5515
+ * Concurrent calls share one exchange. Returns null when no Bounded return is present. */
5516
+ async function completeLoginFromRedirect() {
5517
+ const result = await completePendingLogin();
5518
+ const error = getLoginReturnError();
5519
+ if (error)
5520
+ throw error;
5521
+ return result.user;
5522
+ }
5347
5523
  /** Popup variant: open the hosted login in a popup; resolves with the User once the popup's
5348
- * redirect_uri page posts the code back. The redirect_uri page just calls the usual
5349
- * completeLoginFromRedirect() (which auto-detects the popup) — no separate call needed. */
5524
+ * redirect_uri page initializes the SDK, which hands the code back automatically. */
5350
5525
  async function loginWithPopup(opts) {
5351
5526
  if (!hasDOM$2())
5352
5527
  throw new Error('loginWithPopup is web-only — on React Native use loginWithRedirect()');
@@ -5365,7 +5540,7 @@ async function loginWithPopup(opts) {
5365
5540
  throw new Error('Popup blocked — use loginWithRedirect() instead');
5366
5541
  let authorization;
5367
5542
  try {
5368
- authorization = await buildAuthorize(opts);
5543
+ authorization = await buildAuthorize(opts, 'popup');
5369
5544
  }
5370
5545
  catch (error) {
5371
5546
  try {
@@ -5400,6 +5575,7 @@ async function loginWithPopup(opts) {
5400
5575
  popup.location.href = url;
5401
5576
  }
5402
5577
  catch (_d) {
5578
+ removeAttempt(pkce.state);
5403
5579
  try {
5404
5580
  popup.close();
5405
5581
  }
@@ -5415,17 +5591,21 @@ async function loginWithPopup(opts) {
5415
5591
  let settled = false;
5416
5592
  let timer;
5417
5593
  let navigationTimer;
5594
+ let completionTimer;
5418
5595
  const cleanup = () => {
5419
5596
  settled = true;
5420
5597
  clearInterval(timer);
5421
5598
  clearTimeout(navigationTimer);
5599
+ clearTimeout(completionTimer);
5422
5600
  window.removeEventListener('message', onMsg);
5601
+ removeAttempt(pkce.state);
5423
5602
  };
5424
5603
  const onMsg = async (ev) => {
5425
- if (ev.origin !== expectedOrigin)
5426
- return; // only trust the redirect_uri origin
5604
+ if (settled || ev.origin !== expectedOrigin || ev.source !== popup)
5605
+ return;
5427
5606
  const d = ev.data;
5428
- if (!d || d.type !== 'bounded-oidc-code' || typeof d.code !== 'string')
5607
+ if (!d || d.type !== 'bounded-oidc-code' ||
5608
+ (typeof d.code !== 'string' && typeof d.error !== 'string'))
5429
5609
  return;
5430
5610
  // Validate state BEFORE tearing down this listener, against the record THIS
5431
5611
  // attempt captured in the closure (`pkce`), not a shared sessionStorage reread.
@@ -5433,10 +5613,15 @@ async function loginWithPopup(opts) {
5433
5613
  // stay registered for our own code, instead of cleaning up on another's message.
5434
5614
  if (d.state !== pkce.state)
5435
5615
  return;
5616
+ if (d.appId !== pkce.appId || d.issuer !== pkce.issuer)
5617
+ return;
5436
5618
  cleanup();
5437
5619
  try {
5438
- const user = await exchangeCode(d.code, pkce.verifier, pkce.redirectUri);
5439
- sessionStorage.removeItem(pkceKey(pkce.state));
5620
+ await validateAttempt(pkce, new URL(d.callbackUrl));
5621
+ popup.postMessage({ type: 'bounded-oidc-accepted', state: pkce.state }, expectedOrigin);
5622
+ if (d.error)
5623
+ throw new Error(typeof d.errorDescription === 'string' ? d.errorDescription : `Login failed: ${d.error}`);
5624
+ const user = await exchangeCode(d.code, pkce);
5440
5625
  try {
5441
5626
  popup.close();
5442
5627
  }
@@ -5444,10 +5629,22 @@ async function loginWithPopup(opts) {
5444
5629
  resolve(user);
5445
5630
  }
5446
5631
  catch (e) {
5632
+ try {
5633
+ popup.close();
5634
+ }
5635
+ catch ( /* ignore */_b) { /* ignore */ }
5447
5636
  reject(e);
5448
5637
  }
5449
5638
  };
5450
5639
  window.addEventListener('message', onMsg);
5640
+ completionTimer = setTimeout(() => {
5641
+ cleanup();
5642
+ try {
5643
+ popup.close();
5644
+ }
5645
+ catch ( /* ignore */_a) { /* ignore */ }
5646
+ reject(new Error('Login expired or its popup disconnected. Start sign-in again.'));
5647
+ }, LOGIN_TIMEOUT_MS);
5451
5648
  // Some browser/extension combinations can return a real popup handle but
5452
5649
  // strand the window at about:blank instead of navigating to the hosted
5453
5650
  // authorize URL. The old code then waited forever because the popup was
@@ -5505,25 +5702,6 @@ async function loginWithPopup(opts) {
5505
5702
  setAuthLoading(false);
5506
5703
  }
5507
5704
  }
5508
- /** Call this on the redirect_uri page when login was opened via loginWithPopup(): posts the
5509
- * code back to the opener and closes. (For the redirect variant use completeLoginFromRedirect.) */
5510
- function completeLoginInPopup(openerOrigin) {
5511
- if (!hasDOM$2() || !window.opener)
5512
- return;
5513
- const url = new URL(window.location.href);
5514
- const code = url.searchParams.get('code');
5515
- const state = url.searchParams.get('state');
5516
- if (!code)
5517
- return;
5518
- try {
5519
- window.opener.postMessage({ type: 'bounded-oidc-code', code, state }, openerOrigin);
5520
- }
5521
- catch ( /* ignore */_a) { /* ignore */ }
5522
- try {
5523
- window.close();
5524
- }
5525
- catch ( /* ignore */_b) { /* ignore */ }
5526
- }
5527
5705
  // Establish a Bounded session from a server-minted inline-session response (the same
5528
5706
  // { idToken, accessToken, refreshToken } shape as /session/refresh). Used by the
5529
5707
  // Turnkey-native login path (authMode:"turnkey"), which mints the session server-side
@@ -5566,14 +5744,20 @@ async function inlineAuthEndpoint() {
5566
5744
  const cfg = await getConfig();
5567
5745
  return { base: await issuerBase(), appId: cfg.appId };
5568
5746
  }
5569
- async function exchangeCode(code, verifier, redirectUri) {
5570
- const cfg = await getConfig();
5571
- const base = await issuerBase();
5747
+ async function exchangeCode(code, pkce) {
5748
+ const generation = getInitializationGeneration();
5749
+ const beforeCommit = () => {
5750
+ if (generation !== getInitializationGeneration())
5751
+ throw new Error('Login was superseded by SDK reinitialization. Start sign-in again.');
5752
+ };
5753
+ await validateAttempt(pkce);
5754
+ beforeCommit();
5755
+ const base = pkce.issuer;
5572
5756
  // OIDC token endpoint: application/x-www-form-urlencoded (the standard). Public client (no
5573
5757
  // secret) authenticated by PKCE. The code is single-use + short-TTL on the server.
5574
5758
  const body = new URLSearchParams({
5575
- grant_type: 'authorization_code', code, redirect_uri: redirectUri,
5576
- client_id: cfg.appId, code_verifier: verifier,
5759
+ grant_type: 'authorization_code', code, redirect_uri: pkce.redirectUri,
5760
+ client_id: pkce.appId, code_verifier: pkce.verifier,
5577
5761
  });
5578
5762
  let res;
5579
5763
  try {
@@ -5597,10 +5781,20 @@ async function exchangeCode(code, verifier, redirectUri) {
5597
5781
  const accessToken = tokenField(data, 'access_token', 'accessToken') || idToken;
5598
5782
  const refreshToken = boundedRefreshTokenField(data);
5599
5783
  const user = userFromIdToken(idToken);
5600
- await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base);
5784
+ // Reinitialization while the request was in flight must not install this
5785
+ // app's result under a different app's session manager/configuration.
5786
+ beforeCommit();
5787
+ const rollback = await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base, beforeCommit);
5601
5788
  // Adopt on EVERY session surface (see establishInlineSession) - hosted
5602
5789
  // popup/redirect completions all land here, so this closes their gap too.
5603
- await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user });
5790
+ try {
5791
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user, beforeCommit });
5792
+ }
5793
+ catch (error) {
5794
+ if (typeof rollback === 'function')
5795
+ await rollback();
5796
+ throw error;
5797
+ }
5604
5798
  // OIDC login does establish the issuer's Better Auth browser cookie. Record
5605
5799
  // that fact separately from the provider name so logout clears the cookie
5606
5800
  // and returns through the registered app origin. Stamped AFTER the adoption
@@ -5966,7 +6160,7 @@ async function getAuthProvider(config) {
5966
6160
  // manager as legacy email sessions. Human login should be started
5967
6161
  // with loginWithRedirect(); this provider is kept as the restore and
5968
6162
  // logout surface for those sessions.
5969
- currentAuthProvider = new (await import('./email-auth-provider-DA2dPFXt.mjs')).EmailAuthProvider();
6163
+ currentAuthProvider = new (await import('./email-auth-provider-DEchcb2r.mjs')).EmailAuthProvider();
5970
6164
  break;
5971
6165
  case "phantom": {
5972
6166
  // First-class Solana WALLET LOGIN (the poof/TaroBase-style "connect
@@ -6008,7 +6202,7 @@ async function getAuthProvider(config) {
6008
6202
  // (bounded-login-widget.ts), so registration never counts as
6009
6203
  // evidence the user has a wallet.
6010
6204
  await ensureSolanaMobileWalletRegistered(config, configuredNetwork);
6011
- const { InjectedWalletProvider } = await import('./injected-wallet-provider-BGeNaoEY.mjs');
6205
+ const { InjectedWalletProvider } = await import('./injected-wallet-provider-DNZA1IJ9.mjs');
6012
6206
  currentAuthProvider = new InjectedWalletProvider(resolveInjectedWalletConfig(config), rpcUrl, configuredNetwork);
6013
6207
  break;
6014
6208
  }
@@ -6021,7 +6215,7 @@ async function getAuthProvider(config) {
6021
6215
  // Selecting authMethod:'evm-wallet' IS the opt-in — pass evmWalletConfig
6022
6216
  // to point at a specific wallet or set chainId/domain.
6023
6217
  const evmWalletConfig = ((_a = config.evmWalletConfig) !== null && _a !== void 0 ? _a : {});
6024
- const { InjectedEvmWalletProvider } = await import('./injected-evm-wallet-provider-BPkcFSQX.mjs');
6218
+ const { InjectedEvmWalletProvider } = await import('./injected-evm-wallet-provider-pI1JMr7g.mjs');
6025
6219
  currentAuthProvider = new InjectedEvmWalletProvider(evmWalletConfig);
6026
6220
  break;
6027
6221
  }
@@ -6046,7 +6240,7 @@ async function getAuthProvider(config) {
6046
6240
  break;
6047
6241
  case "guest":
6048
6242
  // Anonymous auth: a device-local ed25519 keypair signs the challenge.
6049
- currentAuthProvider = new (await import('./guest-auth-provider-BiWPelOD.mjs')).GuestAuthProvider();
6243
+ currentAuthProvider = new (await import('./guest-auth-provider-CF1zahC4.mjs')).GuestAuthProvider();
6050
6244
  break;
6051
6245
  }
6052
6246
  if (!currentAuthProvider) {
@@ -6108,7 +6302,7 @@ async function signInAnonymously() {
6108
6302
  // Lazy-load the guest provider so its ed25519/web3 dependencies stay out of
6109
6303
  // the main bundle for data-only apps. Specifier is a static string literal
6110
6304
  // so bundle-splitting can see it.
6111
- const provider = new (await import('./guest-auth-provider-BiWPelOD.mjs')).GuestAuthProvider();
6305
+ const provider = new (await import('./guest-auth-provider-CF1zahC4.mjs')).GuestAuthProvider();
6112
6306
  setAuthLoading(true); // reflect "signing in…" while the guest keypair session is minted
6113
6307
  // adoptSessionProvider awaits only getConfig() (already resolved here) and then
6114
6308
  // runs every write - provider/method/config/marker/user - in one synchronous
@@ -6279,7 +6473,7 @@ function ensureSharedWalletLoginReady() {
6279
6473
  if (!sharedWalletReady) {
6280
6474
  sharedWalletReady = (async () => {
6281
6475
  await getConfig();
6282
- await import('./injected-wallet-provider-BGeNaoEY.mjs');
6476
+ await import('./injected-wallet-provider-DNZA1IJ9.mjs');
6283
6477
  })().catch((error) => {
6284
6478
  sharedWalletReady = null;
6285
6479
  throw error;
@@ -6382,7 +6576,7 @@ async function loginWithWallet(options) {
6382
6576
  await ensureSharedWalletLoginReady();
6383
6577
  else
6384
6578
  await ensureWalletLoginReady();
6385
- const { InjectedWalletProvider } = await import('./injected-wallet-provider-BGeNaoEY.mjs');
6579
+ const { InjectedWalletProvider } = await import('./injected-wallet-provider-DNZA1IJ9.mjs');
6386
6580
  // Keep the RAW wallet provider: the offchain wrapper forwards only a fixed
6387
6581
  // set of methods, so cleanup below must speak to the provider that actually
6388
6582
  // holds the config rather than to whatever is wrapping it.
@@ -6635,13 +6829,14 @@ var index = /*#__PURE__*/Object.freeze({
6635
6829
  WalletConfigError: WalletConfigError,
6636
6830
  clearIncompatibleSession: clearIncompatibleSession,
6637
6831
  completeLoginFromRedirect: completeLoginFromRedirect,
6638
- completeLoginInPopup: completeLoginInPopup,
6832
+ completePendingLogin: completePendingLogin,
6639
6833
  ensureSharedWalletLoginReady: ensureSharedWalletLoginReady,
6640
6834
  ensureWalletLoginReady: ensureWalletLoginReady,
6641
6835
  getAuthProvider: getAuthProvider,
6642
6836
  getConfiguredSolanaNetwork: getConfiguredSolanaNetwork,
6643
6837
  getCurrentAuthMethod: getCurrentAuthMethod,
6644
6838
  getInitConfig: getInitConfig,
6839
+ getLoginReturnError: getLoginReturnError,
6645
6840
  isEmailRequired: isEmailRequired,
6646
6841
  isWalletLoginDisabled: isWalletLoginDisabled,
6647
6842
  isWalletLoginEnabled: isWalletLoginEnabled,
@@ -6718,6 +6913,10 @@ let authLoadingListeners = [];
6718
6913
  let initCompleted = false;
6719
6914
  let isAuthLoading = false;
6720
6915
  let authReadyPromise = null;
6916
+ let initializingConfig = null;
6917
+ let initializationGeneration = 0;
6918
+ /** Internal guard for a login result arriving after the app reconfigured the SDK. */
6919
+ function getInitializationGeneration() { return initializationGeneration; }
6721
6920
  // This file acts as a middleware for the global state of the SDK
6722
6921
  // This mostly involves setting up the AuthProvider and managing the current user
6723
6922
  async function initialize(newConfig) {
@@ -6742,7 +6941,20 @@ async function initialize(newConfig) {
6742
6941
  // Get auth provider first
6743
6942
  authProviderInstance = await getAuthProvider(newConfig);
6744
6943
  // Initialize config with auth provider
6745
- await init$1(Object.assign(Object.assign({}, newConfig), { authProvider: authProviderInstance }));
6944
+ await init$2(Object.assign(Object.assign({}, newConfig), { authProvider: authProviderInstance }));
6945
+ // A return establishes its own provider/session, or hands off a popup.
6946
+ // Restoring the old provider afterwards would overwrite the new login.
6947
+ try {
6948
+ if ((await completePendingLogin()).handled) {
6949
+ initCompleted = true;
6950
+ return;
6951
+ }
6952
+ }
6953
+ catch (_a) {
6954
+ // A cancelled/failed return is a login failure, not a broken SDK.
6955
+ // The completion layer keeps it in getLoginReturnError(); restore
6956
+ // any existing session and leave sign-in/readiness usable for retry.
6957
+ }
6746
6958
  // Now that core is initialized, update the auth method on the config
6747
6959
  const authMethod = getCurrentAuthMethod();
6748
6960
  if (authMethod) {
@@ -6774,7 +6986,42 @@ async function initialize(newConfig) {
6774
6986
  setAuthLoading(false);
6775
6987
  }
6776
6988
  }
6777
- function init(newConfig) {
6989
+ // Compare data options by value, but preserve identity for callbacks and class
6990
+ // instances such as providers. Comparing only app/endpoints would silently drop
6991
+ // a concurrent change to wallet policy or provider configuration.
6992
+ function sameConfigValue(a, b, seen = new WeakMap()) {
6993
+ var _a;
6994
+ if (Object.is(a, b))
6995
+ return true;
6996
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object')
6997
+ return false;
6998
+ if (Array.isArray(a) !== Array.isArray(b))
6999
+ return false;
7000
+ if (Array.isArray(a) && a.length !== b.length)
7001
+ return false;
7002
+ if (!Array.isArray(a)) {
7003
+ for (const value of [a, b]) {
7004
+ const prototype = Object.getPrototypeOf(value);
7005
+ if (prototype !== null && ((_a = prototype.constructor) === null || _a === void 0 ? void 0 : _a.name) !== 'Object')
7006
+ return false;
7007
+ }
7008
+ }
7009
+ if (seen.has(a))
7010
+ return seen.get(a) === b;
7011
+ seen.set(a, b);
7012
+ const left = Object.keys(a), right = Object.keys(b);
7013
+ return left.length === right.length && left.every((key) => Object.prototype.hasOwnProperty.call(b, key)
7014
+ && sameConfigValue(a[key], b[key], seen));
7015
+ }
7016
+ function init$1(newConfig) {
7017
+ if (initializingConfig) {
7018
+ if (sameConfigValue(initializingConfig, newConfig)) {
7019
+ return authReadyPromise;
7020
+ }
7021
+ return Promise.reject(new Error('SDK initialization is in progress. Await init() before changing its configuration.'));
7022
+ }
7023
+ initializingConfig = Object.assign({}, newConfig);
7024
+ initializationGeneration++;
6778
7025
  let resolveReady;
6779
7026
  let rejectReady;
6780
7027
  const ready = new Promise((resolve, reject) => {
@@ -6785,7 +7032,7 @@ function init(newConfig) {
6785
7032
  // auth-loading listener to call waitForAuthReady() synchronously when init()
6786
7033
  // announces that session restoration has begun.
6787
7034
  authReadyPromise = ready;
6788
- void initialize(newConfig).then(resolveReady, rejectReady);
7035
+ void initialize(initializingConfig).then(() => { initializingConfig = null; resolveReady(); }, (error) => { initializingConfig = null; rejectReady(error); });
6789
7036
  return ready;
6790
7037
  }
6791
7038
  /**
@@ -6908,6 +7155,38 @@ function setAuthProviderInstance(provider) {
6908
7155
  authProviderInstance = provider;
6909
7156
  }
6910
7157
 
7158
+ /** The text OpenApps templates carried where an app id belongs. Never a real id. */
7159
+ const APP_ID_PLACEHOLDER = 'REPLACE_WITH_MAIN_APP_ID';
7160
+ const NETWORKS = new Set(['bounded', 'bounded-staging', 'bounded-production']);
7161
+ // Bounded hosting states which app a page is: `<meta name="bounded-app-id">`
7162
+ // and `<meta name="bounded-network">`, first in <head> of every page it serves,
7163
+ // so the first tag of each name is the host's (docs/app-identity.md). Outside a
7164
+ // browser there is no page to read.
7165
+ function pageMeta(name) {
7166
+ var _a, _b;
7167
+ if (typeof document === 'undefined' || typeof document.querySelector !== 'function')
7168
+ return undefined;
7169
+ 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();
7170
+ return value || undefined;
7171
+ }
7172
+ /**
7173
+ * The configuration init() runs with. An explicit appId keeps the call exactly
7174
+ * as written. Without one (or with the template placeholder), a page served by
7175
+ * Bounded supplies its app id, and its network unless the call names one, so
7176
+ * code in any framework can call init() without carrying an id in its source.
7177
+ */
7178
+ function withHostedIdentity(config) {
7179
+ if (config.appId && config.appId !== APP_ID_PLACEHOLDER)
7180
+ return config;
7181
+ const appId = pageMeta('bounded-app-id');
7182
+ if (!appId || appId === APP_ID_PLACEHOLDER) {
7183
+ throw new Error('No app ID provided. Pass init({ appId }), or serve this page from Bounded hosting, '
7184
+ + 'which states it in <meta name="bounded-app-id">.');
7185
+ }
7186
+ const network = config.network === undefined ? pageMeta('bounded-network') : undefined;
7187
+ return Object.assign(Object.assign(Object.assign({}, config), { appId }), (network && NETWORKS.has(network) ? { network: network } : {}));
7188
+ }
7189
+
6911
7190
  // bounded-widget-ownership.ts - per-open ownership generation for the singleton
6912
7191
  // login/signing modal (bounded-widget-modal.ts).
6913
7192
  //
@@ -7512,7 +7791,7 @@ function runOtpFrame(run) {
7512
7791
  // Backpack) via loginWithWallet(), run ENTIRELY on the app page (injected
7513
7792
  // providers only exist there, never inside a cross-origin frame).
7514
7793
  //
7515
- // Redirect stays available as the popup-blocked fallback (loginWithRedirect).
7794
+ // Mobile uses a full-page redirect; desktop keeps a popup with redirect fallback.
7516
7795
  // The same modal is reused for Turnkey wallet signing, so login -> first
7517
7796
  // signature is one continuous surface.
7518
7797
  // Palette + type ladder mirror the hosted auth page (login-page.ts): dark editorial
@@ -7755,6 +8034,16 @@ async function prepareAndOpen(opts, pending) {
7755
8034
  var _a, _b, _c, _d, _e, _f;
7756
8035
  // Imported lazily to avoid a static cycle with the auth index barrel.
7757
8036
  const authApi = await untilCancelled(Promise.resolve().then(function () { return index; }), pending);
8037
+ const hostedLogin = (options) => {
8038
+ if (detectMobile())
8039
+ return authApi.loginWithRedirect(options);
8040
+ // Invoke immediately inside the click handler to retain popup activation.
8041
+ return authApi.loginWithPopup(options).catch((error) => {
8042
+ if (shouldFallbackToRedirect(error))
8043
+ return authApi.loginWithRedirect(options);
8044
+ throw error;
8045
+ });
8046
+ };
7758
8047
  // BEFORE any side effect. Registering the mobile wallet from an empty config
7759
8048
  // would take the chainless mainnet default, and that registration cannot be
7760
8049
  // withdrawn - so opening the widget before init() would pin the page to the
@@ -7848,6 +8137,7 @@ async function prepareAndOpen(opts, pending) {
7848
8137
  // asserts rather than fixing a bug - which is exactly why it is worth keeping.
7849
8138
  pendingOpens.delete(pending);
7850
8139
  return new Promise((resolve, reject) => {
8140
+ var _a;
7851
8141
  // Per-open ownership generation (DS3-0402): a later open bumps the modal's
7852
8142
  // generation, so this open's token stops matching and all of its callbacks
7853
8143
  // (backdrop, success, cancel, and the async view swap) no-op instead of
@@ -8066,23 +8356,20 @@ async function prepareAndOpen(opts, pending) {
8066
8356
  loginHint: input.value,
8067
8357
  redirectUri: opts.redirectUri,
8068
8358
  };
8069
- authApi
8359
+ hostedLogin(emailOpts)
8070
8360
  // A hosted login that RESOLVES minted a real session, so it settles
8071
8361
  // the card even if another lane has since taken it - dropping it
8072
8362
  // would leave the app signed in behind a widget that says it is not.
8073
8363
  // Only the failure path belongs to one attempt's UI.
8074
- .loginWithPopup(emailOpts)
8075
- .then((u) => { settleAttempt(attempt); succeed(u); })
8076
- .catch((err) => {
8077
- // Popup unavailable: same-tab redirect instead. It resolves with
8078
- // nothing when it is about to navigate, so the only thing that
8079
- // comes back here is a REJECTION - which must still reach the
8080
- // handler below, or the lane stays locked with nothing running.
8081
- if (shouldFallbackToRedirect(err)) {
8082
- return authApi.loginWithRedirect(emailOpts).then((u) => { if (u)
8083
- succeed(u); });
8364
+ .then((u) => {
8365
+ if (u) {
8366
+ settleAttempt(attempt);
8367
+ succeed(u);
8368
+ }
8369
+ else if (settleAttempt(attempt)) {
8370
+ submit.disabled = false;
8371
+ submit.textContent = "Continue";
8084
8372
  }
8085
- throw err;
8086
8373
  })
8087
8374
  .catch((err) => {
8088
8375
  if (!settleAttempt(attempt))
@@ -8119,15 +8406,16 @@ async function prepareAndOpen(opts, pending) {
8119
8406
  }
8120
8407
  setBusy(b, "Opening secure sign-in\u2026");
8121
8408
  const socialOpts = { provider: method, redirectUri: opts.redirectUri };
8122
- authApi
8123
- .loginWithPopup(socialOpts)
8124
- .then((u) => { settleAttempt(attempt); succeed(u); })
8125
- .catch((err) => {
8126
- if (shouldFallbackToRedirect(err)) {
8127
- return authApi.loginWithRedirect(socialOpts).then((u) => { if (u)
8128
- succeed(u); });
8409
+ hostedLogin(socialOpts)
8410
+ .then((u) => {
8411
+ if (u) {
8412
+ settleAttempt(attempt);
8413
+ succeed(u);
8414
+ }
8415
+ else if (settleAttempt(attempt)) {
8416
+ b.disabled = false;
8417
+ setLabel(b, method);
8129
8418
  }
8130
- throw err;
8131
8419
  })
8132
8420
  .catch((err) => {
8133
8421
  if (!settleAttempt(attempt))
@@ -8391,7 +8679,7 @@ async function prepareAndOpen(opts, pending) {
8391
8679
  fitPanelToContent(m);
8392
8680
  });
8393
8681
  };
8394
- swap(renderChoose());
8682
+ swap(renderChoose((_a = authApi.getLoginReturnError()) === null || _a === void 0 ? void 0 : _a.message));
8395
8683
  showModal(m);
8396
8684
  });
8397
8685
  }
@@ -10685,5 +10973,21 @@ async function connectServiceSession(ticket, options) {
10685
10973
  }
10686
10974
  }
10687
10975
 
10688
- export { onrampStatus as $, getAuthProvider as A, signInAnonymously as B, loginWithPrivy as C, loginWithWallet as D, ensureWalletLoginReady as E, ensureSharedWalletLoginReady as F, preloadWalletLogin as G, loginWithRedirect as H, completeLoginFromRedirect as I, loginWithPopup as J, completeLoginInPopup as K, openBoundedWidget as L, ensureSignedIn as M, useAuth as N, useQuery as O, useBuildRun as P, apps as Q, signSolanaMessageViaTurnkey as R, getOrCreateTurnkeyWallet as S, openTurnkeyKeyExport as T, getTurnkeyWalletSessionState as U, getTurnkeyWalletRecoveryUrl as V, WalletConfigError as W, startTurnkeyEmailLogin as X, getIdToken as Y, onramp as Z, onrampQuote as _, signMessageWithTurnkey as a, setPlatform as a0, OffchainAuthProvider as a1, PrivyExpoProvider as a2, requestEip6963Providers as a3, connectBroadcast as a4, connectServiceSession as a5, builds as a6, BuildsApiError as a7, mediaAgentTools as a8, NON_TERMINAL_STATES as a9, TERMINAL_STATES as aa, RUN_STATES as ab, PARK_REASONS as ac, FAILURE_REASONS as ad, BUILD_REVIEW_CAPABILITY_SCHEMA as ae, BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA as af, BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA as ag, signTransactionWithTurnkey as b, signTransactionV1WithTurnkey as c, signAndSubmitTransactionWithTurnkey as d, bs58 as e, defaultInjectedProvider as f, getPlatform as g, hasTurnkeyWallet as h, firstStandardWalletProvider as i, setAuthLoading as j, bufferExports as k, setStoredAuthMethod as l, confirmAndCheckTransaction as m, normalizeSolanaRpcUrl as n, defaultInjectedEvmProvider as o, init as p, getCurrentUser as q, resolveSolanaRpcUrl as r, setCurrentUser as s, transactionHasSignature as t, onAuthStateChanged as u, onAuthLoadingChanged as v, getAuthLoading as w, waitForAuthReady as x, login as y, logout as z };
10689
- //# sourceMappingURL=index-DKkAO0g-.mjs.map
10976
+ /**
10977
+ * Initialize the SDK. A page served by Bounded already states which app it is,
10978
+ * so `init()` there needs no `appId`; pass one to run anywhere else (another
10979
+ * host, local development). An explicit `appId` is used exactly as given.
10980
+ */
10981
+ function init(config = {}) {
10982
+ let resolved;
10983
+ try {
10984
+ resolved = withHostedIdentity(config);
10985
+ }
10986
+ catch (error) {
10987
+ return Promise.reject(error);
10988
+ }
10989
+ return init$1(resolved);
10990
+ }
10991
+
10992
+ export { onrampStatus as $, getAuthProvider as A, signInAnonymously as B, loginWithPrivy as C, loginWithWallet as D, ensureWalletLoginReady as E, ensureSharedWalletLoginReady as F, preloadWalletLogin as G, loginWithRedirect as H, completeLoginFromRedirect as I, getLoginReturnError as J, loginWithPopup as K, openBoundedWidget as L, ensureSignedIn as M, useAuth as N, useQuery as O, useBuildRun as P, apps as Q, signSolanaMessageViaTurnkey as R, getOrCreateTurnkeyWallet as S, openTurnkeyKeyExport as T, getTurnkeyWalletSessionState as U, getTurnkeyWalletRecoveryUrl as V, WalletConfigError as W, startTurnkeyEmailLogin as X, getIdToken as Y, onramp as Z, onrampQuote as _, signMessageWithTurnkey as a, setPlatform as a0, OffchainAuthProvider as a1, PrivyExpoProvider as a2, requestEip6963Providers as a3, connectBroadcast as a4, connectServiceSession as a5, builds as a6, BuildsApiError as a7, mediaAgentTools as a8, NON_TERMINAL_STATES as a9, TERMINAL_STATES as aa, RUN_STATES as ab, PARK_REASONS as ac, FAILURE_REASONS as ad, BUILD_REVIEW_CAPABILITY_SCHEMA as ae, BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA as af, BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA as ag, signTransactionWithTurnkey as b, signTransactionV1WithTurnkey as c, signAndSubmitTransactionWithTurnkey as d, bs58 as e, defaultInjectedProvider as f, getPlatform as g, hasTurnkeyWallet as h, firstStandardWalletProvider as i, setAuthLoading as j, bufferExports as k, setStoredAuthMethod as l, confirmAndCheckTransaction as m, normalizeSolanaRpcUrl as n, defaultInjectedEvmProvider as o, init as p, getCurrentUser as q, resolveSolanaRpcUrl as r, setCurrentUser as s, transactionHasSignature as t, onAuthStateChanged as u, onAuthLoadingChanged as v, getAuthLoading as w, waitForAuthReady as x, login as y, logout as z };
10993
+ //# sourceMappingURL=index-nVYK3QUO.mjs.map