@bounded-sh/client 0.0.98 → 0.0.100-performance.0

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 (68) 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 +4 -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/types.d.ts +31 -0
  10. package/dist/{email-auth-provider-DF7Urh8i.mjs → email-auth-provider-BGTfU1_i.mjs} +2 -2
  11. package/dist/{email-auth-provider-CiPAD3vz.mjs.map → email-auth-provider-BGTfU1_i.mjs.map} +1 -1
  12. package/dist/{email-auth-provider-DxB_pJKO.js → email-auth-provider-C6SFJd9L.js} +2 -2
  13. package/dist/{email-auth-provider-DxB_pJKO.js.map → email-auth-provider-C6SFJd9L.js.map} +1 -1
  14. package/dist/{email-auth-provider-CiPAD3vz.mjs → email-auth-provider-CIRbOSM3.mjs} +2 -2
  15. package/dist/{email-auth-provider-DF7Urh8i.mjs.map → email-auth-provider-CIRbOSM3.mjs.map} +1 -1
  16. package/dist/{email-auth-provider-BhYzRN0r.js → email-auth-provider-Cpj8hIFu.js} +2 -2
  17. package/dist/{email-auth-provider-BhYzRN0r.js.map → email-auth-provider-Cpj8hIFu.js.map} +1 -1
  18. package/dist/{guest-auth-provider-BiM35KHb.js → guest-auth-provider-0PjGFGDm.js} +2 -2
  19. package/dist/{guest-auth-provider-BiM35KHb.js.map → guest-auth-provider-0PjGFGDm.js.map} +1 -1
  20. package/dist/{guest-auth-provider-Dtsq4QaY.mjs → guest-auth-provider-C23vWrOT.mjs} +2 -2
  21. package/dist/{guest-auth-provider-DVw-tITC.mjs.map → guest-auth-provider-C23vWrOT.mjs.map} +1 -1
  22. package/dist/{guest-auth-provider-DVw-tITC.mjs → guest-auth-provider-DVxlea-z.mjs} +2 -2
  23. package/dist/{guest-auth-provider-Dtsq4QaY.mjs.map → guest-auth-provider-DVxlea-z.mjs.map} +1 -1
  24. package/dist/{guest-auth-provider-B9C3Iub_.js → guest-auth-provider-pC-AheTZ.js} +2 -2
  25. package/dist/{guest-auth-provider-B9C3Iub_.js.map → guest-auth-provider-pC-AheTZ.js.map} +1 -1
  26. package/dist/{index-CYdYsf8s.js → index-DUVRmF9-.js} +971 -606
  27. package/dist/index-DUVRmF9-.js.map +1 -0
  28. package/dist/{index-Cflemdaf.mjs → index-DmAXYXDi.mjs} +970 -607
  29. package/dist/index-DmAXYXDi.mjs.map +1 -0
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +3 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +1 -1
  34. package/dist/{index.native-BZOWS_sR.js → index.native-CllNqdoy.js} +442 -81
  35. package/dist/index.native-CllNqdoy.js.map +1 -0
  36. package/dist/{index.native-gVbAsohv.mjs → index.native-r1I-9E8e.mjs} +442 -81
  37. package/dist/index.native-r1I-9E8e.mjs.map +1 -0
  38. package/dist/index.native.js +1 -1
  39. package/dist/index.native.mjs +1 -1
  40. package/dist/{injected-evm-wallet-provider-CMqUqqxN.mjs → injected-evm-wallet-provider-BPZMEE8I.mjs} +2 -2
  41. package/dist/{injected-evm-wallet-provider-CMqUqqxN.mjs.map → injected-evm-wallet-provider-BPZMEE8I.mjs.map} +1 -1
  42. package/dist/{injected-evm-wallet-provider-C3MwUTRd.js → injected-evm-wallet-provider-BXM8TfS3.js} +2 -2
  43. package/dist/{injected-evm-wallet-provider-C3MwUTRd.js.map → injected-evm-wallet-provider-BXM8TfS3.js.map} +1 -1
  44. package/dist/{injected-evm-wallet-provider-K0j697jt.mjs → injected-evm-wallet-provider-CkhN5u3d.mjs} +2 -2
  45. package/dist/{injected-evm-wallet-provider-K0j697jt.mjs.map → injected-evm-wallet-provider-CkhN5u3d.mjs.map} +1 -1
  46. package/dist/{injected-evm-wallet-provider-Bv8VLuB1.js → injected-evm-wallet-provider-Dq3FDV1M.js} +2 -2
  47. package/dist/{injected-evm-wallet-provider-Bv8VLuB1.js.map → injected-evm-wallet-provider-Dq3FDV1M.js.map} +1 -1
  48. package/dist/{injected-wallet-provider-BDt4CN0b.js → injected-wallet-provider-B3LLEpfp.js} +2 -2
  49. package/dist/injected-wallet-provider-B3LLEpfp.js.map +1 -0
  50. package/dist/{injected-wallet-provider-CG3oKdp_.js → injected-wallet-provider-Bi_mB_q3.js} +2 -2
  51. package/dist/injected-wallet-provider-Bi_mB_q3.js.map +1 -0
  52. package/dist/{injected-wallet-provider-DH8yLpyf.mjs → injected-wallet-provider-BlSdRbgq.mjs} +2 -2
  53. package/dist/injected-wallet-provider-BlSdRbgq.mjs.map +1 -0
  54. package/dist/{injected-wallet-provider-Dlo96aV9.mjs → injected-wallet-provider-CLLhLBAJ.mjs} +2 -2
  55. package/dist/injected-wallet-provider-CLLhLBAJ.mjs.map +1 -0
  56. package/package.json +7 -3
  57. package/dist/bounded-widget-modal-C0Z7gmxH.mjs +0 -31
  58. package/dist/bounded-widget-modal-C0Z7gmxH.mjs.map +0 -1
  59. package/dist/bounded-widget-modal-s0PL86Y-.js +0 -33
  60. package/dist/bounded-widget-modal-s0PL86Y-.js.map +0 -1
  61. package/dist/index-CYdYsf8s.js.map +0 -1
  62. package/dist/index-Cflemdaf.mjs.map +0 -1
  63. package/dist/index.native-BZOWS_sR.js.map +0 -1
  64. package/dist/index.native-gVbAsohv.mjs.map +0 -1
  65. package/dist/injected-wallet-provider-BDt4CN0b.js.map +0 -1
  66. package/dist/injected-wallet-provider-CG3oKdp_.js.map +0 -1
  67. package/dist/injected-wallet-provider-DH8yLpyf.mjs.map +0 -1
  68. package/dist/injected-wallet-provider-Dlo96aV9.mjs.map +0 -1
