@bounded-sh/client 0.0.98 → 0.0.99

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 (67) hide show
  1. package/dist/auth/bounded-widget-modal.d.ts +7 -2
  2. package/dist/auth/oidc-auth.d.ts +1 -1
  3. package/dist/auth/session-provider.d.ts +1 -0
  4. package/dist/auth/turnkey-auth.d.ts +7 -1
  5. package/dist/auth/turnkey-signer-bridge.d.ts +8 -5
  6. package/dist/auth/turnkey-signer-frame.d.ts +4 -1
  7. package/dist/auth/turnkey-wallet-context.d.ts +24 -0
  8. package/dist/auth/turnkey-wallet-widget.d.ts +2 -0
  9. package/dist/builds/client.d.ts +5 -2
  10. package/dist/builds/types.d.ts +108 -1
  11. package/dist/{email-auth-provider-DxB_pJKO.js → email-auth-provider-62OK5X30.js} +2 -2
  12. package/dist/{email-auth-provider-DxB_pJKO.js.map → email-auth-provider-62OK5X30.js.map} +1 -1
  13. package/dist/{email-auth-provider-DF7Urh8i.mjs → email-auth-provider-BKYVi3dl.mjs} +2 -2
  14. package/dist/{email-auth-provider-CiPAD3vz.mjs.map → email-auth-provider-BKYVi3dl.mjs.map} +1 -1
  15. package/dist/{email-auth-provider-CiPAD3vz.mjs → email-auth-provider-CIRbOSM3.mjs} +2 -2
  16. package/dist/{email-auth-provider-DF7Urh8i.mjs.map → email-auth-provider-CIRbOSM3.mjs.map} +1 -1
  17. package/dist/{email-auth-provider-BhYzRN0r.js → email-auth-provider-Cpj8hIFu.js} +2 -2
  18. package/dist/{email-auth-provider-BhYzRN0r.js.map → email-auth-provider-Cpj8hIFu.js.map} +1 -1
  19. package/dist/{guest-auth-provider-BiM35KHb.js → guest-auth-provider-0PjGFGDm.js} +2 -2
  20. package/dist/{guest-auth-provider-BiM35KHb.js.map → guest-auth-provider-0PjGFGDm.js.map} +1 -1
  21. package/dist/{guest-auth-provider-B9C3Iub_.js → guest-auth-provider-B6dJixRm.js} +2 -2
  22. package/dist/{guest-auth-provider-B9C3Iub_.js.map → guest-auth-provider-B6dJixRm.js.map} +1 -1
  23. package/dist/{guest-auth-provider-Dtsq4QaY.mjs → guest-auth-provider-C23vWrOT.mjs} +2 -2
  24. package/dist/{guest-auth-provider-DVw-tITC.mjs.map → guest-auth-provider-C23vWrOT.mjs.map} +1 -1
  25. package/dist/{guest-auth-provider-DVw-tITC.mjs → guest-auth-provider-E7Fg7-E2.mjs} +2 -2
  26. package/dist/{guest-auth-provider-Dtsq4QaY.mjs.map → guest-auth-provider-E7Fg7-E2.mjs.map} +1 -1
  27. package/dist/{index-CYdYsf8s.js → index-BrU_tGd-.js} +1148 -597
  28. package/dist/index-BrU_tGd-.js.map +1 -0
  29. package/dist/{index-Cflemdaf.mjs → index-l5E4pjpt.mjs} +1145 -598
  30. package/dist/index-l5E4pjpt.mjs.map +1 -0
  31. package/dist/index.d.ts +6 -2
  32. package/dist/index.js +5 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/index.mjs +1 -1
  35. package/dist/{index.native-BZOWS_sR.js → index.native-CllNqdoy.js} +442 -81
  36. package/dist/index.native-CllNqdoy.js.map +1 -0
  37. package/dist/{index.native-gVbAsohv.mjs → index.native-r1I-9E8e.mjs} +442 -81
  38. package/dist/index.native-r1I-9E8e.mjs.map +1 -0
  39. package/dist/index.native.js +1 -1
  40. package/dist/index.native.mjs +1 -1
  41. package/dist/{injected-evm-wallet-provider-C3MwUTRd.js → injected-evm-wallet-provider-BXM8TfS3.js} +2 -2
  42. package/dist/{injected-evm-wallet-provider-C3MwUTRd.js.map → injected-evm-wallet-provider-BXM8TfS3.js.map} +1 -1
  43. package/dist/{injected-evm-wallet-provider-CMqUqqxN.mjs → injected-evm-wallet-provider-BmDPRV5L.mjs} +2 -2
  44. package/dist/{injected-evm-wallet-provider-CMqUqqxN.mjs.map → injected-evm-wallet-provider-BmDPRV5L.mjs.map} +1 -1
  45. package/dist/{injected-evm-wallet-provider-Bv8VLuB1.js → injected-evm-wallet-provider-CO_aC4TY.js} +2 -2
  46. package/dist/{injected-evm-wallet-provider-Bv8VLuB1.js.map → injected-evm-wallet-provider-CO_aC4TY.js.map} +1 -1
  47. package/dist/{injected-evm-wallet-provider-K0j697jt.mjs → injected-evm-wallet-provider-CkhN5u3d.mjs} +2 -2
  48. package/dist/{injected-evm-wallet-provider-K0j697jt.mjs.map → injected-evm-wallet-provider-CkhN5u3d.mjs.map} +1 -1
  49. package/dist/{injected-wallet-provider-BDt4CN0b.js → injected-wallet-provider-B9eU7t8o.js} +2 -2
  50. package/dist/{injected-wallet-provider-BDt4CN0b.js.map → injected-wallet-provider-B9eU7t8o.js.map} +1 -1
  51. package/dist/{injected-wallet-provider-CG3oKdp_.js → injected-wallet-provider-Bi_mB_q3.js} +2 -2
  52. package/dist/{injected-wallet-provider-CG3oKdp_.js.map → injected-wallet-provider-Bi_mB_q3.js.map} +1 -1
  53. package/dist/{injected-wallet-provider-DH8yLpyf.mjs → injected-wallet-provider-BlSdRbgq.mjs} +2 -2
  54. package/dist/{injected-wallet-provider-DH8yLpyf.mjs.map → injected-wallet-provider-BlSdRbgq.mjs.map} +1 -1
  55. package/dist/{injected-wallet-provider-Dlo96aV9.mjs → injected-wallet-provider-yl51pLBa.mjs} +2 -2
  56. package/dist/{injected-wallet-provider-Dlo96aV9.mjs.map → injected-wallet-provider-yl51pLBa.mjs.map} +1 -1
  57. package/dist/services/broadcast.d.ts +28 -0
  58. package/dist/services/session.d.ts +28 -0
  59. package/package.json +7 -3
  60. package/dist/bounded-widget-modal-C0Z7gmxH.mjs +0 -31
  61. package/dist/bounded-widget-modal-C0Z7gmxH.mjs.map +0 -1
  62. package/dist/bounded-widget-modal-s0PL86Y-.js +0 -33
  63. package/dist/bounded-widget-modal-s0PL86Y-.js.map +0 -1
  64. package/dist/index-CYdYsf8s.js.map +0 -1
  65. package/dist/index-Cflemdaf.mjs.map +0 -1
  66. package/dist/index.native-BZOWS_sR.js.map +0 -1
  67. package/dist/index.native-gVbAsohv.mjs.map +0 -1
@@ -1,6 +1,53 @@
1
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';
2
2
  import * as React from 'react';
3
3
 
4
+ function walletContextFromClaims(claims, baseUrl, expectedAppId, tenantOrigin) {
5
+ const base = baseUrl.replace(/\/$/, '');
6
+ const scope = claims['custom:walletScope'];
7
+ const protocol = claims['custom:walletProtocol'];
8
+ if (scope !== undefined && scope !== 'legacy' && scope !== 'app')
9
+ throw new Error('wallet_scope_invalid');
10
+ if ((scope === 'app' && protocol !== 2) || (scope === 'legacy' && protocol !== 1)
11
+ || (scope === undefined && protocol !== undefined))
12
+ throw new Error('wallet_protocol_invalid');
13
+ const appId = claims['custom:appId'];
14
+ const userId = claims['custom:userId'];
15
+ const address = claims['custom:walletAddress'];
16
+ if (scope === 'app' && (appId !== expectedAppId || typeof appId !== 'string'
17
+ || !/^[a-f0-9]{24}$/.test(appId) || typeof userId !== 'string' || !userId
18
+ || claims.iss !== base))
19
+ throw new Error('wallet_session_context_invalid');
20
+ const resolvedScope = scope === 'app' ? 'app' : 'legacy';
21
+ const route = resolvedScope === 'app' ? `/wallet/turnkey/app/${appId}` : '/wallet/turnkey';
22
+ return {
23
+ scope: resolvedScope, appId: String(appId || expectedAppId), userId: String(userId || ''),
24
+ address: typeof address === 'string' ? address : '', base, origin: new URL(base).origin,
25
+ url: `${base}${route}/signer${resolvedScope === 'app' ? `?origin=${encodeURIComponent(tenantOrigin)}` : ''}`,
26
+ key: JSON.stringify([base, appId || expectedAppId, userId || '', address || '', resolvedScope]),
27
+ };
28
+ }
29
+ let loginGeneration = 0;
30
+ function invalidatePendingWalletLogin() { ++loginGeneration; }
31
+ function walletOperationGuard() {
32
+ const generation = loginGeneration;
33
+ return () => {
34
+ if (generation !== loginGeneration)
35
+ throw new Error('wallet_session_changed');
36
+ };
37
+ }
38
+ // This realm owns one active SDK session and one retained signer frame.
39
+ let activeWalletState;
40
+ function setWalletSessionState(key, state) { activeWalletState = { key, state }; }
41
+ function walletSessionState(key) {
42
+ if ((activeWalletState === null || activeWalletState === void 0 ? void 0 : activeWalletState.key) !== key)
43
+ return { status: 'verification-required' };
44
+ const state = activeWalletState.state;
45
+ if (state.status === 'ready' && (!Number.isFinite(state.expiresAt) || state.expiresAt <= Date.now())) {
46
+ return { status: 'verification-required', reason: 'wallet_session_expired' };
47
+ }
48
+ return state;
49
+ }
50
+
4
51
  /**
5
52
  * Platform abstraction layer for React Native compatibility.
6
53
  *
@@ -944,7 +991,10 @@ function registerAuthRegistrySync(fn) {
944
991
  * Returns the normalized user published by this commit, or null when omitted.
945
992
  */