@@ -22,6 +22,60 @@ function _interopNamespaceDefault(e) {
22
22
 
23
23
  var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
24
24
 
25
+ function walletContextFromClaims(claims, baseUrl, expectedAppId, tenantOrigin) {
26
+ const base = baseUrl.replace(/\/$/, '');
27
+ const scope = claims['custom:walletScope'];
28
+ const protocol = claims['custom:walletProtocol'];
29
+ if (scope !== undefined && scope !== 'legacy' && scope !== 'app')
30
+ throw new Error('wallet_scope_invalid');
31
+ if ((scope === 'app' && protocol !== 2) || (scope === 'legacy' && protocol !== 1)
32
+ || (scope === undefined && protocol !== undefined))
33
+ throw new Error('wallet_protocol_invalid');
34
+ const appId = claims['custom:appId'];
35
+ const userId = claims['custom:userId'];
36
+ const address = claims['custom:walletAddress'];
37
+ if (scope === 'app' && (appId !== expectedAppId || typeof appId !== 'string'
38
+ || !/^[a-f0-9]{24}$/.test(appId) || typeof userId !== 'string' || !userId
39
+ || claims.iss !== base))
40
+ throw new Error('wallet_session_context_invalid');
41
+ const resolvedScope = scope === 'app' ? 'app' : 'legacy';
42
+ const route = resolvedScope === 'app' ? `/wallet/turnkey/app/${appId}` : '/wallet/turnkey';
43
+ return {
44
+ scope: resolvedScope, appId: String(appId || expectedAppId), userId: String(userId || ''),
45
+ address: typeof address === 'string' ? address : '', base, origin: new URL(base).origin,
46
+ url: `${base}${route}/signer${resolvedScope === 'app' ? `?origin=${encodeURIComponent(tenantOrigin)}` : ''}`,
47
+ key: JSON.stringify([base, appId || expectedAppId, userId || '', address || '', resolvedScope]),
48
+ };
49
+ }
50
+ let loginGeneration = 0;
51
+ function invalidatePendingWalletLogin() { ++loginGeneration; }
52
+ function walletOperationGuard() {
53
+ const generation = loginGeneration;
54
+ return () => {
55
+ if (generation !== loginGeneration)
56
+ throw new Error('wallet_session_changed');
57
+ };
58
+ }
59
+ function walletLoginGuard(signal) {
60
+ const generation = ++loginGeneration;
61
+ return () => {
62
+ if ((signal === null || signal === void 0 ? void 0 : signal.aborted) || generation !== loginGeneration)
63
+ throw new Error('wallet_login_cancelled');
64
+ };
65
+ }
66
+ // This realm owns one active SDK session and one retained signer frame.
67
+ let activeWalletState;
68
+ function setWalletSessionState(key, state) { activeWalletState = { key, state }; }
69
+ function walletSessionState(key) {
70
+ if ((activeWalletState === null || activeWalletState === void 0 ? void 0 : activeWalletState.key) !== key)
71
+ return { status: 'verification-required' };
72
+ const state = activeWalletState.state;
73
+ if (state.status === 'ready' && (!Number.isFinite(state.expiresAt) || state.expiresAt <= Date.now())) {
74
+ return { status: 'verification-required', reason: 'wallet_session_expired' };
75
+ }
76
+ return state;
77
+ }
78
+
25
79
  /**
26
80
  * Platform abstraction layer for React Native compatibility.
27
81
  *
@@ -965,7 +1019,10 @@ function registerAuthRegistrySync(fn) {
965
1019
  * Returns the normalized user published by this commit, or null when omitted.
966
1020
  */
967
1021
  async function adoptSessionProvider(opts) {
1022
+ var _a;
968
1023
  const coreConfig = await core.getConfig();
1024
+ (_a = opts.beforeCommit) === null || _a === void 0 ? void 0 : _a.call(opts);
1025
+ invalidatePendingWalletLogin();
969
1026
  coreConfig.authProvider = opts.provider;
970
1027
  coreConfig.authMethod = opts.method;
971
1028
  setAuthProviderInstance(opts.provider);
@@ -4199,145 +4256,562 @@ async function ensureSolanaMobileWalletRegistered(config, network) {
4199
4256
 
4200
4257
  var bufferExports = requireBuffer();
4201
4258
 
4202
- // turnkey-signer-frame.ts - a HIDDEN, offscreen iframe of the Bounded signer page
4203
- // for UI-less signer exchanges: sealing a login code (bounded:turnkey:encryptOtp),
4204
- // establishing the 24h signing session right after login
4205
- // (bounded:turnkey:establishSigningSession), and destroying it on logout
4206
- // (bounded:turnkey:clearSigningSession).
4259
+ // bounded-widget-modal.ts - the shared in-app WIDGET shell for Bounded's login
4260
+ // and signing surfaces. One Shadow-DOM overlay + panel that matches the Bounded
4261
+ // widget (Seal) look and MOTION: a backdrop fade, a panel rise on desktop, a
4262
+ // bottom-sheet on mobile, a height tween as content swaps, and a clean close.
4207
4263
  //
4208
- // One handle = one live frame. The frame is kept across requests so multi-step
4209
- // exchanges (seal a code, then establish the session with the keypair the SEAL
4210
- // left in that exact page instance) talk to the same page. Origin + source +
4211
- // requestId are all pinned; dispose() tears everything down.
4264
+ // It hosts EITHER:
4265
+ // - NATIVE content (the login card the SDK renders itself), or
4266
+ // - an IFRAME (the Bounded-owned signer page, which must run on the single
4267
+ // signer origin because the wallet's 24h signing session lives in that
4268
+ // origin's storage, out of every app's reach).
4269
+ //
4270
+ // The modal is a warm singleton: it stays mounted (hidden) between uses so a
4271
+ // second open - or a signature right after login - is instant and, when it is
4272
+ // already open, a clean in-place view swap rather than a reopen.
4212
4273
  function hasDOM$5() {
4213
- return typeof window !== 'undefined' && typeof document !== 'undefined';
4274
+ return typeof window !== "undefined" && typeof document !== "undefined";
4214
4275
  }
4215
- async function signerPage() {
4216
- const cfg = await core.getConfig();
4217
- const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
4218
- return { url: `${base}/wallet/turnkey/signer`, origin: new URL(base).origin };
4276
+ const OVERLAY_CSS = `
4277
+ :host{all:initial}
4278
+ *{box-sizing:border-box}
4279
+ .bw-ov{position:fixed;inset:0;z-index:2147483647;display:none;
4280
+ font-family:"Schibsted Grotesk","Helvetica Neue",Arial,sans-serif}
4281
+ .bw-ov.mounted{display:block}
4282
+ .bw-back{position:fixed;inset:0;background:rgba(4,4,5,.62);opacity:0;
4283
+ transition:opacity .24s ease;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}
4284
+ .bw-wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:20px}
4285
+ .bw-panel{position:relative;width:420px;max-width:calc(100vw - 28px);height:280px;
4286
+ border-radius:14px;overflow:hidden;background:#0e0f11;border:1px solid rgba(232,230,222,.08);
4287
+ box-shadow:0 1px 0 rgba(232,230,222,.03) inset,0 40px 90px -45px rgba(0,0,0,.85);
4288
+ opacity:0;transform:translateY(14px) scale(.98);will-change:transform,opacity,height;
4289
+ transition:opacity .28s cubic-bezier(.2,.8,.2,1),transform .28s cubic-bezier(.2,.8,.2,1),
4290
+ height .3s cubic-bezier(.2,.8,.2,1)}
4291
+ .bw-ov.show .bw-back{opacity:1}
4292
+ .bw-ov.show .bw-panel{opacity:1;transform:none}
4293
+ .bw-ov.closing .bw-back{opacity:0}
4294
+ .bw-ov.closing .bw-panel{opacity:0;transform:translateY(12px) scale(.99)}
4295
+ .bw-content{position:absolute;inset:0;overflow:hidden}
4296
+ .bw-frame{display:block;width:100%;height:100%;border:0;background:transparent;color-scheme:dark}
4297
+ /* editorial corner crop-marks, matching the reference frame */
4298
+ .bw-mk{position:absolute;width:13px;height:13px;pointer-events:none;z-index:2;
4299
+ border-color:rgba(232,230,222,.22);border-style:solid;border-width:0}
4300
+ .bw-mk.tl{top:11px;left:11px;border-top-width:1px;border-left-width:1px}
4301
+ .bw-mk.tr{top:11px;right:11px;border-top-width:1px;border-right-width:1px}
4302
+ .bw-mk.bl{bottom:11px;left:11px;border-bottom-width:1px;border-left-width:1px}
4303
+ .bw-mk.br{bottom:11px;right:11px;border-bottom-width:1px;border-right-width:1px}
4304
+ @media(max-width:560px){
4305
+ .bw-wrap{align-items:flex-end;padding:0}
4306
+ .bw-panel{width:100%;max-width:100%;border-radius:18px 18px 0 0;border-bottom:0}
4307
+ .bw-content{overflow-y:auto}
4308
+ .bw-ov.show .bw-panel{transform:none}
4309
+ .bw-ov:not(.show) .bw-panel,.bw-ov.closing .bw-panel{transform:translateY(18%)}
4219
4310
  }
4220
- /** Mount the hidden signer frame. Callers MUST dispose() when done. */
4221
- async function openHiddenSignerFrame() {
4222
- if (!hasDOM$5())
4223
- throw new Error('The Bounded signer frame is only available in the browser.');
4224
- const { url, origin } = await signerPage();
4225
- const frame = document.createElement('iframe');
4226
- frame.setAttribute('aria-hidden', 'true');
4227
- frame.style.cssText =
4228
- 'position:fixed;width:1px;height:1px;left:-9999px;top:-9999px;border:0;opacity:0;pointer-events:none';
4229
- let disposed = false;
4230
- let readyResolve = null;
4231
- const ready = new Promise((resolve) => { readyResolve = resolve; });
4232
- const pending = new Map();
4233
- const onMsg = (event) => {
4234
- if (event.origin !== origin || event.source !== frame.contentWindow)
4311
+ @media(prefers-reduced-motion:reduce){.bw-back,.bw-panel{transition:opacity .12s linear!important}}
4312
+ `;
4313
+ let modal = null;
4314
+ function getModal() {
4315
+ if (modal)
4316
+ return modal;
4317
+ const root = document.createElement("div");
4318
+ root.setAttribute("data-bounded-widget", "");
4319
+ const shadow = root.attachShadow({ mode: "open" });
4320
+ const style = document.createElement("style");
4321
+ style.textContent = OVERLAY_CSS;
4322
+ shadow.appendChild(style);
4323
+ const overlay = document.createElement("div");
4324
+ overlay.className = "bw-ov";
4325
+ const back = document.createElement("div");
4326
+ back.className = "bw-back";
4327
+ const wrap = document.createElement("div");
4328
+ wrap.className = "bw-wrap";
4329
+ const panel = document.createElement("div");
4330
+ panel.className = "bw-panel";
4331
+ const content = document.createElement("div");
4332
+ content.className = "bw-content";
4333
+ panel.appendChild(content);
4334
+ for (const corner of ["tl", "tr", "bl", "br"]) {
4335
+ const mk = document.createElement("span");
4336
+ mk.className = "bw-mk " + corner;
4337
+ panel.appendChild(mk);
4338
+ }
4339
+ wrap.appendChild(panel);
4340
+ overlay.appendChild(back);
4341
+ overlay.appendChild(wrap);
4342
+ shadow.appendChild(overlay);
4343
+ (document.body || document.documentElement).appendChild(root);
4344
+ const m = {
4345
+ root,
4346
+ overlay,
4347
+ panel,
4348
+ content,
4349
+ iframe: null,
4350
+ iframeOrigin: "",
4351
+ iframeReady: false,
4352
+ iframeContext: "",
4353
+ onBackdrop: null,
4354
+ onIframeMessage: null,
4355
+ chain: Promise.resolve(),
4356
+ fitObserver: null,
4357
+ generation: 0,
4358
+ onSupersede: null,
4359
+ };
4360
+ // The dismiss handler lives on `.bw-wrap`: it is the full-viewport flex layer
4361
+ // that sits ABOVE `.bw-back`, so it (not the backdrop element) receives clicks
4362
+ // outside the panel. Fire only when the click lands on the wrap itself, never
4363
+ // when it bubbles up from the panel or its content.
4364
+ wrap.addEventListener("click", (event) => {
4365
+ if (event.target === wrap && m.onBackdrop)
4366
+ m.onBackdrop();
4367
+ });
4368
+ window.addEventListener("message", (event) => {
4369
+ if (!m.iframe)
4235
4370
  return;
4236
- const d = event.data;
4237
- if (!d || typeof d !== 'object')
4371
+ if (event.origin !== m.iframeOrigin)
4238
4372
  return;
4239
- if (d.type === 'bounded:turnkey:ready') {
4240
- readyResolve === null || readyResolve === void 0 ? void 0 : readyResolve();
4373
+ if (event.source !== m.iframe.contentWindow)
4241
4374
  return;
4242
- }
4243
- if (d.type === 'bounded:turnkey:result' && typeof d.requestId === 'string') {
4244
- const settle = pending.get(d.requestId);
4245
- if (settle) {
4246
- pending.delete(d.requestId);
4247
- settle(d);
4248
- }
4249
- }
4250
- };
4251
- window.addEventListener('message', onMsg);
4252
- frame.src = url;
4253
- document.body.appendChild(frame);
4254
- const dispose = () => {
4255
- if (disposed)
4375
+ const d = event.data;
4376
+ if (!d || typeof d !== "object")
4256
4377
  return;
4257
- disposed = true;
4258
- window.removeEventListener('message', onMsg);
4259
- pending.clear();
4260
- try {
4261
- frame.remove();
4262
- }
4263
- catch ( /* already gone */_a) { /* already gone */ }
4264
- };
4265
- return {
4266
- dispose,
4267
- request(message, timeoutMs) {
4268
- const requestId = String(message.requestId || '');
4269
- if (!requestId)
4270
- return Promise.reject(new Error('signer frame request needs a requestId'));
4271
- if (disposed)
4272
- return Promise.reject(new Error('signer frame disposed'));
4273
- return new Promise((resolve, reject) => {
4274
- let timer;
4275
- pending.set(requestId, (d) => {
4276
- if (timer)
4277
- clearTimeout(timer);
4278
- resolve(d);
4279
- });
4280
- timer = setTimeout(() => {
4281
- pending.delete(requestId);
4282
- reject(new Error('The Bounded signer did not respond in time.'));
4283
- }, timeoutMs);
4284
- ready.then(() => {
4285
- var _a;
4286
- if (disposed || !pending.has(requestId))
4287
- return;
4288
- try {
4289
- (_a = frame.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage(message, origin);
4290
- }
4291
- catch ( /* frame gone; timeout rejects */_b) { /* frame gone; timeout rejects */ }
4292
- });
4293
- });
4294
- },
4295
- };
4378
+ // Readiness belongs to the document, even while login owns the modal and no
4379
+ // signing exchange is listening. The retained frame will not load again.
4380
+ if (d.type === "bounded:turnkey:ready")
4381
+ m.iframeReady = true;
4382
+ if (m.onIframeMessage)
4383
+ m.onIframeMessage(d);
4384
+ });
4385
+ modal = m;
4386
+ return m;
4296
4387
  }
4297
-
4298
- // turnkey-signing-capability.ts - the pure AS-02 (DS2-0112) signing-capability logic,
4299
- // kept import-free (no DOM, no session, no @bounded-sh/core) so it is unit-testable.
4300
- //
4301
- // The hosted Turnkey signer refuses to sign unless the request carries an issuer-signed
4302
- // SIGNING CAPABILITY bound to the app id, the requesting origin, and the EXACT payload
4303
- // hash. `requestTurnkeySigningCapability` mints one for the given bytes and
4304
- // `buildTurnkeySignRequest` assembles the signer request that carries it. The bridge
4305
- // (turnkey-signer-bridge.ts) supplies the base url + id token and routes through both,
4306
- // so a regression here (a dropped capability, a wrong route, a swallowed error) is
4307
- // caught without a browser.
4308
- // POST the Bounded id token + payload hash to the issuer's capability mint route and
4309
- // return the issuer-signed capability for exactly these bytes. Credential-free: identity
4310
- // is proven with the id token in the body (not an ambient cookie the cross-origin fetch
4311
- // would not carry). Throws with the same stable messages the bridge relied on inline.
4312
- async function requestTurnkeySigningCapability(opts) {
4313
- const base = opts.baseUrl.replace(/\/$/, '');
4314
- let res;
4315
- try {
4316
- res = await opts.fetchImpl(`${base}/wallet/turnkey/capability`, {
4317
- method: 'POST',
4318
- headers: { 'content-type': 'application/json' },
4319
- body: JSON.stringify({ idToken: opts.idToken, messageHex: opts.messageHex }),
4320
- });
4321
- }
4322
- catch (_a) {
4323
- throw new Error('Could not reach the Bounded signer to authorize this signature.');
4324
- }
4325
- if (!res.ok) {
4326
- let error = 'signing_capability_denied';
4327
- try {
4328
- const d = (await res.json());
4329
- if (d && typeof d.error === 'string')
4330
- error = d.error;
4331
- }
4332
- catch (_b) {
4333
- /* keep the default marker */
4334
- }
4335
- throw new Error(`Turnkey signing was not authorized (${error}).`);
4336
- }
4337
- const d = (await res.json());
4338
- if (!d || typeof d.capability !== 'string' || !d.capability) {
4339
- throw new Error('Turnkey signing capability was malformed.');
4340
- }
4388
+ function showModal(m) {
4389
+ m.overlay.classList.remove("closing");
4390
+ m.overlay.classList.add("mounted");
4391
+ // Force a reflow so the .show transition runs from the hidden state every open.
4392
+ void m.panel.offsetWidth;
4393
+ m.overlay.classList.add("show");
4394
+ }
4395
+ function hideModal(m, after) {
4396
+ m.overlay.classList.remove("show");
4397
+ m.overlay.classList.add("closing");
4398
+ // Capture the generation at hide time. If a newer open re-shows the modal within
4399
+ // the close tween, this stale timer must NOT strip `.mounted`/`.closing` off the
4400
+ // freshly reopened widget - only tear down when we are still the same open.
4401
+ const gen = m.generation;
4402
+ window.setTimeout(() => {
4403
+ if (m.generation === gen)
4404
+ m.overlay.classList.remove("mounted", "closing");
4405
+ }, 260);
4406
+ }
4407
+ function setPanelHeight(m, px) {
4408
+ const h = Math.max(140, Math.min(680, Math.round(px)));
4409
+ m.panel.style.height = h + "px";
4410
+ }
4411
+ // True outer height of the mounted card: the max of its padding-box scroll height
4412
+ // (catches overflowing descendants) and its border-box height plus own vertical
4413
+ // margins (catches border + margins, which scrollHeight omits). Ceiled so a
4414
+ // sub-pixel row is never clipped under `overflow:hidden`.
4415
+ function measureOuterHeight(target) {
4416
+ var _a;
4417
+ const cs = (_a = target.ownerDocument.defaultView) === null || _a === void 0 ? void 0 : _a.getComputedStyle(target);
4418
+ const marginY = cs ? parseFloat(cs.marginTop) + parseFloat(cs.marginBottom) : 0;
4419
+ const borderBox = target.offsetHeight + (Number.isFinite(marginY) ? marginY : 0);
4420
+ const rect = target.getBoundingClientRect().height;
4421
+ return Math.ceil(Math.max(target.scrollHeight, borderBox, rect));
4422
+ }
4423
+ /**
4424
+ * Measure the currently mounted native content and tween the panel to fit it.
4425
+ * Re-fits automatically when the card's intrinsic height changes - notably after
4426
+ * the Newsreader/Schibsted web fonts swap in (which grows line boxes a few px
4427
+ * after first paint) and on inline view swaps (email -> OTP code -> success).
4428
+ */
4429
+ function fitPanelToContent(m) {
4430
+ var _a, _b;
4431
+ const first = m.content.firstElementChild;
4432
+ const target = first || m.content;
4433
+ const apply = () => {
4434
+ // Only fit while this exact node is still the mounted card.
4435
+ if (m.content.firstElementChild !== first && first)
4436
+ return;
4437
+ const h = measureOuterHeight(target);
4438
+ if (h > 0)
4439
+ setPanelHeight(m, h);
4440
+ };
4441
+ apply();
4442
+ const view = target.ownerDocument.defaultView;
4443
+ // Re-fit after fonts load (one-shot) and once the ResizeObserver-less browsers
4444
+ // have painted, so the height never lags a late reflow.
4445
+ (_a = view === null || view === void 0 ? void 0 : view.requestAnimationFrame) === null || _a === void 0 ? void 0 : _a.call(view, apply);
4446
+ const fonts = target.ownerDocument.fonts;
4447
+ if (fonts === null || fonts === void 0 ? void 0 : fonts.ready)
4448
+ fonts.ready.then(apply).catch(() => { });
4449
+ (_b = m.fitObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
4450
+ m.fitObserver = null;
4451
+ if (first && typeof (view === null || view === void 0 ? void 0 : view.ResizeObserver) === "function") {
4452
+ const ro = new view.ResizeObserver(() => apply());
4453
+ ro.observe(first);
4454
+ m.fitObserver = ro;
4455
+ }
4456
+ }
4457
+ /** Switch the modal into NATIVE-content mode: detach any iframe, mount `node`. */
4458
+ function mountContent(m, node) {
4459
+ var _a;
4460
+ detachIframe(m);
4461
+ (_a = m.fitObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
4462
+ m.fitObserver = null;
4463
+ m.content.replaceChildren(node);
4464
+ m.content.style.display = "block";
4465
+ }
4466
+ /** Get (or lazily create) the warm iframe for signer/hosted-page mode. */
4467
+ function ensureIframe(m, url, origin, context = "") {
4468
+ var _a;
4469
+ if (m.iframe && m.iframeOrigin === origin && m.iframe.src === url && m.iframeContext === context)
4470
+ return m.iframe;
4471
+ detachIframe(m);
4472
+ (_a = m.fitObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
4473
+ m.fitObserver = null;
4474
+ m.content.style.display = "none";
4475
+ const iframe = document.createElement("iframe");
4476
+ iframe.className = "bw-frame";
4477
+ iframe.setAttribute("title", "Bounded");
4478
+ iframe.setAttribute("allow", "clipboard-write");
4479
+ iframe.src = url;
4480
+ m.iframeOrigin = origin;
4481
+ m.iframeContext = context;
4482
+ m.iframeReady = false;
4483
+ m.panel.appendChild(iframe);
4484
+ m.iframe = iframe;
4485
+ return iframe;
4486
+ }
4487
+ /**
4488
+ * Destroy the warm signer iframe if one is currently mounted for `origin`, and report
4489
+ * whether there was one. Logout needs this: a signing session can live in that document's
4490
+ * MEMORY (the signer keeps an in-memory mirror when the browser blocks its storage), and a
4491
+ * throwaway iframe opened by the sweep can only ever clear the shared store, never another
4492
+ * document's memory. Removing the iframe discards its browsing context, which takes the
4493
+ * mirror with it.
4494
+ *
4495
+ * Deliberately does NOT call getModal(): that would BUILD a modal just to tear it down, on
4496
+ * every logout of every app, most of which never mounted one.
4497
+ */
4498
+ function detachWarmSignerIframe(origin) {
4499
+ if (!modal || !modal.iframe || (origin))
4500
+ return false;
4501
+ detachIframe(modal);
4502
+ return true;
4503
+ }
4504
+ /** A removed frame cannot still supply its in-memory signing session. */
4505
+ function hasWarmSignerFrame(context) {
4506
+ var _a;
4507
+ return Boolean(((_a = modal === null || modal === void 0 ? void 0 : modal.iframe) === null || _a === void 0 ? void 0 : _a.isConnected) && modal.iframeContext === context);
4508
+ }
4509
+ function detachIframe(m) {
4510
+ if (m.iframe) {
4511
+ try {
4512
+ m.iframe.remove();
4513
+ }
4514
+ catch (_a) {
4515
+ /* ignore */
4516
+ }
4517
+ }
4518
+ m.iframe = null;
4519
+ m.iframeReady = false;
4520
+ m.iframeOrigin = "";
4521
+ m.iframeContext = "";
4522
+ m.onIframeMessage = null;
4523
+ }
4524
+ // Load the Bounded editorial typefaces (Newsreader display + Schibsted Grotesk body +
4525
+ // JetBrains Mono) once at the document level so they apply inside the shadow root. A
4526
+ // no-op if already present or off-DOM. Fonts loaded here match the hosted auth page.
4527
+ 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";
4528
+ function ensureFonts() {
4529
+ if (!hasDOM$5())
4530
+ return;
4531
+ if (document.querySelector('link[data-bounded-fonts="1"]'))
4532
+ return;
4533
+ const pre1 = document.createElement("link");
4534
+ pre1.rel = "preconnect";
4535
+ pre1.href = "https://fonts.googleapis.com";
4536
+ const pre2 = document.createElement("link");
4537
+ pre2.rel = "preconnect";
4538
+ pre2.href = "https://fonts.gstatic.com";
4539
+ pre2.crossOrigin = "anonymous";
4540
+ const link = document.createElement("link");
4541
+ link.rel = "stylesheet";
4542
+ link.href = FONTS_HREF;
4543
+ link.setAttribute("data-bounded-fonts", "1");
4544
+ document.head.append(pre1, pre2, link);
4545
+ }
4546
+ /** Tag the already mounted login document without moving or navigating it. */
4547
+ function bindWarmSignerFrame(frame, context) {
4548
+ if ((modal === null || modal === void 0 ? void 0 : modal.iframe) === frame)
4549
+ modal.iframeContext = context;
4550
+ }
4551
+
4552
+ // turnkey-wallet-widget.ts - runs one Turnkey signer exchange inside the shared
4553
+ // in-app WIDGET (bounded-widget-modal.ts). The Bounded-owned signer page is loaded
4554
+ // in a warm iframe behind the modal so signing feels native: an in-app approve
4555
+ // card (with inline code entry when the 24h signing session must first be
4556
+ // established), never a jarring popup.
4557
+ //
4558
+ // WHY AN IFRAME: the wallet's signing session lives in the single Bounded signer
4559
+ // origin's storage, out of every app's reach, so signing MUST run on that origin;
4560
+ // the iframe brings it in-app.
4561
+ //
4562
+ // Restricted embedders: if the frame never loads, the SAME request reruns through
4563
+ // `popupFallback` (a real window). Callers pass a popup implementation and never
4564
+ // see the difference.
4565
+ /**
4566
+ * Run one signer exchange through the in-app widget. Resolves when `onResult`
4567
+ * accepts a result; falls back to `popupFallback` if the frame cannot be mounted
4568
+ * or never loads.
4569
+ */
4570
+ async function runWidget(url, signerOrigin, ex, popupFallback) {
4571
+ if (!hasDOM$5())
4572
+ return popupFallback();
4573
+ const m = getModal();
4574
+ const requestId = String(ex.request.requestId);
4575
+ // Serialize: the approve card is modal, and one warm frame handles one request at a time.
4576
+ const run = m.chain.then(() => new Promise((resolve, reject) => {
4577
+ var _a;
4578
+ let settled = false;
4579
+ let usedFallback = false;
4580
+ (_a = ex.assertCurrent) === null || _a === void 0 ? void 0 : _a.call(ex);
4581
+ const iframe = ensureIframe(m, url, signerOrigin, ex.context);
4582
+ const send = () => {
4583
+ var _a;
4584
+ if (!iframe.contentWindow)
4585
+ return;
4586
+ try {
4587
+ (_a = ex.assertCurrent) === null || _a === void 0 ? void 0 : _a.call(ex);
4588
+ iframe.contentWindow.postMessage(ex.request, signerOrigin);
4589
+ }
4590
+ catch (_b) {
4591
+ finish(() => reject(new Error('wallet_session_changed')));
4592
+ }
4593
+ };
4594
+ let loadTimer = 0;
4595
+ let ceilingTimer = 0;
4596
+ const finish = (fn) => {
4597
+ if (settled)
4598
+ return;
4599
+ settled = true;
4600
+ m.onIframeMessage = null;
4601
+ m.onBackdrop = null;
4602
+ window.clearTimeout(loadTimer);
4603
+ window.clearTimeout(ceilingTimer);
4604
+ fn();
4605
+ };
4606
+ // The frame is WARM across exchanges, so an abandoned approval must be told to drop its
4607
+ // in-flight ceremony. Otherwise the signer keeps `approving` set for a card nobody is
4608
+ // looking at and refuses every later create/sign as `signer_request_superseded` (the
4609
+ // BSA-2026-012 guard) until the page reloads.
4610
+ const cancelInFrame = () => {
4611
+ if (!iframe.contentWindow)
4612
+ return;
4613
+ try {
4614
+ iframe.contentWindow.postMessage({ type: "bounded:turnkey:cancel", requestId }, signerOrigin);
4615
+ }
4616
+ catch (_a) {
4617
+ /* frame gone */
4618
+ }
4619
+ };
4620
+ const goFallback = () => {
4621
+ if (usedFallback || settled)
4622
+ return;
4623
+ usedFallback = true;
4624
+ hideModal(m);
4625
+ finish(() => popupFallback().then(resolve, reject));
4626
+ };
4627
+ m.onBackdrop = () => {
4628
+ cancelInFrame();
4629
+ hideModal(m);
4630
+ finish(() => reject(new Error("cancelled")));
4631
+ };
4632
+ m.onIframeMessage = (d) => {
4633
+ if (d.type === "bounded:turnkey:ready") {
4634
+ send();
4635
+ return;
4636
+ }
4637
+ if (d.type === "bounded:turnkey:resize" && typeof d.height === "number") {
4638
+ setPanelHeight(m, d.height);
4639
+ return;
4640
+ }
4641
+ if (d.type === "bounded:turnkey:result" && d.requestId === requestId) {
4642
+ const outcome = ex.onResult(d);
4643
+ if ("error" in outcome) {
4644
+ hideModal(m);
4645
+ finish(() => reject(new Error(outcome.error)));
4646
+ return;
4647
+ }
4648
+ if (outcome.done) {
4649
+ hideModal(m);
4650
+ finish(() => resolve(outcome.value));
4651
+ }
4652
+ }
4653
+ };
4654
+ showModal(m);
4655
+ if (m.iframeReady)
4656
+ send();
4657
+ // If the frame never loads, degrade to the popup (best-effort - a post-load
4658
+ // popup may be blocked without a gesture, in which case the fallback rejects).
4659
+ loadTimer = window.setTimeout(() => {
4660
+ if (!m.iframeReady && !settled)
4661
+ goFallback();
4662
+ }, 8000);
4663
+ // Absolute ceiling so a stuck flow cannot hang forever.
4664
+ ceilingTimer = window.setTimeout(() => {
4665
+ if (!settled) {
4666
+ cancelInFrame();
4667
+ hideModal(m);
4668
+ finish(() => reject(new Error("Wallet approval timed out.")));
4669
+ }
4670
+ }, 5 * 60 * 1000);
4671
+ }));
4672
+ m.chain = run.then(() => undefined, () => undefined);
4673
+ return run;
4674
+ }
4675
+
4676
+ // turnkey-signer-frame.ts - a HIDDEN, offscreen iframe of the Bounded signer page
4677
+ // for UI-less signer exchanges: sealing a login code (bounded:turnkey:encryptOtp),
4678
+ // establishing the 24h signing session right after login
4679
+ // (bounded:turnkey:establishSigningSession), and destroying it on logout
4680
+ // (bounded:turnkey:clearSigningSession).
4681
+ //
4682
+ // One handle = one live frame. The frame is kept across requests so multi-step
4683
+ // exchanges (seal a code, then establish the session with the keypair the SEAL
4684
+ // left in that exact page instance) talk to the same page. Origin + source +
4685
+ // requestId are all pinned; dispose() tears everything down.
4686
+ function hasDOM$4() {
4687
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
4688
+ }
4689
+ async function signerPage() {
4690
+ const cfg = await core.getConfig();
4691
+ const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
4692
+ return { url: `${base}/wallet/turnkey/signer`, origin: new URL(base).origin };
4693
+ }
4694
+ /** Mount the hidden signer frame. Callers MUST dispose() when done. */
4695
+ async function openHiddenSignerFrame(page) {
4696
+ if (!hasDOM$4())
4697
+ throw new Error('The Bounded signer frame is only available in the browser.');
4698
+ const { url, origin } = page !== null && page !== void 0 ? page : await signerPage();
4699
+ const frame = document.createElement('iframe');
4700
+ frame.setAttribute('aria-hidden', 'true');
4701
+ frame.style.cssText =
4702
+ 'position:fixed;width:1px;height:1px;left:-9999px;top:-9999px;border:0;opacity:0;pointer-events:none';
4703
+ let disposed = false;
4704
+ let readyResolve = null;
4705
+ const ready = new Promise((resolve) => { readyResolve = resolve; });
4706
+ const pending = new Map();
4707
+ const onMsg = (event) => {
4708
+ if (event.origin !== origin || event.source !== frame.contentWindow)
4709
+ return;
4710
+ const d = event.data;
4711
+ if (!d || typeof d !== 'object')
4712
+ return;
4713
+ if (d.type === 'bounded:turnkey:ready') {
4714
+ readyResolve === null || readyResolve === void 0 ? void 0 : readyResolve();
4715
+ return;
4716
+ }
4717
+ if (d.type === 'bounded:turnkey:result' && typeof d.requestId === 'string') {
4718
+ const settle = pending.get(d.requestId);
4719
+ if (settle) {
4720
+ pending.delete(d.requestId);
4721
+ settle(d);
4722
+ }
4723
+ }
4724
+ };
4725
+ window.addEventListener('message', onMsg);
4726
+ frame.src = url;
4727
+ document.body.appendChild(frame);
4728
+ const dispose = () => {
4729
+ if (disposed)
4730
+ return;
4731
+ disposed = true;
4732
+ window.removeEventListener('message', onMsg);
4733
+ pending.clear();
4734
+ try {
4735
+ frame.remove();
4736
+ }
4737
+ catch ( /* already gone */_a) { /* already gone */ }
4738
+ };
4739
+ return {
4740
+ dispose,
4741
+ request(message, timeoutMs) {
4742
+ const requestId = String(message.requestId || '');
4743
+ if (!requestId)
4744
+ return Promise.reject(new Error('signer frame request needs a requestId'));
4745
+ if (disposed)
4746
+ return Promise.reject(new Error('signer frame disposed'));
4747
+ return new Promise((resolve, reject) => {
4748
+ let timer;
4749
+ pending.set(requestId, (d) => {
4750
+ if (timer)
4751
+ clearTimeout(timer);
4752
+ resolve(d);
4753
+ });
4754
+ timer = setTimeout(() => {
4755
+ pending.delete(requestId);
4756
+ reject(new Error('The Bounded signer did not respond in time.'));
4757
+ }, timeoutMs);
4758
+ ready.then(() => {
4759
+ var _a;
4760
+ if (disposed || !pending.has(requestId))
4761
+ return;
4762
+ try {
4763
+ (_a = frame.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage(message, origin);
4764
+ }
4765
+ catch ( /* frame gone; timeout rejects */_b) { /* frame gone; timeout rejects */ }
4766
+ });
4767
+ });
4768
+ },
4769
+ };
4770
+ }
4771
+
4772
+ // turnkey-signing-capability.ts - the pure AS-02 (DS2-0112) signing-capability logic,
4773
+ // kept import-free (no DOM, no session, no @bounded-sh/core) so it is unit-testable.
4774
+ //
4775
+ // The hosted Turnkey signer refuses to sign unless the request carries an issuer-signed
4776
+ // SIGNING CAPABILITY bound to the app id, the requesting origin, and the EXACT payload
4777
+ // hash. `requestTurnkeySigningCapability` mints one for the given bytes and
4778
+ // `buildTurnkeySignRequest` assembles the signer request that carries it. The bridge
4779
+ // (turnkey-signer-bridge.ts) supplies the base url + id token and routes through both,
4780
+ // so a regression here (a dropped capability, a wrong route, a swallowed error) is
4781
+ // caught without a browser.
4782
+ // POST the Bounded id token + payload hash to the issuer's capability mint route and
4783
+ // return the issuer-signed capability for exactly these bytes. Credential-free: identity
4784
+ // is proven with the id token in the body (not an ambient cookie the cross-origin fetch
4785
+ // would not carry). Throws with the same stable messages the bridge relied on inline.
4786
+ async function requestTurnkeySigningCapability(opts) {
4787
+ const base = opts.baseUrl.replace(/\/$/, '');
4788
+ let res;
4789
+ try {
4790
+ res = await opts.fetchImpl(`${base}/wallet/turnkey/capability`, {
4791
+ method: 'POST',
4792
+ headers: { 'content-type': 'application/json' },
4793
+ body: JSON.stringify({ idToken: opts.idToken, messageHex: opts.messageHex, walletProtocol: 2 }),
4794
+ });
4795
+ }
4796
+ catch (_a) {
4797
+ throw new Error('Could not reach the Bounded signer to authorize this signature.');
4798
+ }
4799
+ if (!res.ok) {
4800
+ let error = 'signing_capability_denied';
4801
+ try {
4802
+ const d = (await res.json());
4803
+ if (d && typeof d.error === 'string')
4804
+ error = d.error;
4805
+ }
4806
+ catch (_b) {
4807
+ /* keep the default marker */
4808
+ }
4809
+ throw new Error(`Turnkey signing was not authorized (${error}).`);
4810
+ }
4811
+ const d = (await res.json());
4812
+ if (!d || typeof d.capability !== 'string' || !d.capability) {
4813
+ throw new Error('Turnkey signing capability was malformed.');
4814
+ }
4341
4815
  return d.capability;
4342
4816
  }
4343
4817
  // Assemble the `bounded:turnkey:sign` postMessage request, carrying the issuer-signed
@@ -4368,7 +4842,7 @@ async function requestTurnkeyActionAuthorization(opts) {
4368
4842
  res = await opts.fetchImpl(`${base}/wallet/turnkey/action-authorization`, {
4369
4843
  method: 'POST',
4370
4844
  headers: { 'content-type': 'application/json' },
4371
- body: JSON.stringify({ idToken: opts.idToken, action: opts.action }),
4845
+ body: JSON.stringify({ idToken: opts.idToken, action: opts.action, walletProtocol: 2 }),
4372
4846
  });
4373
4847
  }
4374
4848
  catch (_a) {
@@ -4420,26 +4894,46 @@ function buildTurnkeyActionRequest(opts) {
4420
4894
  // code is the sole signing authority; within a live session every signature is one
4421
4895
  // approval click). The app retains the transaction itself.
4422
4896
  //
4423
- // ONE ORIGIN, ONE SESSION: the signing session lives in the signer origin's own
4424
- // storage, out of every app's reach, so ALL Bounded apps (any subdomain / custom
4425
- // domain) sign through this one origin - giving every user one wallet that roams
4426
- // across every app.
4427
- function hasDOM$4() {
4897
+ // Signing keys remain on the trusted signer origin. New apps use independently
4898
+ // bound wallets and storage slots; legacy apps retain their shared wallet.
4899
+ function hasDOM$3() {
4428
4900
  return typeof window !== 'undefined' && typeof document !== 'undefined';
4429
4901
  }
4430
- // BSA-2026-006: dispatch every signing / get-or-create exchange through a TOP-LEVEL
4431
- // POPUP the caller opened from the user gesture - NEVER a framed widget. An authorized
4432
- // tenant that embedded the signer could overlay its predictable Approve control and steal
4433
- // one concealed click into a fund-moving signature; a top-level window it cannot reach or
4434
- // visually redress closes that. A blocked popup surfaces `popup_blocked` (runPopup) and we
4435
- // never silently fall back to an iframe.
4902
+ // Dispatch one signer exchange through the in-app WIDGET, degrading to the POPUP
4903
+ // when we cannot embed (no DOM, or the frame never mounts/loads). Nothing in the
4904
+ // email-session flow needs a popup by preference: the code entry and the approval
4905
+ // click both run fine inside a cross-origin iframe on every engine (the old
4906
+ // Safari popup preference existed only for in-iframe WebAuthn CREATE).
4436
4907
  async function deliver(url, origin, ex) {
4437
- return runPopup(url, origin, ex);
4438
- }
4439
- async function turnkeySignerPage() {
4908
+ var _a, _b;
4909
+ const popup = () => runPopup(url, origin, ex);
4910
+ (_a = ex.assertCurrent) === null || _a === void 0 ? void 0 : _a.call(ex);
4911
+ const result = await runWidget(url, origin, { request: ex.request, onResult: ex.onResult, context: ex.context, assertCurrent: ex.assertCurrent }, popup);
4912
+ (_b = ex.assertCurrent) === null || _b === void 0 ? void 0 : _b.call(ex);
4913
+ return result;
4914
+ }
4915
+ async function turnkeySignerPage(token) {
4916
+ const assertGeneration = walletOperationGuard();
4917
+ const idToken = token === undefined ? core.getActiveSessionManager().getIdToken() : token;
4440
4918
  const cfg = await core.getConfig();
4441
4919
  const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
4442
- return { url: `${base}/wallet/turnkey/signer`, origin: new URL(base).origin };
4920
+ const context = walletContextFromClaims(idToken ? decodeJwt$1(idToken) : {}, base, cfg.appId, window.location.origin);
4921
+ return Object.assign(Object.assign({}, context), { idToken: idToken || '', assertCurrent() {
4922
+ assertGeneration();
4923
+ const current = core.getActiveSessionManager().getIdToken();
4924
+ if (!current)
4925
+ throw new Error('wallet_session_changed');
4926
+ const next = walletContextFromClaims(decodeJwt$1(current), base, cfg.appId, window.location.origin);
4927
+ if (next.key !== context.key)
4928
+ throw new Error('wallet_session_changed');
4929
+ } });
4930
+ }
4931
+ async function getTurnkeyWalletSessionState() {
4932
+ const context = await turnkeySignerPage();
4933
+ context.assertCurrent();
4934
+ if (!hasWarmSignerFrame(context.key))
4935
+ return { status: 'verification-required' };
4936
+ return walletSessionState(context.key);
4443
4937
  }
4444
4938
  function toHex(bytes) {
4445
4939
  let h = '';
@@ -4495,7 +4989,7 @@ async function runPopup(url, signerOrigin, ex) {
4495
4989
  const top = window.screenY + Math.max(0, (window.outerHeight - h) / 2);
4496
4990
  const popup = window.open(url, 'bounded-turnkey-signer', `width=${w},height=${h},left=${left},top=${top}`);
4497
4991
  if (!popup) {
4498
- 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.');
4992
+ throw new Error('Popup blocked - call this from a user gesture (e.g. a click handler) so the wallet approval window can open.');
4499
4993
  }
4500
4994
  return await new Promise((resolve, reject) => {
4501
4995
  let settled = false;
@@ -4511,11 +5005,12 @@ async function runPopup(url, signerOrigin, ex) {
4511
5005
  let closeTimer;
4512
5006
  const cleanup = () => { settled = true; window.removeEventListener('message', onMsg); if (closeTimer)
4513
5007
  clearInterval(closeTimer); };
4514
- const sendRequest = () => { if (settled || acked)
5008
+ const sendRequest = () => { var _a; if (settled || acked)
4515
5009
  return; try {
5010
+ (_a = ex.assertCurrent) === null || _a === void 0 ? void 0 : _a.call(ex);
4516
5011
  popup.postMessage(ex.request, signerOrigin);
4517
5012
  }
4518
- catch ( /* popup gone */_a) { /* popup gone */ } };
5013
+ catch ( /* popup gone */_b) { /* popup gone */ } };
4519
5014
  const onMsg = (event) => {
4520
5015
  if (event.origin !== signerOrigin)
4521
5016
  return;
@@ -4585,16 +5080,9 @@ async function runPopup(url, signerOrigin, ex) {
4585
5080
  // identity with the Bounded id token WE minted (a body secret, RS256-verified server-side), not
4586
5081
  // an ambient cookie - the issuer's session cookie is SameSite-scoped to its own origin and is
4587
5082
  // not sent on this cross-origin fetch, so the token is what carries the session's wallet.
4588
- async function fetchSigningCapability(messageHex) {
4589
- const cfg = await core.getConfig();
4590
- const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
4591
- let idToken = null;
4592
- try {
4593
- idToken = core.getActiveSessionManager().getIdToken();
4594
- }
4595
- catch (_a) {
4596
- idToken = null;
4597
- }
5083
+ async function fetchSigningCapability(messageHex, context) {
5084
+ context.assertCurrent();
5085
+ const { base, idToken } = context;
4598
5086
  if (!idToken) {
4599
5087
  throw new Error('This session has no Turnkey wallet. Sign in again to provision one.');
4600
5088
  }
@@ -4609,16 +5097,9 @@ async function fetchSigningCapability(messageHex) {
4609
5097
  // requires an issuer-signed authorization bound to this app origin and exact action. Fetch it
4610
5098
  // before posting to the signer. Walletless sessions are valid for createWallet because that is
4611
5099
  // the operation that provisions their first wallet.
4612
- async function fetchActionAuthorization(action) {
4613
- const cfg = await core.getConfig();
4614
- const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
4615
- let idToken = null;
4616
- try {
4617
- idToken = core.getActiveSessionManager().getIdToken();
4618
- }
4619
- catch (_a) {
4620
- idToken = null;
4621
- }
5100
+ async function fetchActionAuthorization(action, context) {
5101
+ context.assertCurrent();
5102
+ const { base, idToken } = context;
4622
5103
  if (!idToken) {
4623
5104
  throw new Error('Sign in again to set up your Turnkey wallet.');
4624
5105
  }
@@ -4638,31 +5119,23 @@ async function fetchActionAuthorization(action) {
4638
5119
  * an unswept session is account-bound and unusable without a live authorization,
4639
5120
  * it just costs its owner nothing and a stranger nothing.
4640
5121
  */
4641
- async function clearTurnkeySigningSession() {
4642
- if (!hasDOM$4())
5122
+ async function clearTurnkeySigningSession(idToken) {
5123
+ if (!hasDOM$3())
4643
5124
  return;
4644
- // The session can live in TWO places, and the sweep has to reach both:
4645
- // 1. the signer origin's store, shared by every signer frame in this app's partition -
4646
- // cleared by posting into a throwaway frame below;
4647
- // 2. the WARM widget iframe's memory. The signer keeps an in-memory mirror when the
4648
- // browser blocks its storage, and hideModal deliberately leaves that iframe mounted
4649
- // between signatures, so the mirror outlives logout. A throwaway frame cannot see
4650
- // another document's memory - the only way to take it is to destroy that document.
4651
- // Done first, and synchronously, so it lands even if the frame exchange below fails.
4652
- try {
4653
- const { origin } = await turnkeySignerPage();
4654
- (await Promise.resolve().then(function () { return boundedWidgetModal; })).detachWarmSignerIframe(origin);
4655
- }
4656
- catch (_a) {
4657
- /* no modal module / no warm frame: nothing to take */
4658
- }
5125
+ // Destroy the trusted in-memory key even if configuration or token parsing fails.
5126
+ // Do it synchronously, before a different login can mount a replacement frame.
5127
+ detachWarmSignerIframe();
5128
+ // Capture the session before asynchronous teardown clears the SDK's token.
5129
+ const contextPromise = turnkeySignerPage(idToken);
4659
5130
  let frame = null;
4660
5131
  try {
4661
- frame = await openHiddenSignerFrame();
5132
+ const context = await contextPromise;
5133
+ setWalletSessionState(context.key, { status: 'verification-required', reason: 'logged_out' });
5134
+ frame = await openHiddenSignerFrame(context);
4662
5135
  await frame.request({ type: 'bounded:turnkey:clearSigningSession', requestId: `tkclear_${Date.now()}_${++requestSeq}` }, 4000);
4663
5136
  }
4664
- catch (_b) {
4665
- /* best-effort: never block or fail logout on the sweep */
5137
+ catch (_a) {
5138
+ // The warm frame was destroyed; the bounded store sweep is best effort.
4666
5139
  }
4667
5140
  finally {
4668
5141
  frame === null || frame === void 0 ? void 0 : frame.dispose();
@@ -4680,12 +5153,14 @@ function hasTurnkeyWallet() {
4680
5153
  * Use this to learn the fee-payer address BEFORE building a transaction to sign.
4681
5154
  */
4682
5155
  async function getOrCreateTurnkeyWallet() {
4683
- if (!hasDOM$4())
4684
- throw new Error('Turnkey embedded wallets are only available in the browser (popup-based).');
4685
- const { url, origin } = await turnkeySignerPage();
5156
+ if (!hasDOM$3())
5157
+ throw new Error('Turnkey embedded wallets are only available in the browser (trusted wallet UI).');
5158
+ const context = await turnkeySignerPage();
5159
+ const { url, origin } = context;
4686
5160
  const requestId = `tkcreate_${Date.now()}_${++requestSeq}`;
4687
- const authorization = await fetchActionAuthorization('createWallet');
5161
+ const authorization = await fetchActionAuthorization('createWallet', context);
4688
5162
  return await deliver(url, origin, {
5163
+ context: context.key, assertCurrent: context.assertCurrent,
4689
5164
  request: buildTurnkeyActionRequest({ requestId, action: 'createWallet', authorization }),
4690
5165
  onResult: (d) => {
4691
5166
  if (d.ok && typeof d.address === 'string')
@@ -4705,7 +5180,7 @@ async function getOrCreateTurnkeyWallet() {
4705
5180
  * that tab - it never transits Bounded.
4706
5181
  */
4707
5182
  async function openTurnkeyKeyExport() {
4708
- if (!hasDOM$4())
5183
+ if (!hasDOM$3())
4709
5184
  throw new Error('Turnkey key export is only available in the browser.');
4710
5185
  if (!hasTurnkeyWallet())
4711
5186
  throw new Error('This session has no Turnkey wallet to export.');
@@ -4726,10 +5201,11 @@ async function openTurnkeyKeyExport() {
4726
5201
  }
4727
5202
  catch ( /* older engines: the tab still cannot read us cross-origin */_a) { /* older engines: the tab still cannot read us cross-origin */ }
4728
5203
  try {
4729
- const cfg = await core.getConfig();
4730
- const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
4731
- const authorization = await fetchActionAuthorization('exportWallet');
4732
- handle.location.href = `${base}/wallet/turnkey/export#authorization=${encodeURIComponent(authorization)}`;
5204
+ const context = await turnkeySignerPage();
5205
+ const authorization = await fetchActionAuthorization('exportWallet', context);
5206
+ context.assertCurrent();
5207
+ const path = context.scope === 'app' ? `/wallet/turnkey/app/${context.appId}` : '/wallet/turnkey';
5208
+ handle.location.href = `${context.base}${path}/export#authorization=${encodeURIComponent(authorization)}`;
4733
5209
  }
4734
5210
  catch (error) {
4735
5211
  // Never strand a blank tab on a failed mint.
@@ -4741,10 +5217,9 @@ async function openTurnkeyKeyExport() {
4741
5217
  }
4742
5218
  }
4743
5219
  /**
4744
- * Open the Bounded Turnkey signer popup and sign `message` (raw bytes) with the
4745
- * user's non-custodial Turnkey wallet, resolving with the ed25519 signature +
4746
- * the wallet address. MUST be called from a USER GESTURE (click/tap) or the
4747
- * browser blocks the popup.
5220
+ * Open the Bounded Turnkey approval card and sign `message` (raw bytes) with the
5221
+ * user's Turnkey wallet, resolving with the ed25519 signature and wallet address.
5222
+ * Call from a user gesture so a deliberate browser fallback can open if needed.
4748
5223
  *
4749
5224
  * The signer shows an approve card (wallet address, requesting origin, Approve /
4750
5225
  * Cancel). With a live 24-hour signing session (normally established by the login
@@ -4753,24 +5228,27 @@ async function openTurnkeyKeyExport() {
4753
5228
  * completes the signature.
4754
5229
  */
4755
5230
  async function signSolanaMessageViaTurnkey(message) {
4756
- if (!hasDOM$4()) {
4757
- throw new Error('Turnkey embedded-wallet signing is only available in the browser (popup-based).');
5231
+ if (!hasDOM$3()) {
5232
+ throw new Error('Turnkey embedded-wallet signing is only available in the browser (trusted wallet UI).');
4758
5233
  }
4759
- const { url, origin } = await turnkeySignerPage();
5234
+ const context = await turnkeySignerPage();
5235
+ const { url, origin } = context;
4760
5236
  const requestId = `tksign_${Date.now()}_${++requestSeq}`;
4761
5237
  const messageHex = toHex(message);
4762
5238
  // AS-02: obtain the issuer-signed capability for THESE exact bytes first; the signer will
4763
5239
  // refuse (capability_required) without it. Fetched per signature (each is single-payload).
4764
- const capability = await fetchSigningCapability(messageHex);
5240
+ const capability = await fetchSigningCapability(messageHex, context);
4765
5241
  // Inline (authMode:"turnkey") sessions plant no Better Auth cookie on the issuer, so the
4766
5242
  // signer's same-origin status/create fetches need a bearer session proof: attach the
4767
5243
  // createWallet action authorization (it covers the sign flow's get-or-create too). Minted
4768
5244
  // from the same id token as the capability, so a session that minted one can mint both.
4769
- const authorization = await fetchActionAuthorization('createWallet');
5245
+ const authorization = await fetchActionAuthorization('createWallet', context);
4770
5246
  return await deliver(url, origin, {
5247
+ context: context.key, assertCurrent: context.assertCurrent,
4771
5248
  request: buildTurnkeySignRequest({ requestId, messageHex, capability, authorization }),
4772
5249
  onResult: (d) => {
4773
5250
  if (d.ok && typeof d.signatureHex === 'string' && typeof d.address === 'string') {
5251
+ setWalletSessionState(context.key, { status: 'ready', expiresAt: Number(d.signingSessionExpiresAt) });
4774
5252
  return { done: true, value: { signature: fromHex(d.signatureHex), address: d.address, subOrgId: String(d.subOrgId || '') } };
4775
5253
  }
4776
5254
  return { error: String(d.error || 'Turnkey signing failed') };
@@ -4880,11 +5358,21 @@ async function signAndSubmitTransactionWithTurnkey(transaction, feePayer) {
4880
5358
  }
4881
5359
  return signature;
4882
5360
  }
5361
+ /** Save this trusted URL to recover the wallet even after the app is deleted. */
5362
+ async function getTurnkeyWalletRecoveryUrl() {
5363
+ const context = await turnkeySignerPage();
5364
+ context.assertCurrent();
5365
+ if (!context.address || !hasTurnkeyWallet())
5366
+ throw new Error('wallet_unavailable');
5367
+ return `${context.base}/wallet/turnkey/recover/${encodeURIComponent(context.address)}`;
5368
+ }
4883
5369
 
4884
5370
  var turnkeySignerBridge = /*#__PURE__*/Object.freeze({
4885
5371
  __proto__: null,
4886
5372
  clearTurnkeySigningSession: clearTurnkeySigningSession,
4887
5373
  getOrCreateTurnkeyWallet: getOrCreateTurnkeyWallet,
5374
+ getTurnkeyWalletRecoveryUrl: getTurnkeyWalletRecoveryUrl,
5375
+ getTurnkeyWalletSessionState: getTurnkeyWalletSessionState,
4888
5376
  hasTurnkeyWallet: hasTurnkeyWallet,
4889
5377
  openTurnkeyKeyExport: openTurnkeyKeyExport,
4890
5378
  signAndSubmitTransactionWithTurnkey: signAndSubmitTransactionWithTurnkey,
@@ -4911,7 +5399,7 @@ const ISSUER_SESSION_KIND_KEY = 'bounded_issuer_session_kind';
4911
5399
  // not by `typeof window` (truthy on RN). OIDC redirect/popup is web-only, but the
4912
5400
  // session it mints is read back through this same selector everywhere.
4913
5401
  const sessionManager = core.getActiveSessionManager;
4914
- function hasDOM$3() {
5402
+ function hasDOM$2() {
4915
5403
  return typeof window !== 'undefined' && typeof document !== 'undefined';
4916
5404
  }
4917
5405
  function decodeJwt(token) {
@@ -5062,7 +5550,7 @@ async function buildAuthorize(opts) {
5062
5550
  throw new Error('appId is not configured (init the SDK with your appId)');
5063
5551
  // Web: default to the current page so callers don't have to pass redirectUri. Native has no
5064
5552
  // window — it MUST pass an https universal link (the system auth session needs a real URL).
5065
- const redirectUri = opts.redirectUri || (hasDOM$3() ? window.location.origin + window.location.pathname : undefined);
5553
+ const redirectUri = opts.redirectUri || (hasDOM$2() ? window.location.origin + window.location.pathname : undefined);
5066
5554
  if (!redirectUri)
5067
5555
  throw new Error('redirectUri is required on React Native — pass the https universal link the app owner registered.');
5068
5556
  const { verifier, challenge } = await makePkce();
@@ -5170,7 +5658,7 @@ async function runNativeLogin(opts) {
5170
5658
  async function loginWithRedirect(opts) {
5171
5659
  if (isNative())
5172
5660
  return runNativeLogin(opts);
5173
- if (!hasDOM$3())
5661
+ if (!hasDOM$2())
5174
5662
  throw new Error('loginWithRedirect requires a browser or a configured React Native platform');
5175
5663
  const { url, pkce } = await buildAuthorize(opts);
5176
5664
  // Stash the verifier + state so the redirect_uri page can finish the exchange. sessionStorage
@@ -5187,7 +5675,7 @@ async function loginWithRedirect(opts) {
5187
5675
  * loginWithRedirect() (which resolves with the User), so there is no separate redirect
5188
5676
  * page to finish from. Calling it on RN is harmless. */
5189
5677
  async function completeLoginFromRedirect() {
5190
- if (!hasDOM$3())
5678
+ if (!hasDOM$2())
5191
5679
  return null;
5192
5680
  const url = new URL(window.location.href);
5193
5681
  const code = url.searchParams.get('code');
@@ -5235,7 +5723,7 @@ async function completeLoginFromRedirect() {
5235
5723
  * redirect_uri page posts the code back. The redirect_uri page just calls the usual
5236
5724
  * completeLoginFromRedirect() (which auto-detects the popup) — no separate call needed. */
5237
5725
  async function loginWithPopup(opts) {
5238
- if (!hasDOM$3())
5726
+ if (!hasDOM$2())
5239
5727
  throw new Error('loginWithPopup is web-only — on React Native use loginWithRedirect()');
5240
5728
  // Open synchronously, before PKCE/config awaits consume the click's user-
5241
5729
  // activation window. Opening only after buildAuthorize() intermittently
@@ -5395,7 +5883,7 @@ async function loginWithPopup(opts) {
5395
5883
  /** Call this on the redirect_uri page when login was opened via loginWithPopup(): posts the
5396
5884
  * code back to the opener and closes. (For the redirect variant use completeLoginFromRedirect.) */
5397
5885
  function completeLoginInPopup(openerOrigin) {
5398
- if (!hasDOM$3() || !window.opener)
5886
+ if (!hasDOM$2() || !window.opener)
5399
5887
  return;
5400
5888
  const url = new URL(window.location.href);
5401
5889
  const code = url.searchParams.get('code');
@@ -5416,19 +5904,27 @@ function completeLoginInPopup(openerOrigin) {
5416
5904
  // Turnkey-native login path (authMode:"turnkey"), which mints the session server-side
5417
5905
  // after Turnkey verifies the email OTP - there is no OIDC code to exchange. Wiring is
5418
5906
  // identical to exchangeCode's tail so the realtime worker cannot tell them apart.
5419
- async function establishInlineSession(data) {
5907
+ async function establishInlineSession(data, beforeCommit, beforeStore) {
5420
5908
  const base = await issuerBase();
5909
+ beforeCommit === null || beforeCommit === void 0 ? void 0 : beforeCommit();
5421
5910
  const idToken = tokenField(data, 'id_token', 'idToken');
5422
5911
  if (!idToken)
5423
5912
  throw new Error('No id_token in the inline session response');
5424
5913
  const accessToken = tokenField(data, 'access_token', 'accessToken') || idToken;
5425
5914
  const refreshToken = boundedRefreshTokenField(data);
5426
5915
  const user = userFromIdToken(idToken);
5427
- await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base);
5916
+ const rollback = await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base, () => { beforeCommit === null || beforeCommit === void 0 ? void 0 : beforeCommit(); beforeStore === null || beforeStore === void 0 ? void 0 : beforeStore(); });
5428
5917
  // Adopt the OIDC provider on EVERY session surface - including core's signing
5429
5918
  // pointer (getConfig().authProvider), which this path used to skip, leaving
5430
5919
  // onchain writes routed through a previous (e.g. logged-out wallet) provider.
5431
- await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user });
5920
+ try {
5921
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user, beforeCommit });
5922
+ }
5923
+ catch (error) {
5924
+ if (typeof rollback === 'function')
5925
+ await rollback();
5926
+ throw error;
5927
+ }
5432
5928
  // Turnkey-native OTP mints an app session directly. It does not establish a
5433
5929
  // Better Auth browser cookie on the issuer, so logout must not navigate
5434
5930
  // through the issuer's cookie-clearing /logout endpoint. Stamped AFTER the
@@ -5492,9 +5988,6 @@ async function exchangeCode(code, verifier, redirectUri) {
5492
5988
  return user;
5493
5989
  }
5494
5990
 
5495
- // NOTE: Provider class re-exports have been moved to index.ts (web) and
5496
- // index.native.ts (RN) to avoid eager resolution of web-only dependencies
5497
- // (react-dom) when this module is imported on RN.
5498
5991
  let currentAuthProvider = null;
5499
5992
  let currentAuthMethod = null;
5500
5993
  let currentAuthProviderConfigKey = null;
@@ -5849,7 +6342,7 @@ async function getAuthProvider(config) {
5849
6342
  // manager as legacy email sessions. Human login should be started
5850
6343
  // with loginWithRedirect(); this provider is kept as the restore and
5851
6344
  // logout surface for those sessions.
5852
- currentAuthProvider = new (await Promise.resolve().then(function () { return require('./email-auth-provider-DxB_pJKO.js'); })).EmailAuthProvider();
6345
+ currentAuthProvider = new (await Promise.resolve().then(function () { return require('./email-auth-provider-C6SFJd9L.js'); })).EmailAuthProvider();
5853
6346
  break;
5854
6347
  case "phantom": {
5855
6348
  // First-class Solana WALLET LOGIN (the poof/TaroBase-style "connect
@@ -5891,7 +6384,7 @@ async function getAuthProvider(config) {
5891
6384
  // (bounded-login-widget.ts), so registration never counts as
5892
6385
  // evidence the user has a wallet.
5893
6386
  await ensureSolanaMobileWalletRegistered(config, configuredNetwork);
5894
- const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-BDt4CN0b.js'); });
6387
+ const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-B3LLEpfp.js'); });
5895
6388
  currentAuthProvider = new InjectedWalletProvider(resolveInjectedWalletConfig(config), rpcUrl, configuredNetwork);
5896
6389
  break;
5897
6390
  }
@@ -5904,7 +6397,7 @@ async function getAuthProvider(config) {
5904
6397
  // Selecting authMethod:'evm-wallet' IS the opt-in — pass evmWalletConfig
5905
6398
  // to point at a specific wallet or set chainId/domain.
5906
6399
  const evmWalletConfig = ((_a = config.evmWalletConfig) !== null && _a !== void 0 ? _a : {});
5907
- const { InjectedEvmWalletProvider } = await Promise.resolve().then(function () { return require('./injected-evm-wallet-provider-Bv8VLuB1.js'); });
6400
+ const { InjectedEvmWalletProvider } = await Promise.resolve().then(function () { return require('./injected-evm-wallet-provider-Dq3FDV1M.js'); });
5908
6401
  currentAuthProvider = new InjectedEvmWalletProvider(evmWalletConfig);
5909
6402
  break;
5910
6403
  }
@@ -5929,7 +6422,7 @@ async function getAuthProvider(config) {
5929
6422
  break;
5930
6423
  case "guest":
5931
6424
  // Anonymous auth: a device-local ed25519 keypair signs the challenge.
5932
- currentAuthProvider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-B9C3Iub_.js'); })).GuestAuthProvider();
6425
+ currentAuthProvider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-pC-AheTZ.js'); })).GuestAuthProvider();
5933
6426
  break;
5934
6427
  }
5935
6428
  if (!currentAuthProvider) {
@@ -5991,7 +6484,7 @@ async function signInAnonymously() {
5991
6484
  // Lazy-load the guest provider so its ed25519/web3 dependencies stay out of
5992
6485
  // the main bundle for data-only apps. Specifier is a static string literal
5993
6486
  // so bundle-splitting can see it.
5994
- const provider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-B9C3Iub_.js'); })).GuestAuthProvider();
6487
+ const provider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-pC-AheTZ.js'); })).GuestAuthProvider();
5995
6488
  setAuthLoading(true); // reflect "signing in…" while the guest keypair session is minted
5996
6489
  // adoptSessionProvider awaits only getConfig() (already resolved here) and then
5997
6490
  // runs every write - provider/method/config/marker/user - in one synchronous
@@ -6162,7 +6655,7 @@ function ensureSharedWalletLoginReady() {
6162
6655
  if (!sharedWalletReady) {
6163
6656
  sharedWalletReady = (async () => {
6164
6657
  await core.getConfig();
6165
- await Promise.resolve().then(function () { return require('./injected-wallet-provider-BDt4CN0b.js'); });
6658
+ await Promise.resolve().then(function () { return require('./injected-wallet-provider-B3LLEpfp.js'); });
6166
6659
  })().catch((error) => {
6167
6660
  sharedWalletReady = null;
6168
6661
  throw error;
@@ -6265,7 +6758,7 @@ async function loginWithWallet(options) {
6265
6758
  await ensureSharedWalletLoginReady();
6266
6759
  else
6267
6760
  await ensureWalletLoginReady();
6268
- const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-BDt4CN0b.js'); });
6761
+ const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-B3LLEpfp.js'); });
6269
6762
  // Keep the RAW wallet provider: the offchain wrapper forwards only a fixed
6270
6763
  // set of methods, so cleanup below must speak to the provider that actually
6271
6764
  // holds the config rather than to whatever is wrapping it.
@@ -6427,13 +6920,14 @@ async function exchangeLogoutHint(issuerBase, idToken) {
6427
6920
  // which can be blocked or offline), so its failures are the one thing logout may
6428
6921
  // tolerate - never revokeSession's. Lazy import per this module's rule (the signer
6429
6922
  // bridge is browser-only machinery). Never rejects.
6430
- async function sweepTurnkeySigningSession() {
6923
+ async function sweepTurnkeySigningSession(idToken) {
6431
6924
  try {
6432
- await (await Promise.resolve().then(function () { return turnkeySignerBridge; })).clearTurnkeySigningSession();
6925
+ await (await Promise.resolve().then(function () { return turnkeySignerBridge; })).clearTurnkeySigningSession(idToken);
6433
6926
  }
6434
6927
  catch ( /* best-effort: never block or fail logout on the sweep */_a) { /* best-effort: never block or fail logout on the sweep */ }
6435
6928
  }
6436
6929
  async function logout$1(options) {
6930
+ invalidatePendingWalletLogin();
6437
6931
  // Re-initialize the provider if it was cleared by a prior logout — the exact mirror
6438
6932
  // of login()'s self-heal above. Every session-establishment path now adopts through
6439
6933
  // session-provider.ts, so this is a REPAIR (the registry was cleared by a previous
@@ -6465,7 +6959,7 @@ async function logout$1(options) {
6465
6959
  // sweep is a hidden signer-iframe exchange; start it now (after revocation is
6466
6960
  // committed - a failed logout leaves the still-logged-in user their session) and
6467
6961
  // let it overlap the provider teardown. sweepTurnkeySigningSession never rejects.
6468
- const signingSessionSweep = sweepTurnkeySigningSession();
6962
+ const signingSessionSweep = sweepTurnkeySigningSession(idTokenHint);
6469
6963
  // Only OIDC-hosted sessions own an issuer browser cookie. Turnkey-native
6470
6964
  // email OTP is also an email auth method, but it mints the app session
6471
6965
  // inline and has no issuer cookie to clear. Use the explicit session-kind
@@ -6679,386 +7173,116 @@ function init(newConfig) {
6679
7173
  async function waitForAuthReady() {
6680
7174
  const ready = authReadyPromise;
6681
7175
  if (!ready) {
6682
- throw new Error('SDK not initialized. Please call init() first.');
6683
- }
6684
- await ready;
6685
- return currentUser;
6686
- }
6687
- function onAuthStateChanged(callback) {
6688
- authStateListeners.push(callback);
6689
- if (initCompleted || hasRunFirstTime) {
6690
- // Call the callback immediately with the current user.
6691
- // We check hasRunFirstTime in addition to initCompleted to handle the race
6692
- // where the Phantom auto-login effect calls setCurrentUser() before init()
6693
- // finishes. Without this, listeners registered between auto-login's
6694
- // setCurrentUser and init()'s initCompleted would never fire (init's
6695
- // setCurrentUser is a no-op for same-address and initCompleted is still false).
6696
- callback(currentUser);
6697
- }
6698
- return () => {
6699
- authStateListeners = authStateListeners.filter((listener) => listener !== callback);
6700
- };
6701
- }
6702
- function onAuthLoadingChanged(callback) {
6703
- authLoadingListeners.push(callback);
6704
- // Call immediately with current loading state
6705
- callback(isAuthLoading);
6706
- return () => {
6707
- authLoadingListeners = authLoadingListeners.filter((listener) => listener !== callback);
6708
- };
6709
- }
6710
- function setAuthLoading(loading) {
6711
- if (isAuthLoading !== loading) {
6712
- isAuthLoading = loading;
6713
- authLoadingListeners.forEach((callback) => callback(loading));
6714
- }
6715
- }
6716
- function getAuthLoading() {
6717
- return isAuthLoading;
6718
- }
6719
- async function login(options) {
6720
- if (!authProviderInstance) {
6721
- throw new Error('SDK not initialized. Please call init() first.');
6722
- }
6723
- const loggedInUser = await login$1(options);
6724
- setCurrentUser(loggedInUser);
6725
- return currentUser;
6726
- }
6727
- async function logout(options) {
6728
- if (!authProviderInstance) {
6729
- throw new Error('SDK not initialized. Please call init() first.');
6730
- }
6731
- await logout$1(options);
6732
- setCurrentUser(null);
6733
- }
6734
- var hasRunFirstTime = false;
6735
- function userPrincipalKey(user) {
6736
- var _a, _b, _c;
6737
- if (!user)
6738
- return null;
6739
- 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;
6740
- }
6741
- // Update the currentUser and notify listeners
6742
- function setCurrentUser(user) {
6743
- // Enrich with the universal identity fields (id/email) derived from the
6744
- // active session's idToken — central chokepoint so every provider's user
6745
- // object gains { id, address, email } without each construction site
6746
- // having to set them.
6747
- user = withUserIdentity(user);
6748
- const lastUser = currentUser;
6749
- const wasInitializedBefore = hasRunFirstTime;
6750
- const lastPrincipal = userPrincipalKey(currentUser);
6751
- const nextPrincipal = userPrincipalKey(user);
6752
- const shouldRunListeners = !hasRunFirstTime || lastPrincipal !== nextPrincipal;
6753
- hasRunFirstTime = true;
6754
- currentUser = user;
6755
- if (shouldRunListeners) {
6756
- // Isolate each listener: a consumer callback that throws (during logout in
6757
- // particular) must not abort the notification loop OR skip the socket reopen
6758
- // below. Before this, a single throwing listener propagated out of
6759
- // setCurrentUser, so the realtime socket kept streaming as the signed-out
6760
- // user and the other listeners were never told the principal changed.
6761
- authStateListeners.forEach((callback) => {
6762
- try {
6763
- callback(user);
6764
- }
6765
- catch (error) {
6766
- console.error('Error in onAuthStateChanged listener:', error);
6767
- }
6768
- });
6769
- // Reconnect WebSocket connections with new auth when user changes.
6770
- // Skip on the first setCurrentUser call during init() — there are no
6771
- // existing connections to reconnect at that point.
6772
- if (wasInitializedBefore && userPrincipalKey(lastUser) !== nextPrincipal) {
6773
- core.reconnectWithNewAuth().catch((error) => {
6774
- console.error('Error reconnecting WebSocket after auth change:', error);
6775
- });
6776
- }
6777
- }
6778
- // Return this commit's normalized snapshot. A listener can start another
6779
- // login, so the mutable currentUser may already belong to a newer session.
6780
- return user;
6781
- }
6782
- function getCurrentUser() {
6783
- return currentUser;
6784
- }
6785
- /** @internal Snapshot/restore surface for transactional auth-provider switches. */
6786
- function getAuthProviderInstance() {
6787
- return authProviderInstance;
6788
- }
6789
- function setAuthProviderInstance(provider) {
6790
- authProviderInstance = provider;
6791
- }
6792
-
6793
- // bounded-widget-modal.ts - the shared in-app WIDGET shell for Bounded's login
6794
- // and signing surfaces. One Shadow-DOM overlay + panel that matches the Bounded
6795
- // widget (Seal) look and MOTION: a backdrop fade, a panel rise on desktop, a
6796
- // bottom-sheet on mobile, a height tween as content swaps, and a clean close.
6797
- //
6798
- // It hosts EITHER:
6799
- // - NATIVE content (the login card the SDK renders itself), or
6800
- // - an IFRAME (the Bounded-owned signer page, which must run on the single
6801
- // signer origin because the wallet's 24h signing session lives in that
6802
- // origin's storage, out of every app's reach).
6803
- //
6804
- // The modal is a warm singleton: it stays mounted (hidden) between uses so a
6805
- // second open - or a signature right after login - is instant and, when it is
6806
- // already open, a clean in-place view swap rather than a reopen.
6807
- function hasDOM$2() {
6808
- return typeof window !== "undefined" && typeof document !== "undefined";
6809
- }
6810
- const OVERLAY_CSS = `
6811
- :host{all:initial}
6812
- *{box-sizing:border-box}
6813
- .bw-ov{position:fixed;inset:0;z-index:2147483647;display:none;
6814
- font-family:"Schibsted Grotesk","Helvetica Neue",Arial,sans-serif}
6815
- .bw-ov.mounted{display:block}
6816
- .bw-back{position:fixed;inset:0;background:rgba(4,4,5,.62);opacity:0;
6817
- transition:opacity .24s ease;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}
6818
- .bw-wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:20px}
6819
- .bw-panel{position:relative;width:420px;max-width:calc(100vw - 28px);height:280px;
6820
- border-radius:14px;overflow:hidden;background:#0e0f11;border:1px solid rgba(232,230,222,.08);
6821
- box-shadow:0 1px 0 rgba(232,230,222,.03) inset,0 40px 90px -45px rgba(0,0,0,.85);
6822
- opacity:0;transform:translateY(14px) scale(.98);will-change:transform,opacity,height;
6823
- transition:opacity .28s cubic-bezier(.2,.8,.2,1),transform .28s cubic-bezier(.2,.8,.2,1),
6824
- height .3s cubic-bezier(.2,.8,.2,1)}
6825
- .bw-ov.show .bw-back{opacity:1}
6826
- .bw-ov.show .bw-panel{opacity:1;transform:none}
6827
- .bw-ov.closing .bw-back{opacity:0}
6828
- .bw-ov.closing .bw-panel{opacity:0;transform:translateY(12px) scale(.99)}
6829
- .bw-content{position:absolute;inset:0;overflow:hidden}
6830
- .bw-frame{display:block;width:100%;height:100%;border:0;background:transparent;color-scheme:dark}
6831
- /* editorial corner crop-marks, matching the reference frame */
6832
- .bw-mk{position:absolute;width:13px;height:13px;pointer-events:none;z-index:2;
6833
- border-color:rgba(232,230,222,.22);border-style:solid;border-width:0}
6834
- .bw-mk.tl{top:11px;left:11px;border-top-width:1px;border-left-width:1px}
6835
- .bw-mk.tr{top:11px;right:11px;border-top-width:1px;border-right-width:1px}
6836
- .bw-mk.bl{bottom:11px;left:11px;border-bottom-width:1px;border-left-width:1px}
6837
- .bw-mk.br{bottom:11px;right:11px;border-bottom-width:1px;border-right-width:1px}
6838
- @media(max-width:560px){
6839
- .bw-wrap{align-items:flex-end;padding:0}
6840
- .bw-panel{width:100%;max-width:100%;border-radius:18px 18px 0 0;border-bottom:0}
6841
- .bw-content{overflow-y:auto}
6842
- .bw-ov.show .bw-panel{transform:none}
6843
- .bw-ov:not(.show) .bw-panel,.bw-ov.closing .bw-panel{transform:translateY(18%)}
6844
- }
6845
- @media(prefers-reduced-motion:reduce){.bw-back,.bw-panel{transition:opacity .12s linear!important}}
6846
- `;
6847
- let modal = null;
6848
- function getModal() {
6849
- if (modal)
6850
- return modal;
6851
- const root = document.createElement("div");
6852
- root.setAttribute("data-bounded-widget", "");
6853
- const shadow = root.attachShadow({ mode: "open" });
6854
- const style = document.createElement("style");
6855
- style.textContent = OVERLAY_CSS;
6856
- shadow.appendChild(style);
6857
- const overlay = document.createElement("div");
6858
- overlay.className = "bw-ov";
6859
- const back = document.createElement("div");
6860
- back.className = "bw-back";
6861
- const wrap = document.createElement("div");
6862
- wrap.className = "bw-wrap";
6863
- const panel = document.createElement("div");
6864
- panel.className = "bw-panel";
6865
- const content = document.createElement("div");
6866
- content.className = "bw-content";
6867
- panel.appendChild(content);
6868
- for (const corner of ["tl", "tr", "bl", "br"]) {
6869
- const mk = document.createElement("span");
6870
- mk.className = "bw-mk " + corner;
6871
- panel.appendChild(mk);
6872
- }
6873
- wrap.appendChild(panel);
6874
- overlay.appendChild(back);
6875
- overlay.appendChild(wrap);
6876
- shadow.appendChild(overlay);
6877
- (document.body || document.documentElement).appendChild(root);
6878
- const m = {
6879
- root,
6880
- overlay,
6881
- panel,
6882
- content,
6883
- iframe: null,
6884
- iframeOrigin: "",
6885
- iframeReady: false,
6886
- onBackdrop: null,
6887
- onIframeMessage: null,
6888
- chain: Promise.resolve(),
6889
- fitObserver: null,
6890
- generation: 0,
6891
- onSupersede: null,
6892
- };
6893
- // The dismiss handler lives on `.bw-wrap`: it is the full-viewport flex layer
6894
- // that sits ABOVE `.bw-back`, so it (not the backdrop element) receives clicks
6895
- // outside the panel. Fire only when the click lands on the wrap itself, never
6896
- // when it bubbles up from the panel or its content.
6897
- wrap.addEventListener("click", (event) => {
6898
- if (event.target === wrap && m.onBackdrop)
6899
- m.onBackdrop();
6900
- });
6901
- window.addEventListener("message", (event) => {
6902
- if (!m.iframe)
6903
- return;
6904
- if (event.origin !== m.iframeOrigin)
6905
- return;
6906
- if (event.source !== m.iframe.contentWindow)
6907
- return;
6908
- const d = event.data;
6909
- if (!d || typeof d !== "object")
6910
- return;
6911
- if (m.onIframeMessage)
6912
- m.onIframeMessage(d);
6913
- });
6914
- modal = m;
6915
- return m;
7176
+ throw new Error('SDK not initialized. Please call init() first.');
7177
+ }
7178
+ await ready;
7179
+ return currentUser;
6916
7180
  }
6917
- function showModal(m) {
6918
- m.overlay.classList.remove("closing");
6919
- m.overlay.classList.add("mounted");
6920
- // Force a reflow so the .show transition runs from the hidden state every open.
6921
- void m.panel.offsetWidth;
6922
- m.overlay.classList.add("show");
7181
+ function onAuthStateChanged(callback) {
7182
+ authStateListeners.push(callback);
7183
+ if (initCompleted || hasRunFirstTime) {
7184
+ // Call the callback immediately with the current user.
7185
+ // We check hasRunFirstTime in addition to initCompleted to handle the race
7186
+ // where the Phantom auto-login effect calls setCurrentUser() before init()
7187
+ // finishes. Without this, listeners registered between auto-login's
7188
+ // setCurrentUser and init()'s initCompleted would never fire (init's
7189
+ // setCurrentUser is a no-op for same-address and initCompleted is still false).
7190
+ callback(currentUser);
7191
+ }
7192
+ return () => {
7193
+ authStateListeners = authStateListeners.filter((listener) => listener !== callback);
7194
+ };
6923
7195
  }
6924
- function hideModal(m, after) {
6925
- m.overlay.classList.remove("show");
6926
- m.overlay.classList.add("closing");
6927
- // Capture the generation at hide time. If a newer open re-shows the modal within
6928
- // the close tween, this stale timer must NOT strip `.mounted`/`.closing` off the
6929
- // freshly reopened widget - only tear down when we are still the same open.
6930
- const gen = m.generation;
6931
- window.setTimeout(() => {
6932
- if (m.generation === gen)
6933
- m.overlay.classList.remove("mounted", "closing");
6934
- }, 260);
7196
+ function onAuthLoadingChanged(callback) {
7197
+ authLoadingListeners.push(callback);
7198
+ // Call immediately with current loading state
7199
+ callback(isAuthLoading);
7200
+ return () => {
7201
+ authLoadingListeners = authLoadingListeners.filter((listener) => listener !== callback);
7202
+ };
6935
7203
  }
6936
- function setPanelHeight(m, px) {
6937
- const h = Math.max(140, Math.min(680, Math.round(px)));
6938
- m.panel.style.height = h + "px";
7204
+ function setAuthLoading(loading) {
7205
+ if (isAuthLoading !== loading) {
7206
+ isAuthLoading = loading;
7207
+ authLoadingListeners.forEach((callback) => callback(loading));
7208
+ }
6939
7209
  }
6940
- // True outer height of the mounted card: the max of its padding-box scroll height
6941
- // (catches overflowing descendants) and its border-box height plus own vertical
6942
- // margins (catches border + margins, which scrollHeight omits). Ceiled so a
6943
- // sub-pixel row is never clipped under `overflow:hidden`.
6944
- function measureOuterHeight(target) {
6945
- var _a;
6946
- const cs = (_a = target.ownerDocument.defaultView) === null || _a === void 0 ? void 0 : _a.getComputedStyle(target);
6947
- const marginY = cs ? parseFloat(cs.marginTop) + parseFloat(cs.marginBottom) : 0;
6948
- const borderBox = target.offsetHeight + (Number.isFinite(marginY) ? marginY : 0);
6949
- const rect = target.getBoundingClientRect().height;
6950
- return Math.ceil(Math.max(target.scrollHeight, borderBox, rect));
7210
+ function getAuthLoading() {
7211
+ return isAuthLoading;
6951
7212
  }
6952
- /**
6953
- * Measure the currently mounted native content and tween the panel to fit it.
6954
- * Re-fits automatically when the card's intrinsic height changes - notably after
6955
- * the Newsreader/Schibsted web fonts swap in (which grows line boxes a few px
6956
- * after first paint) and on inline view swaps (email -> OTP code -> success).
6957
- */
6958
- function fitPanelToContent(m) {
6959
- var _a, _b;
6960
- const first = m.content.firstElementChild;
6961
- const target = first || m.content;
6962
- const apply = () => {
6963
- // Only fit while this exact node is still the mounted card.
6964
- if (m.content.firstElementChild !== first && first)
6965
- return;
6966
- const h = measureOuterHeight(target);
6967
- if (h > 0)
6968
- setPanelHeight(m, h);
6969
- };
6970
- apply();
6971
- const view = target.ownerDocument.defaultView;
6972
- // Re-fit after fonts load (one-shot) and once the ResizeObserver-less browsers
6973
- // have painted, so the height never lags a late reflow.
6974
- (_a = view === null || view === void 0 ? void 0 : view.requestAnimationFrame) === null || _a === void 0 ? void 0 : _a.call(view, apply);
6975
- const fonts = target.ownerDocument.fonts;
6976
- if (fonts === null || fonts === void 0 ? void 0 : fonts.ready)
6977
- fonts.ready.then(apply).catch(() => { });
6978
- (_b = m.fitObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
6979
- m.fitObserver = null;
6980
- if (first && typeof (view === null || view === void 0 ? void 0 : view.ResizeObserver) === "function") {
6981
- const ro = new view.ResizeObserver(() => apply());
6982
- ro.observe(first);
6983
- m.fitObserver = ro;
7213
+ async function login(options) {
7214
+ if (!authProviderInstance) {
7215
+ throw new Error('SDK not initialized. Please call init() first.');
6984
7216
  }
7217
+ const loggedInUser = await login$1(options);
7218
+ setCurrentUser(loggedInUser);
7219
+ return currentUser;
6985
7220
  }
6986
- /** Switch the modal into NATIVE-content mode: detach any iframe, mount `node`. */
6987
- function mountContent(m, node) {
6988
- var _a;
6989
- detachIframe(m);
6990
- (_a = m.fitObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
6991
- m.fitObserver = null;
6992
- m.content.replaceChildren(node);
6993
- m.content.style.display = "block";
7221
+ async function logout(options) {
7222
+ if (!authProviderInstance) {
7223
+ throw new Error('SDK not initialized. Please call init() first.');
7224
+ }
7225
+ await logout$1(options);
7226
+ setCurrentUser(null);
6994
7227
  }
6995
- /**
6996
- * Destroy the warm signer iframe if one is currently mounted for `origin`, and report
6997
- * whether there was one. Logout needs this: a signing session can live in that document's
6998
- * MEMORY (the signer keeps an in-memory mirror when the browser blocks its storage), and a
6999
- * throwaway iframe opened by the sweep can only ever clear the shared store, never another
7000
- * document's memory. Removing the iframe discards its browsing context, which takes the
7001
- * mirror with it.
7002
- *
7003
- * Deliberately does NOT call getModal(): that would BUILD a modal just to tear it down, on
7004
- * every logout of every app, most of which never mounted one.
7005
- */
7006
- function detachWarmSignerIframe(origin) {
7007
- if (!modal || !modal.iframe || modal.iframeOrigin !== origin)
7008
- return false;
7009
- detachIframe(modal);
7010
- return true;
7228
+ var hasRunFirstTime = false;
7229
+ function userPrincipalKey(user) {
7230
+ var _a, _b, _c;
7231
+ if (!user)
7232
+ return null;
7233
+ 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;
7011
7234
  }
7012
- function detachIframe(m) {
7013
- if (m.iframe) {
7014
- try {
7015
- m.iframe.remove();
7016
- }
7017
- catch (_a) {
7018
- /* ignore */
7235
+ // Update the currentUser and notify listeners
7236
+ function setCurrentUser(user) {
7237
+ // Enrich with the universal identity fields (id/email) derived from the
7238
+ // active session's idToken — central chokepoint so every provider's user
7239
+ // object gains { id, address, email } without each construction site
7240
+ // having to set them.
7241
+ user = withUserIdentity(user);
7242
+ const lastUser = currentUser;
7243
+ const wasInitializedBefore = hasRunFirstTime;
7244
+ const lastPrincipal = userPrincipalKey(currentUser);
7245
+ const nextPrincipal = userPrincipalKey(user);
7246
+ const shouldRunListeners = !hasRunFirstTime || lastPrincipal !== nextPrincipal;
7247
+ hasRunFirstTime = true;
7248
+ currentUser = user;
7249
+ if (shouldRunListeners) {
7250
+ // Isolate each listener: a consumer callback that throws (during logout in
7251
+ // particular) must not abort the notification loop OR skip the socket reopen
7252
+ // below. Before this, a single throwing listener propagated out of
7253
+ // setCurrentUser, so the realtime socket kept streaming as the signed-out
7254
+ // user and the other listeners were never told the principal changed.
7255
+ authStateListeners.forEach((callback) => {
7256
+ try {
7257
+ callback(user);
7258
+ }
7259
+ catch (error) {
7260
+ console.error('Error in onAuthStateChanged listener:', error);
7261
+ }
7262
+ });
7263
+ // Reconnect WebSocket connections with new auth when user changes.
7264
+ // Skip on the first setCurrentUser call during init() — there are no
7265
+ // existing connections to reconnect at that point.
7266
+ if (wasInitializedBefore && userPrincipalKey(lastUser) !== nextPrincipal) {
7267
+ core.reconnectWithNewAuth().catch((error) => {
7268
+ console.error('Error reconnecting WebSocket after auth change:', error);
7269
+ });
7019
7270
  }
7020
7271
  }
7021
- m.iframe = null;
7022
- m.iframeReady = false;
7023
- m.iframeOrigin = "";
7024
- m.onIframeMessage = null;
7272
+ // Return this commit's normalized snapshot. A listener can start another
7273
+ // login, so the mutable currentUser may already belong to a newer session.
7274
+ return user;
7025
7275
  }
7026
- // Load the Bounded editorial typefaces (Newsreader display + Schibsted Grotesk body +
7027
- // JetBrains Mono) once at the document level so they apply inside the shadow root. A
7028
- // no-op if already present or off-DOM. Fonts loaded here match the hosted auth page.
7029
- 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";
7030
- function ensureFonts() {
7031
- if (!hasDOM$2())
7032
- return;
7033
- if (document.querySelector('link[data-bounded-fonts="1"]'))
7034
- return;
7035
- const pre1 = document.createElement("link");
7036
- pre1.rel = "preconnect";
7037
- pre1.href = "https://fonts.googleapis.com";
7038
- const pre2 = document.createElement("link");
7039
- pre2.rel = "preconnect";
7040
- pre2.href = "https://fonts.gstatic.com";
7041
- pre2.crossOrigin = "anonymous";
7042
- const link = document.createElement("link");
7043
- link.rel = "stylesheet";
7044
- link.href = FONTS_HREF;
7045
- link.setAttribute("data-bounded-fonts", "1");
7046
- document.head.append(pre1, pre2, link);
7276
+ function getCurrentUser() {
7277
+ return currentUser;
7278
+ }
7279
+ /** @internal Snapshot/restore surface for transactional auth-provider switches. */
7280
+ function getAuthProviderInstance() {
7281
+ return authProviderInstance;
7282
+ }
7283
+ function setAuthProviderInstance(provider) {
7284
+ authProviderInstance = provider;
7047
7285
  }
7048
-
7049
- var boundedWidgetModal = /*#__PURE__*/Object.freeze({
7050
- __proto__: null,
7051
- detachIframe: detachIframe,
7052
- detachWarmSignerIframe: detachWarmSignerIframe,
7053
- ensureFonts: ensureFonts,
7054
- fitPanelToContent: fitPanelToContent,
7055
- getModal: getModal,
7056
- hasDOM: hasDOM$2,
7057
- hideModal: hideModal,
7058
- mountContent: mountContent,
7059
- setPanelHeight: setPanelHeight,
7060
- showModal: showModal
7061
- });
7062
7286
 
7063
7287
  // bounded-widget-ownership.ts - per-open ownership generation for the singleton
7064
7288
  // login/signing modal (bounded-widget-modal.ts).
@@ -7519,7 +7743,7 @@ async function startTurnkeyEmailLogin(email) {
7519
7743
  const res = await fetch(`${base}/wallet/turnkey/auth/init`, {
7520
7744
  method: 'POST',
7521
7745
  headers: { 'content-type': 'application/json' },
7522
- body: JSON.stringify({ email: trimmed, appId }),
7746
+ body: JSON.stringify({ email: trimmed, appId, walletProtocol: 2 }),
7523
7747
  });
7524
7748
  const data = await res.json().catch(() => null);
7525
7749
  if (!res.ok || !data || typeof data.otpId !== 'string' || typeof data.otpEncryptionTargetBundle !== 'string') {
@@ -7531,13 +7755,21 @@ async function startTurnkeyEmailLogin(email) {
7531
7755
  // (there is nothing to bind the frame key to), which is the correct fail-closed posture.
7532
7756
  const ceremonyId = typeof data.ceremonyId === 'string' ? data.ceremonyId : '';
7533
7757
  const signerOrigin = new URL(base).origin;
7758
+ if (data.walletScope !== 'app' && data.walletScope !== 'legacy')
7759
+ throw new Error('wallet_scope_config_unavailable');
7760
+ if (data.walletProtocol !== (data.walletScope === 'app' ? 2 : 1))
7761
+ throw new Error('wallet_protocol_invalid');
7762
+ const frameUrl = data.walletScope === 'app'
7763
+ ? `${base.replace(/\/$/, '')}/wallet/turnkey/app/${appId}/signer?origin=${encodeURIComponent(window.location.origin)}`
7764
+ : undefined;
7534
7765
  return {
7535
- email: trimmed,
7536
- run(iframe) {
7766
+ email: trimmed, frameUrl, signerOrigin,
7767
+ run(iframe, signal) {
7537
7768
  if (!ceremonyId) {
7538
7769
  return Promise.reject(new Error('This app is not configured for in-frame wallet login. Please try again.'));
7539
7770
  }
7540
- return runOtpFrame({ iframe, base, signerOrigin, appId, email: trimmed, otpId, otpEncryptionTargetBundle, ceremonyId });
7771
+ const run = { iframe, base, signerOrigin, appId, email: trimmed, otpId, otpEncryptionTargetBundle, ceremonyId };
7772
+ return frameUrl ? runScopedOtpFrame(run, frameUrl, signal) : runOtpFrame(run);
7541
7773
  },
7542
7774
  };
7543
7775
  }
@@ -7642,6 +7874,104 @@ function runOtpFrame(run) {
7642
7874
  run.iframe.src = url;
7643
7875
  });
7644
7876
  }
7877
+ // The scoped document owns both OTP and signing. Adopt only its sanitized app
7878
+ // session, acknowledge with a narrow grant, then wait for wallet readiness.
7879
+ function runScopedOtpFrame(run, frameUrl, signal) {
7880
+ const assertCurrent = walletLoginGuard(signal);
7881
+ const beforeStore = core.getActiveSessionManager().captureSessionEstablishment();
7882
+ return new Promise((resolve, reject) => {
7883
+ let settled = false, adopting = false, started = false;
7884
+ let user = null;
7885
+ let adoptedToken = '';
7886
+ let context = null;
7887
+ const post = (data) => { var _a; return (_a = run.iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage(data, run.signerOrigin); };
7888
+ const cleanup = () => {
7889
+ settled = true;
7890
+ clearTimeout(timer);
7891
+ window.removeEventListener('message', onMessage);
7892
+ signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', abort);
7893
+ };
7894
+ const fail = (error) => { cleanup(); reject(error instanceof Error ? error : new Error(String(error))); };
7895
+ const abort = () => { post({ type: 'bounded:otp:cancel' }); fail(new Error('wallet_login_cancelled')); };
7896
+ const finish = (ready, reason, expiresAt) => {
7897
+ if (!user || !context || core.getActiveSessionManager().getIdToken() !== adoptedToken) {
7898
+ fail(new Error('wallet_session_changed'));
7899
+ return;
7900
+ }
7901
+ setWalletSessionState(context.key, ready && Number.isFinite(expiresAt) && expiresAt > Date.now()
7902
+ ? { status: 'ready', expiresAt: expiresAt } : { status: 'verification-required', reason });
7903
+ cleanup();
7904
+ resolve(user);
7905
+ };
7906
+ const onMessage = async (event) => {
7907
+ if (settled || event.origin !== run.signerOrigin || event.source !== run.iframe.contentWindow)
7908
+ return;
7909
+ const d = event.data;
7910
+ if (!d || typeof d !== 'object')
7911
+ return;
7912
+ if (d.type === 'bounded:otp:ready' && !started) {
7913
+ started = true;
7914
+ post({ type: 'bounded:otp:start', email: run.email, ceremonyId: run.ceremonyId,
7915
+ otpId: run.otpId, otpEncryptionTargetBundle: run.otpEncryptionTargetBundle });
7916
+ }
7917
+ if (d.type === 'bounded:otp:wallet-ready' && user) {
7918
+ finish(d.ok === true, typeof d.error === 'string' ? d.error : 'wallet_session_unavailable', d.expiresAt);
7919
+ }
7920
+ if (d.type !== 'bounded:otp:result' || adopting)
7921
+ return;
7922
+ if (d.ok !== true || !d.session || typeof d.session.idToken !== 'string') {
7923
+ fail(new Error(String(d.error || 'wallet_login_failed')));
7924
+ return;
7925
+ }
7926
+ adopting = true;
7927
+ try {
7928
+ assertCurrent();
7929
+ const token = d.session.idToken;
7930
+ const claims = JSON.parse(getPlatform().atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
7931
+ context = walletContextFromClaims(claims, run.base, run.appId, window.location.origin);
7932
+ if (context.scope !== 'app')
7933
+ throw new Error('wallet_scope_mismatch');
7934
+ user = await establishInlineSession(d.session, () => { assertCurrent(); if (settled)
7935
+ throw new Error('wallet_login_cancelled'); }, beforeStore);
7936
+ adoptedToken = token;
7937
+ bindWarmSignerFrame(run.iframe, context.key);
7938
+ if (settled || (signal === null || signal === void 0 ? void 0 : signal.aborted) || core.getActiveSessionManager().getIdToken() !== token)
7939
+ throw new Error('wallet_session_changed');
7940
+ setWalletSessionState(context.key, { status: 'verification-required', reason: 'wallet_session_pending' });
7941
+ const authorization = await requestTurnkeyActionAuthorization({
7942
+ baseUrl: run.base, idToken: token, action: 'createWallet', fetchImpl: (u, i) => fetch(u, i),
7943
+ });
7944
+ if (settled || core.getActiveSessionManager().getIdToken() !== token)
7945
+ throw new Error('wallet_session_changed');
7946
+ post({ type: 'bounded:otp:authorization', authorization });
7947
+ }
7948
+ catch (error) {
7949
+ if (user && adoptedToken) {
7950
+ post({ type: 'bounded:otp:authorization', authorization: '' });
7951
+ finish(false, 'wallet_session_unavailable');
7952
+ }
7953
+ else
7954
+ fail(error);
7955
+ }
7956
+ };
7957
+ const timer = setTimeout(() => {
7958
+ if (user)
7959
+ finish(false, 'wallet_session_timeout');
7960
+ else
7961
+ fail(new Error('The sign-in code entry timed out. Please try again.'));
7962
+ }, 180000);
7963
+ window.addEventListener('message', onMessage);
7964
+ signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', abort, { once: true });
7965
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
7966
+ abort();
7967
+ return;
7968
+ }
7969
+ if (run.iframe.src !== frameUrl)
7970
+ run.iframe.src = frameUrl;
7971
+ // Readiness can predate listener installation when reusing a mounted frame.
7972
+ post({ type: 'bounded:otp:hello' });
7973
+ });
7974
+ }
7645
7975
 
7646
7976
  // bounded-login-widget.ts - the UNIFIED Bounded login surface: one in-app card
7647
7977
  // (Shadow-DOM modal from bounded-widget-modal.ts, Bounded look + motion) that folds
@@ -7865,7 +8195,7 @@ function untilCancelled(work, pending) {
7865
8195
  * run first; call from a user gesture so social/wallet popups are allowed.
7866
8196
  */
7867
8197
  async function openBoundedWidget(opts = {}) {
7868
- if (!hasDOM$2()) {
8198
+ if (!hasDOM$5()) {
7869
8199
  throw new Error("openBoundedWidget is web-only - on React Native use loginWithRedirect()");
7870
8200
  }
7871
8201
  // Registered before the first await - see PendingOpen.
@@ -8027,6 +8357,7 @@ async function prepareAndOpen(opts, pending) {
8027
8357
  // the underlying promise SETTLES, whoever owns the card by then.
8028
8358
  let attemptSeq = 0;
8029
8359
  let activeAttempt = 0;
8360
+ let abortScopedLogin = null;
8030
8361
  let abortGesture = null;
8031
8362
  /** Reject a gesture promise already waiting on a view that is going away. */
8032
8363
  const abortPendingGesture = () => {
@@ -8058,6 +8389,8 @@ async function prepareAndOpen(opts, pending) {
8058
8389
  /** Take the card back (Back, a lane switch, settling the modal). Deliberately
8059
8390
  * NOT a release: the login underneath keeps running - see above. */
8060
8391
  const invalidateAttempts = () => {
8392
+ abortScopedLogin === null || abortScopedLogin === void 0 ? void 0 : abortScopedLogin();
8393
+ abortScopedLogin = null;
8061
8394
  activeAttempt = 0;
8062
8395
  abortPendingGesture();
8063
8396
  };
@@ -8111,7 +8444,7 @@ async function prepareAndOpen(opts, pending) {
8111
8444
  const style = el("style");
8112
8445
  style.textContent = CARD_CSS;
8113
8446
  let emailDraft = "";
8114
- const renderChoose = () => {
8447
+ const renderChoose = (initialError) => {
8115
8448
  const card = el("div", "c");
8116
8449
  card.appendChild(style.cloneNode(true));
8117
8450
  const x = el("button", "x", "Close");
@@ -8121,13 +8454,15 @@ async function prepareAndOpen(opts, pending) {
8121
8454
  card.appendChild(x);
8122
8455
  card.appendChild(el("h1", undefined, title !== null && title !== void 0 ? title : "Sign in"));
8123
8456
  if (subtitle !== "") {
8124
- card.appendChild(el("p", "sub", subtitle !== null && subtitle !== void 0 ? subtitle : "One account and wallet across every Bounded app."));
8457
+ card.appendChild(el("p", "sub", subtitle !== null && subtitle !== void 0 ? subtitle : "Sign in to continue to this app."));
8125
8458
  }
8126
8459
  const status = el("div", "status");
8127
8460
  const setErr = (msg) => {
8128
8461
  status.textContent = msg;
8129
8462
  status.classList.add("err");
8130
8463
  };
8464
+ if (initialError)
8465
+ setErr(initialError);
8131
8466
  const setBusy = (btn, label) => {
8132
8467
  status.classList.remove("err");
8133
8468
  status.textContent = "";
@@ -8171,11 +8506,39 @@ async function prepareAndOpen(opts, pending) {
8171
8506
  // arrives; the code entry claims its own when the user verifies.
8172
8507
  setBusy(submit, "Sending code\u2026");
8173
8508
  startTurnkeyEmailLogin(input.value)
8174
- .then((handle) => { if (settleAttempt(attempt))
8175
- swap(renderOtpFrame(handle)); })
8509
+ .then(async (handle) => {
8510
+ if (!attemptIsCurrent(attempt)) {
8511
+ settleAttempt(attempt);
8512
+ return;
8513
+ }
8514
+ if (!handle.frameUrl || !handle.signerOrigin) {
8515
+ if (settleAttempt(attempt))
8516
+ swap(renderOtpFrame(handle));
8517
+ return;
8518
+ }
8519
+ const controller = new AbortController();
8520
+ abortScopedLogin = () => controller.abort();
8521
+ // Mount once in the shared modal. Moving a live iframe would discard
8522
+ // the trusted document holding the private in-memory session key.
8523
+ detachIframe(m);
8524
+ const frame = ensureIframe(m, handle.frameUrl, handle.signerOrigin, `login:${attempt}`);
8525
+ setPanelHeight(m, 380);
8526
+ const completion = handle.run(frame, controller.signal);
8527
+ m.chain = completion.then(() => undefined, () => undefined);
8528
+ const user = await completion;
8529
+ abortScopedLogin = null;
8530
+ settleAttempt(attempt);
8531
+ succeed(user);
8532
+ })
8176
8533
  .catch((err) => {
8177
8534
  if (!settleAttempt(attempt))
8178
8535
  return;
8536
+ if (abortScopedLogin) {
8537
+ abortScopedLogin = null;
8538
+ detachIframe(m);
8539
+ swap(renderChoose(errText(err)));
8540
+ return;
8541
+ }
8179
8542
  submit.disabled = false;
8180
8543
  submit.textContent = "Continue";
8181
8544
  setErr(errText(err));
@@ -10171,6 +10534,8 @@ exports.getCurrentUser = getCurrentUser;
10171
10534
  exports.getIdToken = getIdToken;
10172
10535
  exports.getOrCreateTurnkeyWallet = getOrCreateTurnkeyWallet;
10173
10536
  exports.getPlatform = getPlatform;
10537
+ exports.getTurnkeyWalletRecoveryUrl = getTurnkeyWalletRecoveryUrl;
10538
+ exports.getTurnkeyWalletSessionState = getTurnkeyWalletSessionState;
10174
10539
  exports.hasTurnkeyWallet = hasTurnkeyWallet;
10175
10540
  exports.init = init;
10176
10541
  exports.login = login;
@@ -10206,4 +10571,4 @@ exports.useAuth = useAuth;
10206
10571
  exports.useBuildRun = useBuildRun;
10207
10572
  exports.useQuery = useQuery;
10208
10573
  exports.waitForAuthReady = waitForAuthReady;
10209
- //# sourceMappingURL=index-CYdYsf8s.js.map
10574
+ //# sourceMappingURL=index-DUVRmF9-.js.map