946
993
  async function adoptSessionProvider(opts) {
994
+ var _a;
947
995
  const coreConfig = await getConfig();
996
+ (_a = opts.beforeCommit) === null || _a === void 0 ? void 0 : _a.call(opts);
997
+ invalidatePendingWalletLogin();
948
998
  coreConfig.authProvider = opts.provider;
949
999
  coreConfig.authMethod = opts.method;
950
1000
  setAuthProviderInstance(opts.provider);
@@ -4178,148 +4228,560 @@ async function ensureSolanaMobileWalletRegistered(config, network) {
4178
4228
 
4179
4229
  var bufferExports = requireBuffer();
4180
4230
 
4181
- // turnkey-signer-frame.ts - a HIDDEN, offscreen iframe of the Bounded signer page
4182
- // for UI-less signer exchanges: sealing a login code (bounded:turnkey:encryptOtp),
4183
- // establishing the 24h signing session right after login
4184
- // (bounded:turnkey:establishSigningSession), and destroying it on logout
4185
- // (bounded:turnkey:clearSigningSession).
4231
+ // bounded-widget-modal.ts - the shared in-app WIDGET shell for Bounded's login
4232
+ // and signing surfaces. One Shadow-DOM overlay + panel that matches the Bounded
4233
+ // widget (Seal) look and MOTION: a backdrop fade, a panel rise on desktop, a
4234
+ // bottom-sheet on mobile, a height tween as content swaps, and a clean close.
4186
4235
  //
4187
- // One handle = one live frame. The frame is kept across requests so multi-step
4188
- // exchanges (seal a code, then establish the session with the keypair the SEAL
4189
- // left in that exact page instance) talk to the same page. Origin + source +
4190
- // requestId are all pinned; dispose() tears everything down.
4236
+ // It hosts EITHER:
4237
+ // - NATIVE content (the login card the SDK renders itself), or
4238
+ // - an IFRAME (the Bounded-owned signer page, which must run on the single
4239
+ // signer origin because the wallet's 24h signing session lives in that
4240
+ // origin's storage, out of every app's reach).
4241
+ //
4242
+ // The modal is a warm singleton: it stays mounted (hidden) between uses so a
4243
+ // second open - or a signature right after login - is instant and, when it is
4244
+ // already open, a clean in-place view swap rather than a reopen.
4191
4245
  function hasDOM$5() {
4192
- return typeof window !== 'undefined' && typeof document !== 'undefined';
4246
+ return typeof window !== "undefined" && typeof document !== "undefined";
4193
4247
  }
4194
- async function signerPage() {
4195
- const cfg = await getConfig();
4196
- const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
4197
- return { url: `${base}/wallet/turnkey/signer`, origin: new URL(base).origin };
4248
+ const OVERLAY_CSS = `
4249
+ :host{all:initial}
4250
+ *{box-sizing:border-box}
4251
+ .bw-ov{position:fixed;inset:0;z-index:2147483647;display:none;
4252
+ font-family:"Schibsted Grotesk","Helvetica Neue",Arial,sans-serif}
4253
+ .bw-ov.mounted{display:block}
4254
+ .bw-back{position:fixed;inset:0;background:rgba(4,4,5,.62);opacity:0;
4255
+ transition:opacity .24s ease;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}
4256
+ .bw-wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:20px}
4257
+ .bw-panel{position:relative;width:420px;max-width:calc(100vw - 28px);height:280px;
4258
+ border-radius:14px;overflow:hidden;background:#0e0f11;border:1px solid rgba(232,230,222,.08);
4259
+ box-shadow:0 1px 0 rgba(232,230,222,.03) inset,0 40px 90px -45px rgba(0,0,0,.85);
4260
+ opacity:0;transform:translateY(14px) scale(.98);will-change:transform,opacity,height;
4261
+ transition:opacity .28s cubic-bezier(.2,.8,.2,1),transform .28s cubic-bezier(.2,.8,.2,1),
4262
+ height .3s cubic-bezier(.2,.8,.2,1)}
4263
+ .bw-ov.show .bw-back{opacity:1}
4264
+ .bw-ov.show .bw-panel{opacity:1;transform:none}
4265
+ .bw-ov.closing .bw-back{opacity:0}
4266
+ .bw-ov.closing .bw-panel{opacity:0;transform:translateY(12px) scale(.99)}
4267
+ .bw-content{position:absolute;inset:0;overflow:hidden}
4268
+ .bw-frame{display:block;width:100%;height:100%;border:0;background:transparent;color-scheme:dark}
4269
+ /* editorial corner crop-marks, matching the reference frame */
4270
+ .bw-mk{position:absolute;width:13px;height:13px;pointer-events:none;z-index:2;
4271
+ border-color:rgba(232,230,222,.22);border-style:solid;border-width:0}
4272
+ .bw-mk.tl{top:11px;left:11px;border-top-width:1px;border-left-width:1px}
4273
+ .bw-mk.tr{top:11px;right:11px;border-top-width:1px;border-right-width:1px}
4274
+ .bw-mk.bl{bottom:11px;left:11px;border-bottom-width:1px;border-left-width:1px}
4275
+ .bw-mk.br{bottom:11px;right:11px;border-bottom-width:1px;border-right-width:1px}
4276
+ @media(max-width:560px){
4277
+ .bw-wrap{align-items:flex-end;padding:0}
4278
+ .bw-panel{width:100%;max-width:100%;border-radius:18px 18px 0 0;border-bottom:0}
4279
+ .bw-content{overflow-y:auto}
4280
+ .bw-ov.show .bw-panel{transform:none}
4281
+ .bw-ov:not(.show) .bw-panel,.bw-ov.closing .bw-panel{transform:translateY(18%)}
4198
4282
  }
4199
- /** Mount the hidden signer frame. Callers MUST dispose() when done. */
4200
- async function openHiddenSignerFrame() {
4201
- if (!hasDOM$5())
4202
- throw new Error('The Bounded signer frame is only available in the browser.');
4203
- const { url, origin } = await signerPage();
4204
- const frame = document.createElement('iframe');
4205
- frame.setAttribute('aria-hidden', 'true');
4206
- frame.style.cssText =
4207
- 'position:fixed;width:1px;height:1px;left:-9999px;top:-9999px;border:0;opacity:0;pointer-events:none';
4208
- let disposed = false;
4209
- let readyResolve = null;
4210
- const ready = new Promise((resolve) => { readyResolve = resolve; });
4211
- const pending = new Map();
4212
- const onMsg = (event) => {
4213
- if (event.origin !== origin || event.source !== frame.contentWindow)
4283
+ @media(prefers-reduced-motion:reduce){.bw-back,.bw-panel{transition:opacity .12s linear!important}}
4284
+ `;
4285
+ let modal = null;
4286
+ function getModal() {
4287
+ if (modal)
4288
+ return modal;
4289
+ const root = document.createElement("div");
4290
+ root.setAttribute("data-bounded-widget", "");
4291
+ const shadow = root.attachShadow({ mode: "open" });
4292
+ const style = document.createElement("style");
4293
+ style.textContent = OVERLAY_CSS;
4294
+ shadow.appendChild(style);
4295
+ const overlay = document.createElement("div");
4296
+ overlay.className = "bw-ov";
4297
+ const back = document.createElement("div");
4298
+ back.className = "bw-back";
4299
+ const wrap = document.createElement("div");
4300
+ wrap.className = "bw-wrap";
4301
+ const panel = document.createElement("div");
4302
+ panel.className = "bw-panel";
4303
+ const content = document.createElement("div");
4304
+ content.className = "bw-content";
4305
+ panel.appendChild(content);
4306
+ for (const corner of ["tl", "tr", "bl", "br"]) {
4307
+ const mk = document.createElement("span");
4308
+ mk.className = "bw-mk " + corner;
4309
+ panel.appendChild(mk);
4310
+ }
4311
+ wrap.appendChild(panel);
4312
+ overlay.appendChild(back);
4313
+ overlay.appendChild(wrap);
4314
+ shadow.appendChild(overlay);
4315
+ (document.body || document.documentElement).appendChild(root);
4316
+ const m = {
4317
+ root,
4318
+ overlay,
4319
+ panel,
4320
+ content,
4321
+ iframe: null,
4322
+ iframeOrigin: "",
4323
+ iframeReady: false,
4324
+ iframeContext: "",
4325
+ onBackdrop: null,
4326
+ onIframeMessage: null,
4327
+ chain: Promise.resolve(),
4328
+ fitObserver: null,
4329
+ generation: 0,
4330
+ onSupersede: null,
4331
+ };
4332
+ // The dismiss handler lives on `.bw-wrap`: it is the full-viewport flex layer
4333
+ // that sits ABOVE `.bw-back`, so it (not the backdrop element) receives clicks
4334
+ // outside the panel. Fire only when the click lands on the wrap itself, never
4335
+ // when it bubbles up from the panel or its content.
4336
+ wrap.addEventListener("click", (event) => {
4337
+ if (event.target === wrap && m.onBackdrop)
4338
+ m.onBackdrop();
4339
+ });
4340
+ window.addEventListener("message", (event) => {
4341
+ if (!m.iframe)
4214
4342
  return;
4215
- const d = event.data;
4216
- if (!d || typeof d !== 'object')
4343
+ if (event.origin !== m.iframeOrigin)
4217
4344
  return;
4218
- if (d.type === 'bounded:turnkey:ready') {
4219
- readyResolve === null || readyResolve === void 0 ? void 0 : readyResolve();
4345
+ if (event.source !== m.iframe.contentWindow)
4220
4346
  return;
4221
- }
4222
- if (d.type === 'bounded:turnkey:result' && typeof d.requestId === 'string') {
4223
- const settle = pending.get(d.requestId);
4224
- if (settle) {
4225
- pending.delete(d.requestId);
4226
- settle(d);
4227
- }
4228
- }
4229
- };
4230
- window.addEventListener('message', onMsg);
4231
- frame.src = url;
4232
- document.body.appendChild(frame);
4233
- const dispose = () => {
4234
- if (disposed)
4347
+ const d = event.data;
4348
+ if (!d || typeof d !== "object")
4235
4349
  return;
4236
- disposed = true;
4237
- window.removeEventListener('message', onMsg);
4238
- pending.clear();
4239
- try {
4240
- frame.remove();
4241
- }
4242
- catch ( /* already gone */_a) { /* already gone */ }
4243
- };
4244
- return {
4245
- dispose,
4246
- request(message, timeoutMs) {
4247
- const requestId = String(message.requestId || '');
4248
- if (!requestId)
4249
- return Promise.reject(new Error('signer frame request needs a requestId'));
4250
- if (disposed)
4251
- return Promise.reject(new Error('signer frame disposed'));
4252
- return new Promise((resolve, reject) => {
4253
- let timer;
4254
- pending.set(requestId, (d) => {
4255
- if (timer)
4256
- clearTimeout(timer);
4257
- resolve(d);
4258
- });
4259
- timer = setTimeout(() => {
4260
- pending.delete(requestId);
4261
- reject(new Error('The Bounded signer did not respond in time.'));
4262
- }, timeoutMs);
4263
- ready.then(() => {
4264
- var _a;
4265
- if (disposed || !pending.has(requestId))
4266
- return;
4267
- try {
4268
- (_a = frame.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage(message, origin);
4269
- }
4270
- catch ( /* frame gone; timeout rejects */_b) { /* frame gone; timeout rejects */ }
4271
- });
4272
- });
4273
- },
4274
- };
4275
- }
4276
-
4277
- // turnkey-signing-capability.ts - the pure AS-02 (DS2-0112) signing-capability logic,
4278
- // kept import-free (no DOM, no session, no @bounded-sh/core) so it is unit-testable.
4279
- //
4280
- // The hosted Turnkey signer refuses to sign unless the request carries an issuer-signed
4281
- // SIGNING CAPABILITY bound to the app id, the requesting origin, and the EXACT payload
4282
- // hash. `requestTurnkeySigningCapability` mints one for the given bytes and
4283
- // `buildTurnkeySignRequest` assembles the signer request that carries it. The bridge
4284
- // (turnkey-signer-bridge.ts) supplies the base url + id token and routes through both,
4285
- // so a regression here (a dropped capability, a wrong route, a swallowed error) is
4286
- // caught without a browser.
4287
- // POST the Bounded id token + payload hash to the issuer's capability mint route and
4288
- // return the issuer-signed capability for exactly these bytes. Credential-free: identity
4289
- // is proven with the id token in the body (not an ambient cookie the cross-origin fetch
4290
- // would not carry). Throws with the same stable messages the bridge relied on inline.
4291
- async function requestTurnkeySigningCapability(opts) {
4292
- const base = opts.baseUrl.replace(/\/$/, '');
4293
- let res;
4294
- try {
4295
- res = await opts.fetchImpl(`${base}/wallet/turnkey/capability`, {
4296
- method: 'POST',
4297
- headers: { 'content-type': 'application/json' },
4298
- body: JSON.stringify({ idToken: opts.idToken, messageHex: opts.messageHex }),
4299
- });
4300
- }
4301
- catch (_a) {
4302
- throw new Error('Could not reach the Bounded signer to authorize this signature.');
4303
- }
4304
- if (!res.ok) {
4305
- let error = 'signing_capability_denied';
4306
- try {
4307
- const d = (await res.json());
4308
- if (d && typeof d.error === 'string')
4309
- error = d.error;
4310
- }
4311
- catch (_b) {
4312
- /* keep the default marker */
4313
- }
4314
- throw new Error(`Turnkey signing was not authorized (${error}).`);
4315
- }
4316
- const d = (await res.json());
4317
- if (!d || typeof d.capability !== 'string' || !d.capability) {
4318
- throw new Error('Turnkey signing capability was malformed.');
4319
- }
4320
- return d.capability;
4350
+ // Readiness belongs to the document, even while login owns the modal and no
4351
+ // signing exchange is listening. The retained frame will not load again.
4352
+ if (d.type === "bounded:turnkey:ready")
4353
+ m.iframeReady = true;
4354
+ if (m.onIframeMessage)
4355
+ m.onIframeMessage(d);
4356
+ });
4357
+ modal = m;
4358
+ return m;
4321
4359
  }
4322
- // Assemble the `bounded:turnkey:sign` postMessage request, carrying the issuer-signed
4360
+ function showModal(m) {
4361
+ m.overlay.classList.remove("closing");
4362
+ m.overlay.classList.add("mounted");
4363
+ // Force a reflow so the .show transition runs from the hidden state every open.
4364
+ void m.panel.offsetWidth;
4365
+ m.overlay.classList.add("show");
4366
+ }
4367
+ function hideModal(m, after) {
4368
+ m.overlay.classList.remove("show");
4369
+ m.overlay.classList.add("closing");
4370
+ // Capture the generation at hide time. If a newer open re-shows the modal within
4371
+ // the close tween, this stale timer must NOT strip `.mounted`/`.closing` off the
4372
+ // freshly reopened widget - only tear down when we are still the same open.
4373
+ const gen = m.generation;
4374
+ window.setTimeout(() => {
4375
+ if (m.generation === gen)
4376
+ m.overlay.classList.remove("mounted", "closing");
4377
+ }, 260);
4378
+ }
4379
+ function setPanelHeight(m, px) {
4380
+ const h = Math.max(140, Math.min(680, Math.round(px)));
4381
+ m.panel.style.height = h + "px";
4382
+ }
4383
+ // True outer height of the mounted card: the max of its padding-box scroll height
4384
+ // (catches overflowing descendants) and its border-box height plus own vertical
4385
+ // margins (catches border + margins, which scrollHeight omits). Ceiled so a
4386
+ // sub-pixel row is never clipped under `overflow:hidden`.
4387
+ function measureOuterHeight(target) {
4388
+ var _a;
4389
+ const cs = (_a = target.ownerDocument.defaultView) === null || _a === void 0 ? void 0 : _a.getComputedStyle(target);
4390
+ const marginY = cs ? parseFloat(cs.marginTop) + parseFloat(cs.marginBottom) : 0;
4391
+ const borderBox = target.offsetHeight + (Number.isFinite(marginY) ? marginY : 0);
4392
+ const rect = target.getBoundingClientRect().height;
4393
+ return Math.ceil(Math.max(target.scrollHeight, borderBox, rect));
4394
+ }
4395
+ /**
4396
+ * Measure the currently mounted native content and tween the panel to fit it.
4397
+ * Re-fits automatically when the card's intrinsic height changes - notably after
4398
+ * the Newsreader/Schibsted web fonts swap in (which grows line boxes a few px
4399
+ * after first paint) and on inline view swaps (email -> OTP code -> success).
4400
+ */
4401
+ function fitPanelToContent(m) {
4402
+ var _a, _b;
4403
+ const first = m.content.firstElementChild;
4404
+ const target = first || m.content;
4405
+ const apply = () => {
4406
+ // Only fit while this exact node is still the mounted card.
4407
+ if (m.content.firstElementChild !== first && first)
4408
+ return;
4409
+ const h = measureOuterHeight(target);
4410
+ if (h > 0)
4411
+ setPanelHeight(m, h);
4412
+ };
4413
+ apply();
4414
+ const view = target.ownerDocument.defaultView;
4415
+ // Re-fit after fonts load (one-shot) and once the ResizeObserver-less browsers
4416
+ // have painted, so the height never lags a late reflow.
4417
+ (_a = view === null || view === void 0 ? void 0 : view.requestAnimationFrame) === null || _a === void 0 ? void 0 : _a.call(view, apply);
4418
+ const fonts = target.ownerDocument.fonts;
4419
+ if (fonts === null || fonts === void 0 ? void 0 : fonts.ready)
4420
+ fonts.ready.then(apply).catch(() => { });
4421
+ (_b = m.fitObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
4422
+ m.fitObserver = null;
4423
+ if (first && typeof (view === null || view === void 0 ? void 0 : view.ResizeObserver) === "function") {
4424
+ const ro = new view.ResizeObserver(() => apply());
4425
+ ro.observe(first);
4426
+ m.fitObserver = ro;
4427
+ }
4428
+ }
4429
+ /** Switch the modal into NATIVE-content mode: detach any iframe, mount `node`. */
4430
+ function mountContent(m, node) {
4431
+ var _a;
4432
+ detachIframe(m);
4433
+ (_a = m.fitObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
4434
+ m.fitObserver = null;
4435
+ m.content.replaceChildren(node);
4436
+ m.content.style.display = "block";
4437
+ }
4438
+ /** Get (or lazily create) the warm iframe for signer/hosted-page mode. */
4439
+ function ensureIframe(m, url, origin, context = "") {
4440
+ var _a;
4441
+ if (m.iframe && m.iframeOrigin === origin && m.iframe.src === url && m.iframeContext === context)
4442
+ return m.iframe;
4443
+ detachIframe(m);
4444
+ (_a = m.fitObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
4445
+ m.fitObserver = null;
4446
+ m.content.style.display = "none";
4447
+ const iframe = document.createElement("iframe");
4448
+ iframe.className = "bw-frame";
4449
+ iframe.setAttribute("title", "Bounded");
4450
+ iframe.setAttribute("allow", "clipboard-write");
4451
+ iframe.src = url;
4452
+ m.iframeOrigin = origin;
4453
+ m.iframeContext = context;
4454
+ m.iframeReady = false;
4455
+ m.panel.appendChild(iframe);
4456
+ m.iframe = iframe;
4457
+ return iframe;
4458
+ }
4459
+ /**
4460
+ * Destroy the warm signer iframe if one is currently mounted for `origin`, and report
4461
+ * whether there was one. Logout needs this: a signing session can live in that document's
4462
+ * MEMORY (the signer keeps an in-memory mirror when the browser blocks its storage), and a
4463
+ * throwaway iframe opened by the sweep can only ever clear the shared store, never another
4464
+ * document's memory. Removing the iframe discards its browsing context, which takes the
4465
+ * mirror with it.
4466
+ *
4467
+ * Deliberately does NOT call getModal(): that would BUILD a modal just to tear it down, on
4468
+ * every logout of every app, most of which never mounted one.
4469
+ */
4470
+ function detachWarmSignerIframe(origin) {
4471
+ if (!modal || !modal.iframe || (origin))
4472
+ return false;
4473
+ detachIframe(modal);
4474
+ return true;
4475
+ }
4476
+ /** A removed frame cannot still supply its in-memory signing session. */
4477
+ function hasWarmSignerFrame(context) {
4478
+ var _a;
4479
+ return Boolean(((_a = modal === null || modal === void 0 ? void 0 : modal.iframe) === null || _a === void 0 ? void 0 : _a.isConnected) && modal.iframeContext === context);
4480
+ }
4481
+ function detachIframe(m) {
4482
+ if (m.iframe) {
4483
+ try {
4484
+ m.iframe.remove();
4485
+ }
4486
+ catch (_a) {
4487
+ /* ignore */
4488
+ }
4489
+ }
4490
+ m.iframe = null;
4491
+ m.iframeReady = false;
4492
+ m.iframeOrigin = "";
4493
+ m.iframeContext = "";
4494
+ m.onIframeMessage = null;
4495
+ }
4496
+ // Load the Bounded editorial typefaces (Newsreader display + Schibsted Grotesk body +
4497
+ // JetBrains Mono) once at the document level so they apply inside the shadow root. A
4498
+ // no-op if already present or off-DOM. Fonts loaded here match the hosted auth page.
4499
+ const FONTS_HREF = "https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght@6..72,400;6..72,500&family=Schibsted+Grotesk:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap";
4500
+ function ensureFonts() {
4501
+ if (!hasDOM$5())
4502
+ return;
4503
+ if (document.querySelector('link[data-bounded-fonts="1"]'))
4504
+ return;
4505
+ const pre1 = document.createElement("link");
4506
+ pre1.rel = "preconnect";
4507
+ pre1.href = "https://fonts.googleapis.com";
4508
+ const pre2 = document.createElement("link");
4509
+ pre2.rel = "preconnect";
4510
+ pre2.href = "https://fonts.gstatic.com";
4511
+ pre2.crossOrigin = "anonymous";
4512
+ const link = document.createElement("link");
4513
+ link.rel = "stylesheet";
4514
+ link.href = FONTS_HREF;
4515
+ link.setAttribute("data-bounded-fonts", "1");
4516
+ document.head.append(pre1, pre2, link);
4517
+ }
4518
+
4519
+ // turnkey-wallet-widget.ts - runs one Turnkey signer exchange inside the shared
4520
+ // in-app WIDGET (bounded-widget-modal.ts). The Bounded-owned signer page is loaded
4521
+ // in a warm iframe behind the modal so signing feels native: an in-app approve
4522
+ // card (with inline code entry when the 24h signing session must first be
4523
+ // established), never a jarring popup.
4524
+ //
4525
+ // WHY AN IFRAME: the wallet's signing session lives in the single Bounded signer
4526
+ // origin's storage, out of every app's reach, so signing MUST run on that origin;
4527
+ // the iframe brings it in-app.
4528
+ //
4529
+ // Restricted embedders: if the frame never loads, the SAME request reruns through
4530
+ // `popupFallback` (a real window). Callers pass a popup implementation and never
4531
+ // see the difference.
4532
+ /**
4533
+ * Run one signer exchange through the in-app widget. Resolves when `onResult`
4534
+ * accepts a result; falls back to `popupFallback` if the frame cannot be mounted
4535
+ * or never loads.
4536
+ */
4537
+ async function runWidget(url, signerOrigin, ex, popupFallback) {
4538
+ if (!hasDOM$5())
4539
+ return popupFallback();
4540
+ const m = getModal();
4541
+ const requestId = String(ex.request.requestId);
4542
+ // Serialize: the approve card is modal, and one warm frame handles one request at a time.
4543
+ const run = m.chain.then(() => new Promise((resolve, reject) => {
4544
+ var _a;
4545
+ let settled = false;
4546
+ let usedFallback = false;
4547
+ (_a = ex.assertCurrent) === null || _a === void 0 ? void 0 : _a.call(ex);
4548
+ const iframe = ensureIframe(m, url, signerOrigin, ex.context);
4549
+ const send = () => {
4550
+ var _a;
4551
+ if (!iframe.contentWindow)
4552
+ return;
4553
+ try {
4554
+ (_a = ex.assertCurrent) === null || _a === void 0 ? void 0 : _a.call(ex);
4555
+ iframe.contentWindow.postMessage(ex.request, signerOrigin);
4556
+ }
4557
+ catch (_b) {
4558
+ finish(() => reject(new Error('wallet_session_changed')));
4559
+ }
4560
+ };
4561
+ let loadTimer = 0;
4562
+ let ceilingTimer = 0;
4563
+ const finish = (fn) => {
4564
+ if (settled)
4565
+ return;
4566
+ settled = true;
4567
+ m.onIframeMessage = null;
4568
+ m.onBackdrop = null;
4569
+ window.clearTimeout(loadTimer);
4570
+ window.clearTimeout(ceilingTimer);
4571
+ fn();
4572
+ };
4573
+ // The frame is WARM across exchanges, so an abandoned approval must be told to drop its
4574
+ // in-flight ceremony. Otherwise the signer keeps `approving` set for a card nobody is
4575
+ // looking at and refuses every later create/sign as `signer_request_superseded` (the
4576
+ // BSA-2026-012 guard) until the page reloads.
4577
+ const cancelInFrame = () => {
4578
+ if (!iframe.contentWindow)
4579
+ return;
4580
+ try {
4581
+ iframe.contentWindow.postMessage({ type: "bounded:turnkey:cancel", requestId }, signerOrigin);
4582
+ }
4583
+ catch (_a) {
4584
+ /* frame gone */
4585
+ }
4586
+ };
4587
+ const goFallback = () => {
4588
+ if (usedFallback || settled)
4589
+ return;
4590
+ usedFallback = true;
4591
+ hideModal(m);
4592
+ finish(() => popupFallback().then(resolve, reject));
4593
+ };
4594
+ m.onBackdrop = () => {
4595
+ cancelInFrame();
4596
+ hideModal(m);
4597
+ finish(() => reject(new Error("cancelled")));
4598
+ };
4599
+ m.onIframeMessage = (d) => {
4600
+ if (d.type === "bounded:turnkey:ready") {
4601
+ send();
4602
+ return;
4603
+ }
4604
+ if (d.type === "bounded:turnkey:resize" && typeof d.height === "number") {
4605
+ setPanelHeight(m, d.height);
4606
+ return;
4607
+ }
4608
+ if (d.type === "bounded:turnkey:result" && d.requestId === requestId) {
4609
+ const outcome = ex.onResult(d);
4610
+ if ("error" in outcome) {
4611
+ hideModal(m);
4612
+ finish(() => reject(new Error(outcome.error)));
4613
+ return;
4614
+ }
4615
+ if (outcome.done) {
4616
+ hideModal(m);
4617
+ finish(() => resolve(outcome.value));
4618
+ }
4619
+ }
4620
+ };
4621
+ showModal(m);
4622
+ if (m.iframeReady)
4623
+ send();
4624
+ // If the frame never loads, degrade to the popup (best-effort - a post-load
4625
+ // popup may be blocked without a gesture, in which case the fallback rejects).
4626
+ loadTimer = window.setTimeout(() => {
4627
+ if (!m.iframeReady && !settled)
4628
+ goFallback();
4629
+ }, 8000);
4630
+ // Absolute ceiling so a stuck flow cannot hang forever.
4631
+ ceilingTimer = window.setTimeout(() => {
4632
+ if (!settled) {
4633
+ cancelInFrame();
4634
+ hideModal(m);
4635
+ finish(() => reject(new Error("Wallet approval timed out.")));
4636
+ }
4637
+ }, 5 * 60 * 1000);
4638
+ }));
4639
+ m.chain = run.then(() => undefined, () => undefined);
4640
+ return run;
4641
+ }
4642
+
4643
+ // turnkey-signer-frame.ts - a HIDDEN, offscreen iframe of the Bounded signer page
4644
+ // for UI-less signer exchanges: sealing a login code (bounded:turnkey:encryptOtp),
4645
+ // establishing the 24h signing session right after login
4646
+ // (bounded:turnkey:establishSigningSession), and destroying it on logout
4647
+ // (bounded:turnkey:clearSigningSession).
4648
+ //
4649
+ // One handle = one live frame. The frame is kept across requests so multi-step
4650
+ // exchanges (seal a code, then establish the session with the keypair the SEAL
4651
+ // left in that exact page instance) talk to the same page. Origin + source +
4652
+ // requestId are all pinned; dispose() tears everything down.
4653
+ function hasDOM$4() {
4654
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
4655
+ }
4656
+ async function signerPage() {
4657
+ const cfg = await getConfig();
4658
+ const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
4659
+ return { url: `${base}/wallet/turnkey/signer`, origin: new URL(base).origin };
4660
+ }
4661
+ /** Mount the hidden signer frame. Callers MUST dispose() when done. */
4662
+ async function openHiddenSignerFrame(page) {
4663
+ if (!hasDOM$4())
4664
+ throw new Error('The Bounded signer frame is only available in the browser.');
4665
+ const { url, origin } = page !== null && page !== void 0 ? page : await signerPage();
4666
+ const frame = document.createElement('iframe');
4667
+ frame.setAttribute('aria-hidden', 'true');
4668
+ frame.style.cssText =
4669
+ 'position:fixed;width:1px;height:1px;left:-9999px;top:-9999px;border:0;opacity:0;pointer-events:none';
4670
+ let disposed = false;
4671
+ let readyResolve = null;
4672
+ const ready = new Promise((resolve) => { readyResolve = resolve; });
4673
+ const pending = new Map();
4674
+ const onMsg = (event) => {
4675
+ if (event.origin !== origin || event.source !== frame.contentWindow)
4676
+ return;
4677
+ const d = event.data;
4678
+ if (!d || typeof d !== 'object')
4679
+ return;
4680
+ if (d.type === 'bounded:turnkey:ready') {
4681
+ readyResolve === null || readyResolve === void 0 ? void 0 : readyResolve();
4682
+ return;
4683
+ }
4684
+ if (d.type === 'bounded:turnkey:result' && typeof d.requestId === 'string') {
4685
+ const settle = pending.get(d.requestId);
4686
+ if (settle) {
4687
+ pending.delete(d.requestId);
4688
+ settle(d);
4689
+ }
4690
+ }
4691
+ };
4692
+ window.addEventListener('message', onMsg);
4693
+ frame.src = url;
4694
+ document.body.appendChild(frame);
4695
+ const dispose = () => {
4696
+ if (disposed)
4697
+ return;
4698
+ disposed = true;
4699
+ window.removeEventListener('message', onMsg);
4700
+ pending.clear();
4701
+ try {
4702
+ frame.remove();
4703
+ }
4704
+ catch ( /* already gone */_a) { /* already gone */ }
4705
+ };
4706
+ return {
4707
+ dispose,
4708
+ request(message, timeoutMs) {
4709
+ const requestId = String(message.requestId || '');
4710
+ if (!requestId)
4711
+ return Promise.reject(new Error('signer frame request needs a requestId'));
4712
+ if (disposed)
4713
+ return Promise.reject(new Error('signer frame disposed'));
4714
+ return new Promise((resolve, reject) => {
4715
+ let timer;
4716
+ pending.set(requestId, (d) => {
4717
+ if (timer)
4718
+ clearTimeout(timer);
4719
+ resolve(d);
4720
+ });
4721
+ timer = setTimeout(() => {
4722
+ pending.delete(requestId);
4723
+ reject(new Error('The Bounded signer did not respond in time.'));
4724
+ }, timeoutMs);
4725
+ ready.then(() => {
4726
+ var _a;
4727
+ if (disposed || !pending.has(requestId))
4728
+ return;
4729
+ try {
4730
+ (_a = frame.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage(message, origin);
4731
+ }
4732
+ catch ( /* frame gone; timeout rejects */_b) { /* frame gone; timeout rejects */ }
4733
+ });
4734
+ });
4735
+ },
4736
+ };
4737
+ }
4738
+
4739
+ // turnkey-signing-capability.ts - the pure AS-02 (DS2-0112) signing-capability logic,
4740
+ // kept import-free (no DOM, no session, no @bounded-sh/core) so it is unit-testable.
4741
+ //
4742
+ // The hosted Turnkey signer refuses to sign unless the request carries an issuer-signed
4743
+ // SIGNING CAPABILITY bound to the app id, the requesting origin, and the EXACT payload
4744
+ // hash. `requestTurnkeySigningCapability` mints one for the given bytes and
4745
+ // `buildTurnkeySignRequest` assembles the signer request that carries it. The bridge
4746
+ // (turnkey-signer-bridge.ts) supplies the base url + id token and routes through both,
4747
+ // so a regression here (a dropped capability, a wrong route, a swallowed error) is
4748
+ // caught without a browser.
4749
+ // POST the Bounded id token + payload hash to the issuer's capability mint route and
4750
+ // return the issuer-signed capability for exactly these bytes. Credential-free: identity
4751
+ // is proven with the id token in the body (not an ambient cookie the cross-origin fetch
4752
+ // would not carry). Throws with the same stable messages the bridge relied on inline.
4753
+ async function requestTurnkeySigningCapability(opts) {
4754
+ const base = opts.baseUrl.replace(/\/$/, '');
4755
+ let res;
4756
+ try {
4757
+ res = await opts.fetchImpl(`${base}/wallet/turnkey/capability`, {
4758
+ method: 'POST',
4759
+ headers: { 'content-type': 'application/json' },
4760
+ body: JSON.stringify({ idToken: opts.idToken, messageHex: opts.messageHex, walletProtocol: 2 }),
4761
+ });
4762
+ }
4763
+ catch (_a) {
4764
+ throw new Error('Could not reach the Bounded signer to authorize this signature.');
4765
+ }
4766
+ if (!res.ok) {
4767
+ let error = 'signing_capability_denied';
4768
+ try {
4769
+ const d = (await res.json());
4770
+ if (d && typeof d.error === 'string')
4771
+ error = d.error;
4772
+ }
4773
+ catch (_b) {
4774
+ /* keep the default marker */
4775
+ }
4776
+ throw new Error(`Turnkey signing was not authorized (${error}).`);
4777
+ }
4778
+ const d = (await res.json());
4779
+ if (!d || typeof d.capability !== 'string' || !d.capability) {
4780
+ throw new Error('Turnkey signing capability was malformed.');
4781
+ }
4782
+ return d.capability;
4783
+ }
4784
+ // Assemble the `bounded:turnkey:sign` postMessage request, carrying the issuer-signed
4323
4785
  // capability for the same bytes. Without the capability the hosted signer refuses
4324
4786
  // (capability_required), so it is a required field of every sign request.
4325
4787
  // `authorization` is the issuer-signed createWallet action authorization: inline
@@ -4347,7 +4809,7 @@ async function requestTurnkeyActionAuthorization(opts) {
4347
4809
  res = await opts.fetchImpl(`${base}/wallet/turnkey/action-authorization`, {
4348
4810
  method: 'POST',
4349
4811
  headers: { 'content-type': 'application/json' },
4350
- body: JSON.stringify({ idToken: opts.idToken, action: opts.action }),
4812
+ body: JSON.stringify({ idToken: opts.idToken, action: opts.action, walletProtocol: 2 }),
4351
4813
  });
4352
4814
  }
4353
4815
  catch (_a) {
@@ -4399,26 +4861,46 @@ function buildTurnkeyActionRequest(opts) {
4399
4861
  // code is the sole signing authority; within a live session every signature is one
4400
4862
  // approval click). The app retains the transaction itself.
4401
4863
  //
4402
- // ONE ORIGIN, ONE SESSION: the signing session lives in the signer origin's own
4403
- // storage, out of every app's reach, so ALL Bounded apps (any subdomain / custom
4404
- // domain) sign through this one origin - giving every user one wallet that roams
4405
- // across every app.
4406
- function hasDOM$4() {
4864
+ // Signing keys remain on the trusted signer origin. New apps use independently
4865
+ // bound wallets and storage slots; legacy apps retain their shared wallet.
4866
+ function hasDOM$3() {
4407
4867
  return typeof window !== 'undefined' && typeof document !== 'undefined';
4408
4868
  }
4409
- // BSA-2026-006: dispatch every signing / get-or-create exchange through a TOP-LEVEL
4410
- // POPUP the caller opened from the user gesture - NEVER a framed widget. An authorized
4411
- // tenant that embedded the signer could overlay its predictable Approve control and steal
4412
- // one concealed click into a fund-moving signature; a top-level window it cannot reach or
4413
- // visually redress closes that. A blocked popup surfaces `popup_blocked` (runPopup) and we
4414
- // never silently fall back to an iframe.
4869
+ // Dispatch one signer exchange through the in-app WIDGET, degrading to the POPUP
4870
+ // when we cannot embed (no DOM, or the frame never mounts/loads). Nothing in the
4871
+ // email-session flow needs a popup by preference: the code entry and the approval
4872
+ // click both run fine inside a cross-origin iframe on every engine (the old
4873
+ // Safari popup preference existed only for in-iframe WebAuthn CREATE).
4415
4874
  async function deliver(url, origin, ex) {
4416
- return runPopup(url, origin, ex);
4417
- }
4418
- async function turnkeySignerPage() {
4875
+ var _a, _b;
4876
+ const popup = () => runPopup(url, origin, ex);
4877
+ (_a = ex.assertCurrent) === null || _a === void 0 ? void 0 : _a.call(ex);
4878
+ const result = await runWidget(url, origin, { request: ex.request, onResult: ex.onResult, context: ex.context, assertCurrent: ex.assertCurrent }, popup);
4879
+ (_b = ex.assertCurrent) === null || _b === void 0 ? void 0 : _b.call(ex);
4880
+ return result;
4881
+ }
4882
+ async function turnkeySignerPage(token) {
4883
+ const assertGeneration = walletOperationGuard();
4884
+ const idToken = token === undefined ? getActiveSessionManager().getIdToken() : token;
4419
4885
  const cfg = await getConfig();
4420
4886
  const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
4421
- return { url: `${base}/wallet/turnkey/signer`, origin: new URL(base).origin };
4887
+ const context = walletContextFromClaims(idToken ? decodeJwt$1(idToken) : {}, base, cfg.appId, window.location.origin);
4888
+ return Object.assign(Object.assign({}, context), { idToken: idToken || '', assertCurrent() {
4889
+ assertGeneration();
4890
+ const current = getActiveSessionManager().getIdToken();
4891
+ if (!current)
4892
+ throw new Error('wallet_session_changed');
4893
+ const next = walletContextFromClaims(decodeJwt$1(current), base, cfg.appId, window.location.origin);
4894
+ if (next.key !== context.key)
4895
+ throw new Error('wallet_session_changed');
4896
+ } });
4897
+ }
4898
+ async function getTurnkeyWalletSessionState() {
4899
+ const context = await turnkeySignerPage();
4900
+ context.assertCurrent();
4901
+ if (!hasWarmSignerFrame(context.key))
4902
+ return { status: 'verification-required' };
4903
+ return walletSessionState(context.key);
4422
4904
  }
4423
4905
  function toHex(bytes) {
4424
4906
  let h = '';
@@ -4474,7 +4956,7 @@ async function runPopup(url, signerOrigin, ex) {
4474
4956
  const top = window.screenY + Math.max(0, (window.outerHeight - h) / 2);
4475
4957
  const popup = window.open(url, 'bounded-turnkey-signer', `width=${w},height=${h},left=${left},top=${top}`);
4476
4958
  if (!popup) {
4477
- throw new Error('popup_blocked - the wallet approval window was blocked. Call this from a user gesture (e.g. a click handler) so it can open; we never fall back to an in-page frame.');
4959
+ throw new Error('Popup blocked - call this from a user gesture (e.g. a click handler) so the wallet approval window can open.');
4478
4960
  }
4479
4961
  return await new Promise((resolve, reject) => {
4480
4962
  let settled = false;
@@ -4490,11 +4972,12 @@ async function runPopup(url, signerOrigin, ex) {
4490
4972
  let closeTimer;
4491
4973
  const cleanup = () => { settled = true; window.removeEventListener('message', onMsg); if (closeTimer)
4492
4974
  clearInterval(closeTimer); };
4493
- const sendRequest = () => { if (settled || acked)
4975
+ const sendRequest = () => { var _a; if (settled || acked)
4494
4976
  return; try {
4977
+ (_a = ex.assertCurrent) === null || _a === void 0 ? void 0 : _a.call(ex);
4495
4978
  popup.postMessage(ex.request, signerOrigin);
4496
4979
  }
4497
- catch ( /* popup gone */_a) { /* popup gone */ } };
4980
+ catch ( /* popup gone */_b) { /* popup gone */ } };
4498
4981
  const onMsg = (event) => {
4499
4982
  if (event.origin !== signerOrigin)
4500
4983
  return;
@@ -4564,16 +5047,9 @@ async function runPopup(url, signerOrigin, ex) {
4564
5047
  // identity with the Bounded id token WE minted (a body secret, RS256-verified server-side), not
4565
5048
  // an ambient cookie - the issuer's session cookie is SameSite-scoped to its own origin and is
4566
5049
  // not sent on this cross-origin fetch, so the token is what carries the session's wallet.
4567
- async function fetchSigningCapability(messageHex) {
4568
- const cfg = await getConfig();
4569
- const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
4570
- let idToken = null;
4571
- try {
4572
- idToken = getActiveSessionManager().getIdToken();
4573
- }
4574
- catch (_a) {
4575
- idToken = null;
4576
- }
5050
+ async function fetchSigningCapability(messageHex, context) {
5051
+ context.assertCurrent();
5052
+ const { base, idToken } = context;
4577
5053
  if (!idToken) {
4578
5054
  throw new Error('This session has no Turnkey wallet. Sign in again to provision one.');
4579
5055
  }
@@ -4588,16 +5064,9 @@ async function fetchSigningCapability(messageHex) {
4588
5064
  // requires an issuer-signed authorization bound to this app origin and exact action. Fetch it
4589
5065
  // before posting to the signer. Walletless sessions are valid for createWallet because that is
4590
5066
  // the operation that provisions their first wallet.
4591
- async function fetchActionAuthorization(action) {
4592
- const cfg = await getConfig();
4593
- const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
4594
- let idToken = null;
4595
- try {
4596
- idToken = getActiveSessionManager().getIdToken();
4597
- }
4598
- catch (_a) {
4599
- idToken = null;
4600
- }
5067
+ async function fetchActionAuthorization(action, context) {
5068
+ context.assertCurrent();
5069
+ const { base, idToken } = context;
4601
5070
  if (!idToken) {
4602
5071
  throw new Error('Sign in again to set up your Turnkey wallet.');
4603
5072
  }
@@ -4617,31 +5086,23 @@ async function fetchActionAuthorization(action) {
4617
5086
  * an unswept session is account-bound and unusable without a live authorization,
4618
5087
  * it just costs its owner nothing and a stranger nothing.
4619
5088
  */
4620
- async function clearTurnkeySigningSession() {
4621
- if (!hasDOM$4())
5089
+ async function clearTurnkeySigningSession(idToken) {
5090
+ if (!hasDOM$3())
4622
5091
  return;
4623
- // The session can live in TWO places, and the sweep has to reach both:
4624
- // 1. the signer origin's store, shared by every signer frame in this app's partition -
4625
- // cleared by posting into a throwaway frame below;
4626
- // 2. the WARM widget iframe's memory. The signer keeps an in-memory mirror when the
4627
- // browser blocks its storage, and hideModal deliberately leaves that iframe mounted
4628
- // between signatures, so the mirror outlives logout. A throwaway frame cannot see
4629
- // another document's memory - the only way to take it is to destroy that document.
4630
- // Done first, and synchronously, so it lands even if the frame exchange below fails.
4631
- try {
4632
- const { origin } = await turnkeySignerPage();
4633
- (await Promise.resolve().then(function () { return boundedWidgetModal; })).detachWarmSignerIframe(origin);
4634
- }
4635
- catch (_a) {
4636
- /* no modal module / no warm frame: nothing to take */
4637
- }
5092
+ // Destroy the trusted in-memory key even if configuration or token parsing fails.
5093
+ // Do it synchronously, before a different login can mount a replacement frame.
5094
+ detachWarmSignerIframe();
5095
+ // Capture the session before asynchronous teardown clears the SDK's token.
5096
+ const contextPromise = turnkeySignerPage(idToken);
4638
5097
  let frame = null;
4639
5098
  try {
4640
- frame = await openHiddenSignerFrame();
5099
+ const context = await contextPromise;
5100
+ setWalletSessionState(context.key, { status: 'verification-required', reason: 'logged_out' });
5101
+ frame = await openHiddenSignerFrame(context);
4641
5102
  await frame.request({ type: 'bounded:turnkey:clearSigningSession', requestId: `tkclear_${Date.now()}_${++requestSeq}` }, 4000);
4642
5103
  }
4643
- catch (_b) {
4644
- /* best-effort: never block or fail logout on the sweep */
5104
+ catch (_a) {
5105
+ // The warm frame was destroyed; the bounded store sweep is best effort.
4645
5106
  }
4646
5107
  finally {
4647
5108
  frame === null || frame === void 0 ? void 0 : frame.dispose();
@@ -4659,12 +5120,14 @@ function hasTurnkeyWallet() {
4659
5120
  * Use this to learn the fee-payer address BEFORE building a transaction to sign.
4660
5121
  */
4661
5122
  async function getOrCreateTurnkeyWallet() {
4662
- if (!hasDOM$4())
4663
- throw new Error('Turnkey embedded wallets are only available in the browser (popup-based).');
4664
- const { url, origin } = await turnkeySignerPage();
5123
+ if (!hasDOM$3())
5124
+ throw new Error('Turnkey embedded wallets are only available in the browser (trusted wallet UI).');
5125
+ const context = await turnkeySignerPage();
5126
+ const { url, origin } = context;
4665
5127
  const requestId = `tkcreate_${Date.now()}_${++requestSeq}`;
4666
- const authorization = await fetchActionAuthorization('createWallet');
5128
+ const authorization = await fetchActionAuthorization('createWallet', context);
4667
5129
  return await deliver(url, origin, {
5130
+ context: context.key, assertCurrent: context.assertCurrent,
4668
5131
  request: buildTurnkeyActionRequest({ requestId, action: 'createWallet', authorization }),
4669
5132
  onResult: (d) => {
4670
5133
  if (d.ok && typeof d.address === 'string')
@@ -4684,7 +5147,7 @@ async function getOrCreateTurnkeyWallet() {
4684
5147
  * that tab - it never transits Bounded.
4685
5148
  */
4686
5149
  async function openTurnkeyKeyExport() {
4687
- if (!hasDOM$4())
5150
+ if (!hasDOM$3())
4688
5151
  throw new Error('Turnkey key export is only available in the browser.');
4689
5152
  if (!hasTurnkeyWallet())
4690
5153
  throw new Error('This session has no Turnkey wallet to export.');
@@ -4705,10 +5168,11 @@ async function openTurnkeyKeyExport() {
4705
5168
  }
4706
5169
  catch ( /* older engines: the tab still cannot read us cross-origin */_a) { /* older engines: the tab still cannot read us cross-origin */ }
4707
5170
  try {
4708
- const cfg = await getConfig();
4709
- const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
4710
- const authorization = await fetchActionAuthorization('exportWallet');
4711
- handle.location.href = `${base}/wallet/turnkey/export#authorization=${encodeURIComponent(authorization)}`;
5171
+ const context = await turnkeySignerPage();
5172
+ const authorization = await fetchActionAuthorization('exportWallet', context);
5173
+ context.assertCurrent();
5174
+ const path = context.scope === 'app' ? `/wallet/turnkey/app/${context.appId}` : '/wallet/turnkey';
5175
+ handle.location.href = `${context.base}${path}/export#authorization=${encodeURIComponent(authorization)}`;
4712
5176
  }
4713
5177
  catch (error) {
4714
5178
  // Never strand a blank tab on a failed mint.
@@ -4720,10 +5184,9 @@ async function openTurnkeyKeyExport() {
4720
5184
  }
4721
5185
  }
4722
5186
  /**
4723
- * Open the Bounded Turnkey signer popup and sign `message` (raw bytes) with the
4724
- * user's non-custodial Turnkey wallet, resolving with the ed25519 signature +
4725
- * the wallet address. MUST be called from a USER GESTURE (click/tap) or the
4726
- * browser blocks the popup.
5187
+ * Open the Bounded Turnkey approval card and sign `message` (raw bytes) with the
5188
+ * user's Turnkey wallet, resolving with the ed25519 signature and wallet address.
5189
+ * Call from a user gesture so a deliberate browser fallback can open if needed.
4727
5190
  *
4728
5191
  * The signer shows an approve card (wallet address, requesting origin, Approve /
4729
5192
  * Cancel). With a live 24-hour signing session (normally established by the login
@@ -4732,24 +5195,27 @@ async function openTurnkeyKeyExport() {
4732
5195
  * completes the signature.
4733
5196
  */
4734
5197
  async function signSolanaMessageViaTurnkey(message) {
4735
- if (!hasDOM$4()) {
4736
- throw new Error('Turnkey embedded-wallet signing is only available in the browser (popup-based).');
5198
+ if (!hasDOM$3()) {
5199
+ throw new Error('Turnkey embedded-wallet signing is only available in the browser (trusted wallet UI).');
4737
5200
  }
4738
- const { url, origin } = await turnkeySignerPage();
5201
+ const context = await turnkeySignerPage();
5202
+ const { url, origin } = context;
4739
5203
  const requestId = `tksign_${Date.now()}_${++requestSeq}`;
4740
5204
  const messageHex = toHex(message);
4741
5205
  // AS-02: obtain the issuer-signed capability for THESE exact bytes first; the signer will
4742
5206
  // refuse (capability_required) without it. Fetched per signature (each is single-payload).
4743
- const capability = await fetchSigningCapability(messageHex);
5207
+ const capability = await fetchSigningCapability(messageHex, context);
4744
5208
  // Inline (authMode:"turnkey") sessions plant no Better Auth cookie on the issuer, so the
4745
5209
  // signer's same-origin status/create fetches need a bearer session proof: attach the
4746
5210
  // createWallet action authorization (it covers the sign flow's get-or-create too). Minted
4747
5211
  // from the same id token as the capability, so a session that minted one can mint both.
4748
- const authorization = await fetchActionAuthorization('createWallet');
5212
+ const authorization = await fetchActionAuthorization('createWallet', context);
4749
5213
  return await deliver(url, origin, {
5214
+ context: context.key, assertCurrent: context.assertCurrent,
4750
5215
  request: buildTurnkeySignRequest({ requestId, messageHex, capability, authorization }),
4751
5216
  onResult: (d) => {
4752
5217
  if (d.ok && typeof d.signatureHex === 'string' && typeof d.address === 'string') {
5218
+ setWalletSessionState(context.key, { status: 'ready', expiresAt: Number(d.signingSessionExpiresAt) });
4753
5219
  return { done: true, value: { signature: fromHex(d.signatureHex), address: d.address, subOrgId: String(d.subOrgId || '') } };
4754
5220
  }
4755
5221
  return { error: String(d.error || 'Turnkey signing failed') };
@@ -4859,11 +5325,21 @@ async function signAndSubmitTransactionWithTurnkey(transaction, feePayer) {
4859
5325
  }
4860
5326
  return signature;
4861
5327
  }
5328
+ /** Save this trusted URL to recover the wallet even after the app is deleted. */
5329
+ async function getTurnkeyWalletRecoveryUrl() {
5330
+ const context = await turnkeySignerPage();
5331
+ context.assertCurrent();
5332
+ if (!context.address || !hasTurnkeyWallet())
5333
+ throw new Error('wallet_unavailable');
5334
+ return `${context.base}/wallet/turnkey/recover/${encodeURIComponent(context.address)}`;
5335
+ }
4862
5336
 
4863
5337
  var turnkeySignerBridge = /*#__PURE__*/Object.freeze({
4864
5338
  __proto__: null,
4865
5339
  clearTurnkeySigningSession: clearTurnkeySigningSession,
4866
5340
  getOrCreateTurnkeyWallet: getOrCreateTurnkeyWallet,
5341
+ getTurnkeyWalletRecoveryUrl: getTurnkeyWalletRecoveryUrl,
5342
+ getTurnkeyWalletSessionState: getTurnkeyWalletSessionState,
4867
5343
  hasTurnkeyWallet: hasTurnkeyWallet,
4868
5344
  openTurnkeyKeyExport: openTurnkeyKeyExport,
4869
5345
  signAndSubmitTransactionWithTurnkey: signAndSubmitTransactionWithTurnkey,
@@ -4890,7 +5366,7 @@ const ISSUER_SESSION_KIND_KEY = 'bounded_issuer_session_kind';
4890
5366
  // not by `typeof window` (truthy on RN). OIDC redirect/popup is web-only, but the
4891
5367
  // session it mints is read back through this same selector everywhere.
4892
5368
  const sessionManager = getActiveSessionManager;
4893
- function hasDOM$3() {
5369
+ function hasDOM$2() {
4894
5370
  return typeof window !== 'undefined' && typeof document !== 'undefined';
4895
5371
  }
4896
5372
  function decodeJwt(token) {
@@ -5041,7 +5517,7 @@ async function buildAuthorize(opts) {
5041
5517
  throw new Error('appId is not configured (init the SDK with your appId)');
5042
5518
  // Web: default to the current page so callers don't have to pass redirectUri. Native has no
5043
5519
  // window — it MUST pass an https universal link (the system auth session needs a real URL).
5044
- const redirectUri = opts.redirectUri || (hasDOM$3() ? window.location.origin + window.location.pathname : undefined);
5520
+ const redirectUri = opts.redirectUri || (hasDOM$2() ? window.location.origin + window.location.pathname : undefined);
5045
5521
  if (!redirectUri)
5046
5522
  throw new Error('redirectUri is required on React Native — pass the https universal link the app owner registered.');
5047
5523
  const { verifier, challenge } = await makePkce();
@@ -5149,7 +5625,7 @@ async function runNativeLogin(opts) {
5149
5625
  async function loginWithRedirect(opts) {
5150
5626
  if (isNative())
5151
5627
  return runNativeLogin(opts);
5152
- if (!hasDOM$3())
5628
+ if (!hasDOM$2())
5153
5629
  throw new Error('loginWithRedirect requires a browser or a configured React Native platform');
5154
5630
  const { url, pkce } = await buildAuthorize(opts);
5155
5631
  // Stash the verifier + state so the redirect_uri page can finish the exchange. sessionStorage
@@ -5166,7 +5642,7 @@ async function loginWithRedirect(opts) {
5166
5642
  * loginWithRedirect() (which resolves with the User), so there is no separate redirect
5167
5643
  * page to finish from. Calling it on RN is harmless. */
5168
5644
  async function completeLoginFromRedirect() {
5169
- if (!hasDOM$3())
5645
+ if (!hasDOM$2())
5170
5646
  return null;
5171
5647
  const url = new URL(window.location.href);
5172
5648
  const code = url.searchParams.get('code');
@@ -5214,7 +5690,7 @@ async function completeLoginFromRedirect() {
5214
5690
  * redirect_uri page posts the code back. The redirect_uri page just calls the usual
5215
5691
  * completeLoginFromRedirect() (which auto-detects the popup) — no separate call needed. */
5216
5692
  async function loginWithPopup(opts) {
5217
- if (!hasDOM$3())
5693
+ if (!hasDOM$2())
5218
5694
  throw new Error('loginWithPopup is web-only — on React Native use loginWithRedirect()');
5219
5695
  // Open synchronously, before PKCE/config awaits consume the click's user-
5220
5696
  // activation window. Opening only after buildAuthorize() intermittently
@@ -5374,7 +5850,7 @@ async function loginWithPopup(opts) {
5374
5850
  /** Call this on the redirect_uri page when login was opened via loginWithPopup(): posts the
5375
5851
  * code back to the opener and closes. (For the redirect variant use completeLoginFromRedirect.) */
5376
5852
  function completeLoginInPopup(openerOrigin) {
5377
- if (!hasDOM$3() || !window.opener)
5853
+ if (!hasDOM$2() || !window.opener)
5378
5854
  return;
5379
5855
  const url = new URL(window.location.href);
5380
5856
  const code = url.searchParams.get('code');
@@ -5395,7 +5871,7 @@ function completeLoginInPopup(openerOrigin) {
5395
5871
  // Turnkey-native login path (authMode:"turnkey"), which mints the session server-side
5396
5872
  // after Turnkey verifies the email OTP - there is no OIDC code to exchange. Wiring is
5397
5873
  // identical to exchangeCode's tail so the realtime worker cannot tell them apart.
5398
- async function establishInlineSession(data) {
5874
+ async function establishInlineSession(data, beforeCommit, beforeStore) {
5399
5875
  const base = await issuerBase();
5400
5876
  const idToken = tokenField(data, 'id_token', 'idToken');
5401
5877
  if (!idToken)
@@ -5403,11 +5879,18 @@ async function establishInlineSession(data) {
5403
5879
  const accessToken = tokenField(data, 'access_token', 'accessToken') || idToken;
5404
5880
  const refreshToken = boundedRefreshTokenField(data);
5405
5881
  const user = userFromIdToken(idToken);
5406
- await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base);
5882
+ const rollback = await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base, () => { });
5407
5883
  // Adopt the OIDC provider on EVERY session surface - including core's signing
5408
5884
  // pointer (getConfig().authProvider), which this path used to skip, leaving
5409
5885
  // onchain writes routed through a previous (e.g. logged-out wallet) provider.
5410
- await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user });
5886
+ try {
5887
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user, beforeCommit });
5888
+ }
5889
+ catch (error) {
5890
+ if (typeof rollback === 'function')
5891
+ await rollback();
5892
+ throw error;
5893
+ }
5411
5894
  // Turnkey-native OTP mints an app session directly. It does not establish a
5412
5895
  // Better Auth browser cookie on the issuer, so logout must not navigate
5413
5896
  // through the issuer's cookie-clearing /logout endpoint. Stamped AFTER the
@@ -5471,9 +5954,6 @@ async function exchangeCode(code, verifier, redirectUri) {
5471
5954
  return user;
5472
5955
  }
5473
5956
 
5474
- // NOTE: Provider class re-exports have been moved to index.ts (web) and
5475
- // index.native.ts (RN) to avoid eager resolution of web-only dependencies
5476
- // (react-dom) when this module is imported on RN.
5477
5957
  let currentAuthProvider = null;
5478
5958
  let currentAuthMethod = null;
5479
5959
  let currentAuthProviderConfigKey = null;
@@ -5828,7 +6308,7 @@ async function getAuthProvider(config) {
5828
6308
  // manager as legacy email sessions. Human login should be started
5829
6309
  // with loginWithRedirect(); this provider is kept as the restore and
5830
6310
  // logout surface for those sessions.
5831
- currentAuthProvider = new (await import('./email-auth-provider-DF7Urh8i.mjs')).EmailAuthProvider();
6311
+ currentAuthProvider = new (await import('./email-auth-provider-BKYVi3dl.mjs')).EmailAuthProvider();
5832
6312
  break;
5833
6313
  case "phantom": {
5834
6314
  // First-class Solana WALLET LOGIN (the poof/TaroBase-style "connect
@@ -5870,7 +6350,7 @@ async function getAuthProvider(config) {
5870
6350
  // (bounded-login-widget.ts), so registration never counts as
5871
6351
  // evidence the user has a wallet.
5872
6352
  await ensureSolanaMobileWalletRegistered(config, configuredNetwork);
5873
- const { InjectedWalletProvider } = await import('./injected-wallet-provider-Dlo96aV9.mjs');
6353
+ const { InjectedWalletProvider } = await import('./injected-wallet-provider-yl51pLBa.mjs');
5874
6354
  currentAuthProvider = new InjectedWalletProvider(resolveInjectedWalletConfig(config), rpcUrl, configuredNetwork);
5875
6355
  break;
5876
6356
  }
@@ -5883,7 +6363,7 @@ async function getAuthProvider(config) {
5883
6363
  // Selecting authMethod:'evm-wallet' IS the opt-in — pass evmWalletConfig
5884
6364
  // to point at a specific wallet or set chainId/domain.
5885
6365
  const evmWalletConfig = ((_a = config.evmWalletConfig) !== null && _a !== void 0 ? _a : {});
5886
- const { InjectedEvmWalletProvider } = await import('./injected-evm-wallet-provider-CMqUqqxN.mjs');
6366
+ const { InjectedEvmWalletProvider } = await import('./injected-evm-wallet-provider-BmDPRV5L.mjs');
5887
6367
  currentAuthProvider = new InjectedEvmWalletProvider(evmWalletConfig);
5888
6368
  break;
5889
6369
  }
@@ -5908,7 +6388,7 @@ async function getAuthProvider(config) {
5908
6388
  break;
5909
6389
  case "guest":
5910
6390
  // Anonymous auth: a device-local ed25519 keypair signs the challenge.
5911
- currentAuthProvider = new (await import('./guest-auth-provider-DVw-tITC.mjs')).GuestAuthProvider();
6391
+ currentAuthProvider = new (await import('./guest-auth-provider-E7Fg7-E2.mjs')).GuestAuthProvider();
5912
6392
  break;
5913
6393
  }
5914
6394
  if (!currentAuthProvider) {
@@ -5970,7 +6450,7 @@ async function signInAnonymously() {
5970
6450
  // Lazy-load the guest provider so its ed25519/web3 dependencies stay out of
5971
6451
  // the main bundle for data-only apps. Specifier is a static string literal
5972
6452
  // so bundle-splitting can see it.
5973
- const provider = new (await import('./guest-auth-provider-DVw-tITC.mjs')).GuestAuthProvider();
6453
+ const provider = new (await import('./guest-auth-provider-E7Fg7-E2.mjs')).GuestAuthProvider();
5974
6454
  setAuthLoading(true); // reflect "signing in…" while the guest keypair session is minted
5975
6455
  // adoptSessionProvider awaits only getConfig() (already resolved here) and then
5976
6456
  // runs every write - provider/method/config/marker/user - in one synchronous
@@ -6141,7 +6621,7 @@ function ensureSharedWalletLoginReady() {
6141
6621
  if (!sharedWalletReady) {
6142
6622
  sharedWalletReady = (async () => {
6143
6623
  await getConfig();
6144
- await import('./injected-wallet-provider-Dlo96aV9.mjs');
6624
+ await import('./injected-wallet-provider-yl51pLBa.mjs');
6145
6625
  })().catch((error) => {
6146
6626
  sharedWalletReady = null;
6147
6627
  throw error;
@@ -6244,7 +6724,7 @@ async function loginWithWallet(options) {
6244
6724
  await ensureSharedWalletLoginReady();
6245
6725
  else
6246
6726
  await ensureWalletLoginReady();
6247
- const { InjectedWalletProvider } = await import('./injected-wallet-provider-Dlo96aV9.mjs');
6727
+ const { InjectedWalletProvider } = await import('./injected-wallet-provider-yl51pLBa.mjs');
6248
6728
  // Keep the RAW wallet provider: the offchain wrapper forwards only a fixed
6249
6729
  // set of methods, so cleanup below must speak to the provider that actually
6250
6730
  // holds the config rather than to whatever is wrapping it.
@@ -6406,13 +6886,14 @@ async function exchangeLogoutHint(issuerBase, idToken) {
6406
6886
  // which can be blocked or offline), so its failures are the one thing logout may
6407
6887
  // tolerate - never revokeSession's. Lazy import per this module's rule (the signer
6408
6888
  // bridge is browser-only machinery). Never rejects.
6409
- async function sweepTurnkeySigningSession() {
6889
+ async function sweepTurnkeySigningSession(idToken) {
6410
6890
  try {
6411
- await (await Promise.resolve().then(function () { return turnkeySignerBridge; })).clearTurnkeySigningSession();
6891
+ await (await Promise.resolve().then(function () { return turnkeySignerBridge; })).clearTurnkeySigningSession(idToken);
6412
6892
  }
6413
6893
  catch ( /* best-effort: never block or fail logout on the sweep */_a) { /* best-effort: never block or fail logout on the sweep */ }
6414
6894
  }
6415
6895
  async function logout$1(options) {
6896
+ invalidatePendingWalletLogin();
6416
6897
  // Re-initialize the provider if it was cleared by a prior logout — the exact mirror
6417
6898
  // of login()'s self-heal above. Every session-establishment path now adopts through
6418
6899
  // session-provider.ts, so this is a REPAIR (the registry was cleared by a previous
@@ -6444,7 +6925,7 @@ async function logout$1(options) {
6444
6925
  // sweep is a hidden signer-iframe exchange; start it now (after revocation is
6445
6926
  // committed - a failed logout leaves the still-logged-in user their session) and
6446
6927
  // let it overlap the provider teardown. sweepTurnkeySigningSession never rejects.
6447
- const signingSessionSweep = sweepTurnkeySigningSession();
6928
+ const signingSessionSweep = sweepTurnkeySigningSession(idTokenHint);
6448
6929
  // Only OIDC-hosted sessions own an issuer browser cookie. Turnkey-native
6449
6930
  // email OTP is also an email auth method, but it mints the app session
6450
6931
  // inline and has no issuer cookie to clear. Use the explicit session-kind
@@ -6673,371 +7154,101 @@ function onAuthStateChanged(callback) {
6673
7154
  // setCurrentUser and init()'s initCompleted would never fire (init's
6674
7155
  // setCurrentUser is a no-op for same-address and initCompleted is still false).
6675
7156
  callback(currentUser);
6676
- }
6677
- return () => {
6678
- authStateListeners = authStateListeners.filter((listener) => listener !== callback);
6679
- };
6680
- }
6681
- function onAuthLoadingChanged(callback) {
6682
- authLoadingListeners.push(callback);
6683
- // Call immediately with current loading state
6684
- callback(isAuthLoading);
6685
- return () => {
6686
- authLoadingListeners = authLoadingListeners.filter((listener) => listener !== callback);
6687
- };
6688
- }
6689
- function setAuthLoading(loading) {
6690
- if (isAuthLoading !== loading) {
6691
- isAuthLoading = loading;
6692
- authLoadingListeners.forEach((callback) => callback(loading));
6693
- }
6694
- }
6695
- function getAuthLoading() {
6696
- return isAuthLoading;
6697
- }
6698
- async function login(options) {
6699
- if (!authProviderInstance) {
6700
- throw new Error('SDK not initialized. Please call init() first.');
6701
- }
6702
- const loggedInUser = await login$1(options);
6703
- setCurrentUser(loggedInUser);
6704
- return currentUser;
6705
- }
6706
- async function logout(options) {
6707
- if (!authProviderInstance) {
6708
- throw new Error('SDK not initialized. Please call init() first.');
6709
- }
6710
- await logout$1(options);
6711
- setCurrentUser(null);
6712
- }
6713
- var hasRunFirstTime = false;
6714
- function userPrincipalKey(user) {
6715
- var _a, _b, _c;
6716
- if (!user)
6717
- return null;
6718
- return (_c = (_b = (_a = user.id) !== null && _a !== void 0 ? _a : user.address) !== null && _b !== void 0 ? _b : user.email) !== null && _c !== void 0 ? _c : null;
6719
- }
6720
- // Update the currentUser and notify listeners
6721
- function setCurrentUser(user) {
6722
- // Enrich with the universal identity fields (id/email) derived from the
6723
- // active session's idToken — central chokepoint so every provider's user
6724
- // object gains { id, address, email } without each construction site
6725
- // having to set them.
6726
- user = withUserIdentity(user);
6727
- const lastUser = currentUser;
6728
- const wasInitializedBefore = hasRunFirstTime;
6729
- const lastPrincipal = userPrincipalKey(currentUser);
6730
- const nextPrincipal = userPrincipalKey(user);
6731
- const shouldRunListeners = !hasRunFirstTime || lastPrincipal !== nextPrincipal;
6732
- hasRunFirstTime = true;
6733
- currentUser = user;
6734
- if (shouldRunListeners) {
6735
- // Isolate each listener: a consumer callback that throws (during logout in
6736
- // particular) must not abort the notification loop OR skip the socket reopen
6737
- // below. Before this, a single throwing listener propagated out of
6738
- // setCurrentUser, so the realtime socket kept streaming as the signed-out
6739
- // user and the other listeners were never told the principal changed.
6740
- authStateListeners.forEach((callback) => {
6741
- try {
6742
- callback(user);
6743
- }
6744
- catch (error) {
6745
- console.error('Error in onAuthStateChanged listener:', error);
6746
- }
6747
- });
6748
- // Reconnect WebSocket connections with new auth when user changes.
6749
- // Skip on the first setCurrentUser call during init() — there are no
6750
- // existing connections to reconnect at that point.
6751
- if (wasInitializedBefore && userPrincipalKey(lastUser) !== nextPrincipal) {
6752
- reconnectWithNewAuth().catch((error) => {
6753
- console.error('Error reconnecting WebSocket after auth change:', error);
6754
- });
6755
- }
6756
- }
6757
- // Return this commit's normalized snapshot. A listener can start another
6758
- // login, so the mutable currentUser may already belong to a newer session.
6759
- return user;
6760
- }
6761
- function getCurrentUser() {
6762
- return currentUser;
6763
- }
6764
- /** @internal Snapshot/restore surface for transactional auth-provider switches. */
6765
- function getAuthProviderInstance() {
6766
- return authProviderInstance;
6767
- }
6768
- function setAuthProviderInstance(provider) {
6769
- authProviderInstance = provider;
6770
- }
6771
-
6772
- // bounded-widget-modal.ts - the shared in-app WIDGET shell for Bounded's login
6773
- // and signing surfaces. One Shadow-DOM overlay + panel that matches the Bounded
6774
- // widget (Seal) look and MOTION: a backdrop fade, a panel rise on desktop, a
6775
- // bottom-sheet on mobile, a height tween as content swaps, and a clean close.
6776
- //
6777
- // It hosts EITHER:
6778
- // - NATIVE content (the login card the SDK renders itself), or
6779
- // - an IFRAME (the Bounded-owned signer page, which must run on the single
6780
- // signer origin because the wallet's 24h signing session lives in that
6781
- // origin's storage, out of every app's reach).
6782
- //
6783
- // The modal is a warm singleton: it stays mounted (hidden) between uses so a
6784
- // second open - or a signature right after login - is instant and, when it is
6785
- // already open, a clean in-place view swap rather than a reopen.
6786
- function hasDOM$2() {
6787
- return typeof window !== "undefined" && typeof document !== "undefined";
6788
- }
6789
- const OVERLAY_CSS = `
6790
- :host{all:initial}
6791
- *{box-sizing:border-box}
6792
- .bw-ov{position:fixed;inset:0;z-index:2147483647;display:none;
6793
- font-family:"Schibsted Grotesk","Helvetica Neue",Arial,sans-serif}
6794
- .bw-ov.mounted{display:block}
6795
- .bw-back{position:fixed;inset:0;background:rgba(4,4,5,.62);opacity:0;
6796
- transition:opacity .24s ease;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}
6797
- .bw-wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:20px}
6798
- .bw-panel{position:relative;width:420px;max-width:calc(100vw - 28px);height:280px;
6799
- border-radius:14px;overflow:hidden;background:#0e0f11;border:1px solid rgba(232,230,222,.08);
6800
- box-shadow:0 1px 0 rgba(232,230,222,.03) inset,0 40px 90px -45px rgba(0,0,0,.85);
6801
- opacity:0;transform:translateY(14px) scale(.98);will-change:transform,opacity,height;
6802
- transition:opacity .28s cubic-bezier(.2,.8,.2,1),transform .28s cubic-bezier(.2,.8,.2,1),
6803
- height .3s cubic-bezier(.2,.8,.2,1)}
6804
- .bw-ov.show .bw-back{opacity:1}
6805
- .bw-ov.show .bw-panel{opacity:1;transform:none}
6806
- .bw-ov.closing .bw-back{opacity:0}
6807
- .bw-ov.closing .bw-panel{opacity:0;transform:translateY(12px) scale(.99)}
6808
- .bw-content{position:absolute;inset:0;overflow:hidden}
6809
- .bw-frame{display:block;width:100%;height:100%;border:0;background:transparent;color-scheme:dark}
6810
- /* editorial corner crop-marks, matching the reference frame */
6811
- .bw-mk{position:absolute;width:13px;height:13px;pointer-events:none;z-index:2;
6812
- border-color:rgba(232,230,222,.22);border-style:solid;border-width:0}
6813
- .bw-mk.tl{top:11px;left:11px;border-top-width:1px;border-left-width:1px}
6814
- .bw-mk.tr{top:11px;right:11px;border-top-width:1px;border-right-width:1px}
6815
- .bw-mk.bl{bottom:11px;left:11px;border-bottom-width:1px;border-left-width:1px}
6816
- .bw-mk.br{bottom:11px;right:11px;border-bottom-width:1px;border-right-width:1px}
6817
- @media(max-width:560px){
6818
- .bw-wrap{align-items:flex-end;padding:0}
6819
- .bw-panel{width:100%;max-width:100%;border-radius:18px 18px 0 0;border-bottom:0}
6820
- .bw-content{overflow-y:auto}
6821
- .bw-ov.show .bw-panel{transform:none}
6822
- .bw-ov:not(.show) .bw-panel,.bw-ov.closing .bw-panel{transform:translateY(18%)}
6823
- }
6824
- @media(prefers-reduced-motion:reduce){.bw-back,.bw-panel{transition:opacity .12s linear!important}}
6825
- `;
6826
- let modal = null;
6827
- function getModal() {
6828
- if (modal)
6829
- return modal;
6830
- const root = document.createElement("div");
6831
- root.setAttribute("data-bounded-widget", "");
6832
- const shadow = root.attachShadow({ mode: "open" });
6833
- const style = document.createElement("style");
6834
- style.textContent = OVERLAY_CSS;
6835
- shadow.appendChild(style);
6836
- const overlay = document.createElement("div");
6837
- overlay.className = "bw-ov";
6838
- const back = document.createElement("div");
6839
- back.className = "bw-back";
6840
- const wrap = document.createElement("div");
6841
- wrap.className = "bw-wrap";
6842
- const panel = document.createElement("div");
6843
- panel.className = "bw-panel";
6844
- const content = document.createElement("div");
6845
- content.className = "bw-content";
6846
- panel.appendChild(content);
6847
- for (const corner of ["tl", "tr", "bl", "br"]) {
6848
- const mk = document.createElement("span");
6849
- mk.className = "bw-mk " + corner;
6850
- panel.appendChild(mk);
6851
- }
6852
- wrap.appendChild(panel);
6853
- overlay.appendChild(back);
6854
- overlay.appendChild(wrap);
6855
- shadow.appendChild(overlay);
6856
- (document.body || document.documentElement).appendChild(root);
6857
- const m = {
6858
- root,
6859
- overlay,
6860
- panel,
6861
- content,
6862
- iframe: null,
6863
- iframeOrigin: "",
6864
- iframeReady: false,
6865
- onBackdrop: null,
6866
- onIframeMessage: null,
6867
- chain: Promise.resolve(),
6868
- fitObserver: null,
6869
- generation: 0,
6870
- onSupersede: null,
6871
- };
6872
- // The dismiss handler lives on `.bw-wrap`: it is the full-viewport flex layer
6873
- // that sits ABOVE `.bw-back`, so it (not the backdrop element) receives clicks
6874
- // outside the panel. Fire only when the click lands on the wrap itself, never
6875
- // when it bubbles up from the panel or its content.
6876
- wrap.addEventListener("click", (event) => {
6877
- if (event.target === wrap && m.onBackdrop)
6878
- m.onBackdrop();
6879
- });
6880
- window.addEventListener("message", (event) => {
6881
- if (!m.iframe)
6882
- return;
6883
- if (event.origin !== m.iframeOrigin)
6884
- return;
6885
- if (event.source !== m.iframe.contentWindow)
6886
- return;
6887
- const d = event.data;
6888
- if (!d || typeof d !== "object")
6889
- return;
6890
- if (m.onIframeMessage)
6891
- m.onIframeMessage(d);
6892
- });
6893
- modal = m;
6894
- return m;
6895
- }
6896
- function showModal(m) {
6897
- m.overlay.classList.remove("closing");
6898
- m.overlay.classList.add("mounted");
6899
- // Force a reflow so the .show transition runs from the hidden state every open.
6900
- void m.panel.offsetWidth;
6901
- m.overlay.classList.add("show");
6902
- }
6903
- function hideModal(m, after) {
6904
- m.overlay.classList.remove("show");
6905
- m.overlay.classList.add("closing");
6906
- // Capture the generation at hide time. If a newer open re-shows the modal within
6907
- // the close tween, this stale timer must NOT strip `.mounted`/`.closing` off the
6908
- // freshly reopened widget - only tear down when we are still the same open.
6909
- const gen = m.generation;
6910
- window.setTimeout(() => {
6911
- if (m.generation === gen)
6912
- m.overlay.classList.remove("mounted", "closing");
6913
- }, 260);
6914
- }
6915
- function setPanelHeight(m, px) {
6916
- const h = Math.max(140, Math.min(680, Math.round(px)));
6917
- m.panel.style.height = h + "px";
6918
- }
6919
- // True outer height of the mounted card: the max of its padding-box scroll height
6920
- // (catches overflowing descendants) and its border-box height plus own vertical
6921
- // margins (catches border + margins, which scrollHeight omits). Ceiled so a
6922
- // sub-pixel row is never clipped under `overflow:hidden`.
6923
- function measureOuterHeight(target) {
6924
- var _a;
6925
- const cs = (_a = target.ownerDocument.defaultView) === null || _a === void 0 ? void 0 : _a.getComputedStyle(target);
6926
- const marginY = cs ? parseFloat(cs.marginTop) + parseFloat(cs.marginBottom) : 0;
6927
- const borderBox = target.offsetHeight + (Number.isFinite(marginY) ? marginY : 0);
6928
- const rect = target.getBoundingClientRect().height;
6929
- return Math.ceil(Math.max(target.scrollHeight, borderBox, rect));
7157
+ }
7158
+ return () => {
7159
+ authStateListeners = authStateListeners.filter((listener) => listener !== callback);
7160
+ };
6930
7161
  }
6931
- /**
6932
- * Measure the currently mounted native content and tween the panel to fit it.
6933
- * Re-fits automatically when the card's intrinsic height changes - notably after
6934
- * the Newsreader/Schibsted web fonts swap in (which grows line boxes a few px
6935
- * after first paint) and on inline view swaps (email -> OTP code -> success).
6936
- */
6937
- function fitPanelToContent(m) {
6938
- var _a, _b;
6939
- const first = m.content.firstElementChild;
6940
- const target = first || m.content;
6941
- const apply = () => {
6942
- // Only fit while this exact node is still the mounted card.
6943
- if (m.content.firstElementChild !== first && first)
6944
- return;
6945
- const h = measureOuterHeight(target);
6946
- if (h > 0)
6947
- setPanelHeight(m, h);
7162
+ function onAuthLoadingChanged(callback) {
7163
+ authLoadingListeners.push(callback);
7164
+ // Call immediately with current loading state
7165
+ callback(isAuthLoading);
7166
+ return () => {
7167
+ authLoadingListeners = authLoadingListeners.filter((listener) => listener !== callback);
6948
7168
  };
6949
- apply();
6950
- const view = target.ownerDocument.defaultView;
6951
- // Re-fit after fonts load (one-shot) and once the ResizeObserver-less browsers
6952
- // have painted, so the height never lags a late reflow.
6953
- (_a = view === null || view === void 0 ? void 0 : view.requestAnimationFrame) === null || _a === void 0 ? void 0 : _a.call(view, apply);
6954
- const fonts = target.ownerDocument.fonts;
6955
- if (fonts === null || fonts === void 0 ? void 0 : fonts.ready)
6956
- fonts.ready.then(apply).catch(() => { });
6957
- (_b = m.fitObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
6958
- m.fitObserver = null;
6959
- if (first && typeof (view === null || view === void 0 ? void 0 : view.ResizeObserver) === "function") {
6960
- const ro = new view.ResizeObserver(() => apply());
6961
- ro.observe(first);
6962
- m.fitObserver = ro;
7169
+ }
7170
+ function setAuthLoading(loading) {
7171
+ if (isAuthLoading !== loading) {
7172
+ isAuthLoading = loading;
7173
+ authLoadingListeners.forEach((callback) => callback(loading));
6963
7174
  }
6964
7175
  }
6965
- /** Switch the modal into NATIVE-content mode: detach any iframe, mount `node`. */
6966
- function mountContent(m, node) {
6967
- var _a;
6968
- detachIframe(m);
6969
- (_a = m.fitObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
6970
- m.fitObserver = null;
6971
- m.content.replaceChildren(node);
6972
- m.content.style.display = "block";
7176
+ function getAuthLoading() {
7177
+ return isAuthLoading;
6973
7178
  }
6974
- /**
6975
- * Destroy the warm signer iframe if one is currently mounted for `origin`, and report
6976
- * whether there was one. Logout needs this: a signing session can live in that document's
6977
- * MEMORY (the signer keeps an in-memory mirror when the browser blocks its storage), and a
6978
- * throwaway iframe opened by the sweep can only ever clear the shared store, never another
6979
- * document's memory. Removing the iframe discards its browsing context, which takes the
6980
- * mirror with it.
6981
- *
6982
- * Deliberately does NOT call getModal(): that would BUILD a modal just to tear it down, on
6983
- * every logout of every app, most of which never mounted one.
6984
- */
6985
- function detachWarmSignerIframe(origin) {
6986
- if (!modal || !modal.iframe || modal.iframeOrigin !== origin)
6987
- return false;
6988
- detachIframe(modal);
6989
- return true;
7179
+ async function login(options) {
7180
+ if (!authProviderInstance) {
7181
+ throw new Error('SDK not initialized. Please call init() first.');
7182
+ }
7183
+ const loggedInUser = await login$1(options);
7184
+ setCurrentUser(loggedInUser);
7185
+ return currentUser;
6990
7186
  }
6991
- function detachIframe(m) {
6992
- if (m.iframe) {
6993
- try {
6994
- m.iframe.remove();
6995
- }
6996
- catch (_a) {
6997
- /* ignore */
7187
+ async function logout(options) {
7188
+ if (!authProviderInstance) {
7189
+ throw new Error('SDK not initialized. Please call init() first.');
7190
+ }
7191
+ await logout$1(options);
7192
+ setCurrentUser(null);
7193
+ }
7194
+ var hasRunFirstTime = false;
7195
+ function userPrincipalKey(user) {
7196
+ var _a, _b, _c;
7197
+ if (!user)
7198
+ return null;
7199
+ return (_c = (_b = (_a = user.id) !== null && _a !== void 0 ? _a : user.address) !== null && _b !== void 0 ? _b : user.email) !== null && _c !== void 0 ? _c : null;
7200
+ }
7201
+ // Update the currentUser and notify listeners
7202
+ function setCurrentUser(user) {
7203
+ // Enrich with the universal identity fields (id/email) derived from the
7204
+ // active session's idToken — central chokepoint so every provider's user
7205
+ // object gains { id, address, email } without each construction site
7206
+ // having to set them.
7207
+ user = withUserIdentity(user);
7208
+ const lastUser = currentUser;
7209
+ const wasInitializedBefore = hasRunFirstTime;
7210
+ const lastPrincipal = userPrincipalKey(currentUser);
7211
+ const nextPrincipal = userPrincipalKey(user);
7212
+ const shouldRunListeners = !hasRunFirstTime || lastPrincipal !== nextPrincipal;
7213
+ hasRunFirstTime = true;
7214
+ currentUser = user;
7215
+ if (shouldRunListeners) {
7216
+ // Isolate each listener: a consumer callback that throws (during logout in
7217
+ // particular) must not abort the notification loop OR skip the socket reopen
7218
+ // below. Before this, a single throwing listener propagated out of
7219
+ // setCurrentUser, so the realtime socket kept streaming as the signed-out
7220
+ // user and the other listeners were never told the principal changed.
7221
+ authStateListeners.forEach((callback) => {
7222
+ try {
7223
+ callback(user);
7224
+ }
7225
+ catch (error) {
7226
+ console.error('Error in onAuthStateChanged listener:', error);
7227
+ }
7228
+ });
7229
+ // Reconnect WebSocket connections with new auth when user changes.
7230
+ // Skip on the first setCurrentUser call during init() — there are no
7231
+ // existing connections to reconnect at that point.
7232
+ if (wasInitializedBefore && userPrincipalKey(lastUser) !== nextPrincipal) {
7233
+ reconnectWithNewAuth().catch((error) => {
7234
+ console.error('Error reconnecting WebSocket after auth change:', error);
7235
+ });
6998
7236
  }
6999
7237
  }
7000
- m.iframe = null;
7001
- m.iframeReady = false;
7002
- m.iframeOrigin = "";
7003
- m.onIframeMessage = null;
7238
+ // Return this commit's normalized snapshot. A listener can start another
7239
+ // login, so the mutable currentUser may already belong to a newer session.
7240
+ return user;
7004
7241
  }
7005
- // Load the Bounded editorial typefaces (Newsreader display + Schibsted Grotesk body +
7006
- // JetBrains Mono) once at the document level so they apply inside the shadow root. A
7007
- // no-op if already present or off-DOM. Fonts loaded here match the hosted auth page.
7008
- const FONTS_HREF = "https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght@6..72,400;6..72,500&family=Schibsted+Grotesk:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap";
7009
- function ensureFonts() {
7010
- if (!hasDOM$2())
7011
- return;
7012
- if (document.querySelector('link[data-bounded-fonts="1"]'))
7013
- return;
7014
- const pre1 = document.createElement("link");
7015
- pre1.rel = "preconnect";
7016
- pre1.href = "https://fonts.googleapis.com";
7017
- const pre2 = document.createElement("link");
7018
- pre2.rel = "preconnect";
7019
- pre2.href = "https://fonts.gstatic.com";
7020
- pre2.crossOrigin = "anonymous";
7021
- const link = document.createElement("link");
7022
- link.rel = "stylesheet";
7023
- link.href = FONTS_HREF;
7024
- link.setAttribute("data-bounded-fonts", "1");
7025
- document.head.append(pre1, pre2, link);
7242
+ function getCurrentUser() {
7243
+ return currentUser;
7244
+ }
7245
+ /** @internal Snapshot/restore surface for transactional auth-provider switches. */
7246
+ function getAuthProviderInstance() {
7247
+ return authProviderInstance;
7248
+ }
7249
+ function setAuthProviderInstance(provider) {
7250
+ authProviderInstance = provider;
7026
7251
  }
7027
-
7028
- var boundedWidgetModal = /*#__PURE__*/Object.freeze({
7029
- __proto__: null,
7030
- detachIframe: detachIframe,
7031
- detachWarmSignerIframe: detachWarmSignerIframe,
7032
- ensureFonts: ensureFonts,
7033
- fitPanelToContent: fitPanelToContent,
7034
- getModal: getModal,
7035
- hasDOM: hasDOM$2,
7036
- hideModal: hideModal,
7037
- mountContent: mountContent,
7038
- setPanelHeight: setPanelHeight,
7039
- showModal: showModal
7040
- });
7041
7252
 
7042
7253
  // bounded-widget-ownership.ts - per-open ownership generation for the singleton
7043
7254
  // login/signing modal (bounded-widget-modal.ts).
@@ -7498,7 +7709,7 @@ async function startTurnkeyEmailLogin(email) {
7498
7709
  const res = await fetch(`${base}/wallet/turnkey/auth/init`, {
7499
7710
  method: 'POST',
7500
7711
  headers: { 'content-type': 'application/json' },
7501
- body: JSON.stringify({ email: trimmed, appId }),
7712
+ body: JSON.stringify({ email: trimmed, appId, walletProtocol: 2 }),
7502
7713
  });
7503
7714
  const data = await res.json().catch(() => null);
7504
7715
  if (!res.ok || !data || typeof data.otpId !== 'string' || typeof data.otpEncryptionTargetBundle !== 'string') {
@@ -7510,13 +7721,23 @@ async function startTurnkeyEmailLogin(email) {
7510
7721
  // (there is nothing to bind the frame key to), which is the correct fail-closed posture.
7511
7722
  const ceremonyId = typeof data.ceremonyId === 'string' ? data.ceremonyId : '';
7512
7723
  const signerOrigin = new URL(base).origin;
7724
+ if (data.walletScope !== 'app' && data.walletScope !== 'legacy')
7725
+ throw new Error('wallet_scope_config_unavailable');
7726
+ if (data.walletProtocol !== (data.walletScope === 'app' ? 2 : 1))
7727
+ throw new Error('wallet_protocol_invalid');
7728
+ const frameUrl = data.walletScope === 'app'
7729
+ ? `${base.replace(/\/$/, '')}/wallet/turnkey/app/${appId}/signer?origin=${encodeURIComponent(window.location.origin)}`
7730
+ : undefined;
7513
7731
  return {
7514
- email: trimmed,
7515
- run(iframe) {
7732
+ email: trimmed, frameUrl, signerOrigin,
7733
+ run(iframe, _signal) {
7516
7734
  if (!ceremonyId) {
7517
7735
  return Promise.reject(new Error('This app is not configured for in-frame wallet login. Please try again.'));
7518
7736
  }
7519
- return runOtpFrame({ iframe, base, signerOrigin, appId, email: trimmed, otpId, otpEncryptionTargetBundle, ceremonyId });
7737
+ const run = { iframe, base, signerOrigin, appId, email: trimmed, otpId, otpEncryptionTargetBundle, ceremonyId };
7738
+ // BSA-2026-004: the login code is typed only in /wallet/turnkey/otp-frame.
7739
+ // The scoped signer URL is for post-login signing, not OTP entry.
7740
+ return runOtpFrame(run);
7520
7741
  },
7521
7742
  };
7522
7743
  }
@@ -7844,7 +8065,7 @@ function untilCancelled(work, pending) {
7844
8065
  * run first; call from a user gesture so social/wallet popups are allowed.
7845
8066
  */
7846
8067
  async function openBoundedWidget(opts = {}) {
7847
- if (!hasDOM$2()) {
8068
+ if (!hasDOM$5()) {
7848
8069
  throw new Error("openBoundedWidget is web-only - on React Native use loginWithRedirect()");
7849
8070
  }
7850
8071
  // Registered before the first await - see PendingOpen.
@@ -8006,6 +8227,7 @@ async function prepareAndOpen(opts, pending) {
8006
8227
  // the underlying promise SETTLES, whoever owns the card by then.
8007
8228
  let attemptSeq = 0;
8008
8229
  let activeAttempt = 0;
8230
+ let abortScopedLogin = null;
8009
8231
  let abortGesture = null;
8010
8232
  /** Reject a gesture promise already waiting on a view that is going away. */
8011
8233
  const abortPendingGesture = () => {
@@ -8037,6 +8259,8 @@ async function prepareAndOpen(opts, pending) {
8037
8259
  /** Take the card back (Back, a lane switch, settling the modal). Deliberately
8038
8260
  * NOT a release: the login underneath keeps running - see above. */
8039
8261
  const invalidateAttempts = () => {
8262
+ abortScopedLogin === null || abortScopedLogin === void 0 ? void 0 : abortScopedLogin();
8263
+ abortScopedLogin = null;
8040
8264
  activeAttempt = 0;
8041
8265
  abortPendingGesture();
8042
8266
  };
@@ -8090,7 +8314,7 @@ async function prepareAndOpen(opts, pending) {
8090
8314
  const style = el("style");
8091
8315
  style.textContent = CARD_CSS;
8092
8316
  let emailDraft = "";
8093
- const renderChoose = () => {
8317
+ const renderChoose = (initialError) => {
8094
8318
  const card = el("div", "c");
8095
8319
  card.appendChild(style.cloneNode(true));
8096
8320
  const x = el("button", "x", "Close");
@@ -8100,13 +8324,15 @@ async function prepareAndOpen(opts, pending) {
8100
8324
  card.appendChild(x);
8101
8325
  card.appendChild(el("h1", undefined, title !== null && title !== void 0 ? title : "Sign in"));
8102
8326
  if (subtitle !== "") {
8103
- card.appendChild(el("p", "sub", subtitle !== null && subtitle !== void 0 ? subtitle : "One account and wallet across every Bounded app."));
8327
+ card.appendChild(el("p", "sub", subtitle !== null && subtitle !== void 0 ? subtitle : "Sign in to continue to this app."));
8104
8328
  }
8105
8329
  const status = el("div", "status");
8106
8330
  const setErr = (msg) => {
8107
8331
  status.textContent = msg;
8108
8332
  status.classList.add("err");
8109
8333
  };
8334
+ if (initialError)
8335
+ setErr(initialError);
8110
8336
  const setBusy = (btn, label) => {
8111
8337
  status.classList.remove("err");
8112
8338
  status.textContent = "";
@@ -8150,11 +8376,25 @@ async function prepareAndOpen(opts, pending) {
8150
8376
  // arrives; the code entry claims its own when the user verifies.
8151
8377
  setBusy(submit, "Sending code\u2026");
8152
8378
  startTurnkeyEmailLogin(input.value)
8153
- .then((handle) => { if (settleAttempt(attempt))
8154
- swap(renderOtpFrame(handle)); })
8379
+ .then(async (handle) => {
8380
+ if (!attemptIsCurrent(attempt)) {
8381
+ settleAttempt(attempt);
8382
+ return;
8383
+ }
8384
+ // BSA-2026-004: collect the code in the Bounded OTP frame, never in
8385
+ // this tenant DOM and never in the unframeable signer document.
8386
+ if (settleAttempt(attempt))
8387
+ swap(renderOtpFrame(handle));
8388
+ })
8155
8389
  .catch((err) => {
8156
8390
  if (!settleAttempt(attempt))
8157
8391
  return;
8392
+ if (abortScopedLogin) {
8393
+ abortScopedLogin = null;
8394
+ detachIframe(m);
8395
+ swap(renderChoose(errText(err)));
8396
+ return;
8397
+ }
8158
8398
  submit.disabled = false;
8159
8399
  submit.textContent = "Continue";
8160
8400
  setErr(errText(err));
@@ -10119,5 +10359,312 @@ function defaultInjectedEvmProvider() {
10119
10359
  return null;
10120
10360
  }
10121
10361
 
10122
- export { OffchainAuthProvider 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, startTurnkeyEmailLogin as U, getIdToken as V, WalletConfigError as W, onramp as X, onrampQuote as Y, onrampStatus as Z, setPlatform as _, signMessageWithTurnkey as a, PrivyExpoProvider as a0, requestEip6963Providers as a1, builds as a2, BuildsApiError as a3, NON_TERMINAL_STATES as a4, TERMINAL_STATES as a5, RUN_STATES as a6, PARK_REASONS as a7, FAILURE_REASONS as a8, BUILD_REVIEW_CAPABILITY_SCHEMA as a9, BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA as aa, BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA as ab, 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 };
10123
- //# sourceMappingURL=index-Cflemdaf.mjs.map
10362
+ /** Admission comes from an app-authenticated function invoking the Cloudflare
10363
+ * broadcast connector. No application membership is managed by this helper. */
10364
+ async function connectBroadcast(ticket, options) {
10365
+ var _a, _b, _c, _d;
10366
+ const url = new URL(ticket.url);
10367
+ if (url.username || url.password || url.search || url.hash || (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))))
10368
+ throw new Error("Invalid broadcast URL");
10369
+ if (!/^[a-f0-9]{64}$/.test(ticket.token) || ticket.transport !== "webrtc-broadcast" || !Number.isFinite(ticket.expiresAt) || ticket.expiresAt <= Date.now()
10370
+ || !Number.isInteger(ticket.maxSeconds) || ticket.maxSeconds < 15 || ticket.maxSeconds > 300
10371
+ || !["publisher", "listener"].includes(ticket.role) || ((_a = options.signal) === null || _a === void 0 ? void 0 : _a.aborted))
10372
+ throw new Error("Invalid or expired broadcast ticket");
10373
+ const publisher = ticket.role === "publisher";
10374
+ const tracks = publisher ? [(_b = options.microphone) === null || _b === void 0 ? void 0 : _b.getAudioTracks()[0], (_c = options.assistant) === null || _c === void 0 ? void 0 : _c.getAudioTracks()[0]] : [];
10375
+ if (publisher && tracks.some(track => !track || track.readyState !== "live"))
10376
+ throw new Error("Microphone and assistant audio tracks are required");
10377
+ if (!publisher && !options.audio)
10378
+ throw new Error("Two playback elements are required");
10379
+ const peer = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.cloudflare.com:3478" }] });
10380
+ let stopped = false, stopPromise;
10381
+ let resolveClosed;
10382
+ const closed = new Promise(resolve => { resolveClosed = resolve; });
10383
+ let disconnectTimer;
10384
+ const request = async (method, body) => {
10385
+ const response = await fetch(ticket.url, Object.assign(Object.assign({ method, headers: { Authorization: `Bearer ${ticket.token}`, "Content-Type": "application/json" } }, (body === undefined ? {} : { body: JSON.stringify(body) })), { signal: AbortSignal.timeout(15000), redirect: "error", cache: "no-store" }));
10386
+ if (!response.ok)
10387
+ throw new Error(`Broadcast request failed (${response.status})`);
10388
+ return response.json();
10389
+ };
10390
+ const connection = {
10391
+ closed,
10392
+ status: () => request("GET"),
10393
+ close() {
10394
+ var _a, _b;
10395
+ if (stopPromise)
10396
+ return stopPromise;
10397
+ stopped = true;
10398
+ clearTimeout(deadline);
10399
+ clearTimeout(disconnectTimer);
10400
+ peer.close();
10401
+ (_a = options.signal) === null || _a === void 0 ? void 0 : _a.removeEventListener("abort", abort);
10402
+ tracks.forEach(t => t === null || t === void 0 ? void 0 : t.removeEventListener("ended", abort));
10403
+ (_b = options.audio) === null || _b === void 0 ? void 0 : _b.forEach(audio => { audio.pause(); audio.srcObject = null; });
10404
+ resolveClosed();
10405
+ // These tracks also belong to the primary voice connection. Closing the
10406
+ // broadcast must not stop the microphone or assistant's direct playback.
10407
+ stopPromise = (async () => {
10408
+ for (let attempt = 0; attempt < 3; attempt++) {
10409
+ try {
10410
+ await request("DELETE");
10411
+ return;
10412
+ }
10413
+ catch (error) {
10414
+ if (attempt === 2)
10415
+ throw error;
10416
+ await new Promise(resolve => setTimeout(resolve, 250 * (attempt + 1)));
10417
+ }
10418
+ }
10419
+ })();
10420
+ return stopPromise;
10421
+ },
10422
+ };
10423
+ const abort = () => { void connection.close().catch(() => { }); };
10424
+ const deadline = setTimeout(abort, ticket.maxSeconds * 1000);
10425
+ (_d = options.signal) === null || _d === void 0 ? void 0 : _d.addEventListener("abort", abort, { once: true });
10426
+ tracks.forEach(track => track === null || track === void 0 ? void 0 : track.addEventListener("ended", abort, { once: true }));
10427
+ let audioMids = [];
10428
+ peer.addEventListener("track", event => {
10429
+ var _a;
10430
+ const audio = (_a = options.audio) === null || _a === void 0 ? void 0 : _a[audioMids.indexOf(event.transceiver.mid || "")];
10431
+ if (!audio) {
10432
+ abort();
10433
+ return;
10434
+ }
10435
+ audio.srcObject = new MediaStream([event.track]);
10436
+ void audio.play().catch(abort);
10437
+ });
10438
+ peer.addEventListener("connectionstatechange", () => {
10439
+ clearTimeout(disconnectTimer);
10440
+ if (peer.connectionState === "failed")
10441
+ abort();
10442
+ else if (peer.connectionState === "disconnected")
10443
+ disconnectTimer = setTimeout(abort, 5000);
10444
+ });
10445
+ try {
10446
+ let result;
10447
+ if (publisher) {
10448
+ for (const track of tracks)
10449
+ peer.addTransceiver(track, { direction: "sendonly", sendEncodings: [{ maxBitrate: 32000 }] });
10450
+ await peer.setLocalDescription(await peer.createOffer());
10451
+ result = await request("POST", { sdp: peer.localDescription.sdp });
10452
+ if (stopped)
10453
+ throw new Error("Broadcast cancelled");
10454
+ await peer.setRemoteDescription(result.sessionDescription);
10455
+ }
10456
+ else {
10457
+ result = await request("POST", {});
10458
+ if (stopped)
10459
+ throw new Error("Broadcast cancelled");
10460
+ audioMids = ["microphone", "assistant"].map(name => { var _a, _b; return ((_b = (_a = result.tracks) === null || _a === void 0 ? void 0 : _a.find(track => track.trackName === name)) === null || _b === void 0 ? void 0 : _b.mid) || ""; });
10461
+ if (audioMids.some(mid => !mid) || new Set(audioMids).size !== 2)
10462
+ throw new Error("Invalid broadcast audio tracks");
10463
+ await peer.setRemoteDescription(result.sessionDescription);
10464
+ await peer.setLocalDescription(await peer.createAnswer());
10465
+ await request("PUT", { sdp: peer.localDescription.sdp });
10466
+ }
10467
+ await new Promise((resolve, reject) => {
10468
+ var _a;
10469
+ const timer = setTimeout(() => done(new Error("Broadcast connection timed out")), 15000);
10470
+ const check = () => {
10471
+ if (stopped || ["failed", "closed"].includes(peer.connectionState))
10472
+ done(new Error("Broadcast connection ended"));
10473
+ else if (peer.connectionState === "connected")
10474
+ done();
10475
+ };
10476
+ const done = (error) => { var _a; clearTimeout(timer); peer.removeEventListener("connectionstatechange", check); (_a = options.signal) === null || _a === void 0 ? void 0 : _a.removeEventListener("abort", check); error ? reject(error) : resolve(); };
10477
+ peer.addEventListener("connectionstatechange", check);
10478
+ (_a = options.signal) === null || _a === void 0 ? void 0 : _a.addEventListener("abort", check, { once: true });
10479
+ check();
10480
+ });
10481
+ if (stopped)
10482
+ throw new Error("Broadcast cancelled");
10483
+ return connection;
10484
+ }
10485
+ catch (error) {
10486
+ await connection.close().catch(() => { });
10487
+ throw error;
10488
+ }
10489
+ }
10490
+
10491
+ /** Attach browser media directly to the connector's provider. Bounded observes
10492
+ * usage independently; data-channel messages never count as billing receipts. */
10493
+ async function connectServiceSession(ticket, options) {
10494
+ var _a, _b, _c;
10495
+ const url = new URL(ticket.url);
10496
+ if (url.username || url.password || url.search || url.hash || (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))))
10497
+ throw new Error("Invalid service session URL");
10498
+ if (!/^[a-f0-9]{64}$/.test(ticket.token) || ticket.transport !== "webrtc-session" || !Number.isFinite(ticket.expiresAt) || ticket.expiresAt <= Date.now())
10499
+ throw new Error("Invalid or expired service session ticket");
10500
+ if ((_a = options.signal) === null || _a === void 0 ? void 0 : _a.aborted)
10501
+ throw new Error("Service session aborted");
10502
+ const peer = new RTCPeerConnection();
10503
+ const audioStream = new MediaStream();
10504
+ const channel = peer.createDataChannel("oai-events");
10505
+ let ready = false, finished = false, closing = false;
10506
+ let resolveClosed, rejectClosed;
10507
+ let resolveReady, rejectReady;
10508
+ let pollTimer;
10509
+ const closed = new Promise((resolve, reject) => { resolveClosed = resolve; rejectClosed = reject; });
10510
+ void closed.catch(() => { });
10511
+ const connected = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
10512
+ void connected.catch(() => { });
10513
+ const request = async (method, body) => {
10514
+ const response = await fetch(ticket.url, Object.assign(Object.assign({ method, headers: Object.assign({ Authorization: `Bearer ${ticket.token}` }, (body === undefined ? {} : { "Content-Type": "application/json" })) }, (body === undefined ? {} : { body: JSON.stringify(body) })), { signal: AbortSignal.timeout(15000), cache: "no-store", redirect: "error" }));
10515
+ if (!response.ok)
10516
+ throw new Error(`Service session request failed (${response.status})`);
10517
+ return response.json();
10518
+ };
10519
+ const cleanup = () => {
10520
+ var _a;
10521
+ clearTimeout(startTimer);
10522
+ clearTimeout(pollTimer);
10523
+ (_a = options.signal) === null || _a === void 0 ? void 0 : _a.removeEventListener("abort", abort);
10524
+ options.stream.getTracks().forEach(track => track.stop());
10525
+ options.audio.pause();
10526
+ options.audio.srcObject = null;
10527
+ channel.close();
10528
+ peer.close();
10529
+ };
10530
+ const fail = (error) => {
10531
+ if (finished)
10532
+ return;
10533
+ finished = true;
10534
+ rejectReady(error);
10535
+ rejectClosed(error);
10536
+ cleanup();
10537
+ };
10538
+ const poll = async (until) => {
10539
+ if (finished)
10540
+ return;
10541
+ try {
10542
+ const state = await request("GET");
10543
+ if (state.state === "closed" && state.finalUsage === true && Number.isFinite(state.seconds) && state.seconds >= 0 && Number.isSafeInteger(state.chargedMicroUsd) && state.chargedMicroUsd >= 0) {
10544
+ finished = true;
10545
+ resolveClosed({ seconds: state.seconds, chargedMicroUsd: state.chargedMicroUsd, finalUsage: true });
10546
+ if (!ready)
10547
+ rejectReady(new Error("Service session ended before startup"));
10548
+ cleanup();
10549
+ return;
10550
+ }
10551
+ if (["waiting", "creating", "active"].includes(state.state))
10552
+ await request("DELETE");
10553
+ if (state.state === "attention") {
10554
+ fail(new Error("Service session requires reconciliation"));
10555
+ return;
10556
+ }
10557
+ }
10558
+ catch ( /* bounded retries; never invent a receipt after a network failure */_a) { /* bounded retries; never invent a receipt after a network failure */ }
10559
+ if (Date.now() >= until) {
10560
+ fail(new Error("Service session finalization unconfirmed; check session status"));
10561
+ return;
10562
+ }
10563
+ pollTimer = setTimeout(() => { void poll(until); }, 500);
10564
+ };
10565
+ const session = {
10566
+ audioStream,
10567
+ closed,
10568
+ send(event) {
10569
+ if (!ready || closing || finished || channel.readyState !== "open")
10570
+ throw new Error("Service session is not accepting messages");
10571
+ const json = JSON.stringify(event);
10572
+ if (new TextEncoder().encode(json).byteLength > 65536 || channel.bufferedAmount > 256000)
10573
+ throw new Error("Service session backpressure limit");
10574
+ channel.send(json);
10575
+ },
10576
+ close() {
10577
+ if (!closing && !finished) {
10578
+ closing = true;
10579
+ options.stream.getTracks().forEach(track => track.stop());
10580
+ options.audio.pause();
10581
+ options.audio.srcObject = null;
10582
+ // Keep WebRTC alive while the server collects final usage. HTTP close
10583
+ // also works when the provider data channel is already gone.
10584
+ void request("DELETE").catch(() => { }).then(() => poll(Date.now() + 45000));
10585
+ }
10586
+ return closed;
10587
+ },
10588
+ };
10589
+ const abort = () => { void session.close(); };
10590
+ const startTimer = setTimeout(() => { void request("DELETE").catch(() => { }); fail(new Error("Service session startup timed out")); }, 30000);
10591
+ (_b = options.signal) === null || _b === void 0 ? void 0 : _b.addEventListener("abort", abort, { once: true });
10592
+ peer.addEventListener("track", ({ track }) => { if (closing || finished)
10593
+ return; audioStream.addTrack(track); options.audio.srcObject = audioStream; void options.audio.play().catch(() => { void session.close(); fail(new Error("Audio playback was blocked")); }); });
10594
+ channel.addEventListener("message", ({ data }) => {
10595
+ var _a;
10596
+ if (finished || closing)
10597
+ return;
10598
+ let event;
10599
+ try {
10600
+ event = JSON.parse(String(data));
10601
+ }
10602
+ catch (_b) {
10603
+ return;
10604
+ }
10605
+ if ((event === null || event === void 0 ? void 0 : event.type) === "session.started" && !ready) {
10606
+ ready = true;
10607
+ clearTimeout(startTimer);
10608
+ resolveReady();
10609
+ }
10610
+ if ((event === null || event === void 0 ? void 0 : event.type) === "session.closed")
10611
+ void session.close();
10612
+ try {
10613
+ (_a = options.onEvent) === null || _a === void 0 ? void 0 : _a.call(options, event);
10614
+ }
10615
+ catch ( /* UI callback failure cannot own cleanup */_c) { /* UI callback failure cannot own cleanup */ }
10616
+ });
10617
+ peer.addEventListener("connectionstatechange", () => { if (["failed", "closed"].includes(peer.connectionState) && !finished)
10618
+ void session.close(); });
10619
+ channel.addEventListener("close", () => { if (!finished)
10620
+ void session.close(); });
10621
+ try {
10622
+ for (const track of options.stream.getAudioTracks())
10623
+ peer.addTrack(track, options.stream);
10624
+ if (!options.stream.getAudioTracks().length)
10625
+ throw new Error("An audio track is required");
10626
+ await peer.setLocalDescription(await peer.createOffer());
10627
+ if (peer.iceGatheringState !== "complete")
10628
+ await new Promise((resolve, reject) => {
10629
+ var _a;
10630
+ const timer = setTimeout(() => done(new Error("Service session ICE gathering timed out")), 10000);
10631
+ const done = (error) => {
10632
+ var _a;
10633
+ clearTimeout(timer);
10634
+ peer.removeEventListener("icegatheringstatechange", check);
10635
+ peer.removeEventListener("connectionstatechange", check);
10636
+ (_a = options.signal) === null || _a === void 0 ? void 0 : _a.removeEventListener("abort", check);
10637
+ error ? reject(error) : resolve();
10638
+ };
10639
+ const check = () => {
10640
+ var _a;
10641
+ if (closing || finished || ((_a = options.signal) === null || _a === void 0 ? void 0 : _a.aborted) || peer.connectionState === "closed")
10642
+ done(new Error("Service session aborted during ICE gathering"));
10643
+ else if (peer.iceGatheringState === "complete")
10644
+ done();
10645
+ };
10646
+ peer.addEventListener("icegatheringstatechange", check);
10647
+ peer.addEventListener("connectionstatechange", check);
10648
+ (_a = options.signal) === null || _a === void 0 ? void 0 : _a.addEventListener("abort", check, { once: true });
10649
+ check();
10650
+ });
10651
+ if (closing || finished)
10652
+ throw new Error("Service session aborted before admission");
10653
+ if (!((_c = peer.localDescription) === null || _c === void 0 ? void 0 : _c.sdp.includes("\r\na=candidate:")))
10654
+ throw new Error("Service session has no usable ICE candidates");
10655
+ const answer = await request("POST", { sdp: peer.localDescription.sdp });
10656
+ if (closing || finished)
10657
+ throw new Error("Service session aborted during admission");
10658
+ await peer.setRemoteDescription({ type: "answer", sdp: answer.sdp });
10659
+ await connected;
10660
+ return session;
10661
+ }
10662
+ catch (error) {
10663
+ void request("DELETE").catch(() => { });
10664
+ fail(error instanceof Error ? error : new Error("Service session startup failed"));
10665
+ throw error;
10666
+ }
10667
+ }
10668
+
10669
+ 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, NON_TERMINAL_STATES as a8, TERMINAL_STATES as a9, RUN_STATES as aa, PARK_REASONS as ab, FAILURE_REASONS as ac, BUILD_REVIEW_CAPABILITY_SCHEMA as ad, BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA as ae, BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA as af, 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 };
10670
+ //# sourceMappingURL=index-l5E4pjpt.mjs.map