@bounded-sh/client 0.0.61 → 0.0.64

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 (58) hide show
  1. package/dist/auth/bounded-login-widget.d.ts +1 -1
  2. package/dist/auth/index.d.ts +4 -5
  3. package/dist/auth/providers/email-auth-provider.d.ts +4 -4
  4. package/dist/auth/turnkey-signer-bridge.d.ts +5 -0
  5. package/dist/{email-auth-provider-9BwSziWj.js → email-auth-provider-97CluDlZ.js} +27 -30
  6. package/dist/email-auth-provider-97CluDlZ.js.map +1 -0
  7. package/dist/{email-auth-provider-BttT2sZK.js → email-auth-provider-CKM4O5Z1.js} +27 -30
  8. package/dist/email-auth-provider-CKM4O5Z1.js.map +1 -0
  9. package/dist/{email-auth-provider-B3XaJw-G.mjs → email-auth-provider-h0fPnU76.mjs} +27 -30
  10. package/dist/email-auth-provider-h0fPnU76.mjs.map +1 -0
  11. package/dist/{email-auth-provider-Cb4VKA1E.mjs → email-auth-provider-v9xvY0Ky.mjs} +27 -30
  12. package/dist/email-auth-provider-v9xvY0Ky.mjs.map +1 -0
  13. package/dist/{guest-auth-provider-BBSFawAr.mjs → guest-auth-provider-BLRbjAv2.mjs} +2 -2
  14. package/dist/{guest-auth-provider-BBPpydtM.mjs.map → guest-auth-provider-BLRbjAv2.mjs.map} +1 -1
  15. package/dist/{guest-auth-provider-Di0TRg-z.js → guest-auth-provider-BvIf5W5c.js} +2 -2
  16. package/dist/{guest-auth-provider-Di0TRg-z.js.map → guest-auth-provider-BvIf5W5c.js.map} +1 -1
  17. package/dist/{guest-auth-provider-BcR-MQ0g.js → guest-auth-provider-CLnFuodt.js} +2 -2
  18. package/dist/{guest-auth-provider-BcR-MQ0g.js.map → guest-auth-provider-CLnFuodt.js.map} +1 -1
  19. package/dist/{guest-auth-provider-BBPpydtM.mjs → guest-auth-provider-Dmup2WIL.mjs} +2 -2
  20. package/dist/{guest-auth-provider-BBSFawAr.mjs.map → guest-auth-provider-Dmup2WIL.mjs.map} +1 -1
  21. package/dist/{index-L2R6C5Vl.mjs → index-7yz-Olrm.mjs} +1371 -1525
  22. package/dist/index-7yz-Olrm.mjs.map +1 -0
  23. package/dist/{index-C_L497w1.js → index-Bk0nEjB6.js} +1373 -1528
  24. package/dist/index-Bk0nEjB6.js.map +1 -0
  25. package/dist/index.js +1 -1
  26. package/dist/index.mjs +1 -1
  27. package/dist/{index.native-BIUs8t7F.mjs → index.native-CWcQMPZd.mjs} +518 -221
  28. package/dist/index.native-CWcQMPZd.mjs.map +1 -0
  29. package/dist/{index.native-BSheiGj2.js → index.native-Do-VhPUG.js} +520 -224
  30. package/dist/index.native-Do-VhPUG.js.map +1 -0
  31. package/dist/index.native.js +1 -1
  32. package/dist/index.native.mjs +1 -1
  33. package/dist/{injected-evm-wallet-provider-DgWUaVtt.js → injected-evm-wallet-provider-B5MA0w8I.js} +2 -2
  34. package/dist/{injected-evm-wallet-provider-DgWUaVtt.js.map → injected-evm-wallet-provider-B5MA0w8I.js.map} +1 -1
  35. package/dist/{injected-evm-wallet-provider-7XawIUdJ.js → injected-evm-wallet-provider-BMiT8I-4.js} +2 -2
  36. package/dist/{injected-evm-wallet-provider-7XawIUdJ.js.map → injected-evm-wallet-provider-BMiT8I-4.js.map} +1 -1
  37. package/dist/{injected-evm-wallet-provider-DSFDC8fh.mjs → injected-evm-wallet-provider-DlPsOmik.mjs} +2 -2
  38. package/dist/{injected-evm-wallet-provider-DSFDC8fh.mjs.map → injected-evm-wallet-provider-DlPsOmik.mjs.map} +1 -1
  39. package/dist/{injected-evm-wallet-provider-DnOrVMDX.mjs → injected-evm-wallet-provider-xvZq9Zli.mjs} +2 -2
  40. package/dist/{injected-evm-wallet-provider-DnOrVMDX.mjs.map → injected-evm-wallet-provider-xvZq9Zli.mjs.map} +1 -1
  41. package/dist/{injected-wallet-provider-oCRGuls5.js → injected-wallet-provider-CSj0lY9H.js} +2 -2
  42. package/dist/{injected-wallet-provider-oCRGuls5.js.map → injected-wallet-provider-CSj0lY9H.js.map} +1 -1
  43. package/dist/{injected-wallet-provider-BMDJntMD.js → injected-wallet-provider-CoE5XQQn.js} +2 -2
  44. package/dist/{injected-wallet-provider-BMDJntMD.js.map → injected-wallet-provider-CoE5XQQn.js.map} +1 -1
  45. package/dist/{injected-wallet-provider-ZkPzVj5W.mjs → injected-wallet-provider-Dd7ij6bc.mjs} +2 -2
  46. package/dist/{injected-wallet-provider-ZkPzVj5W.mjs.map → injected-wallet-provider-Dd7ij6bc.mjs.map} +1 -1
  47. package/dist/{injected-wallet-provider-DfJvN3sV.mjs → injected-wallet-provider-YHhhFzsj.mjs} +2 -2
  48. package/dist/{injected-wallet-provider-DfJvN3sV.mjs.map → injected-wallet-provider-YHhhFzsj.mjs.map} +1 -1
  49. package/package.json +2 -2
  50. package/dist/auth/crossmint-signer-bridge.d.ts +0 -45
  51. package/dist/email-auth-provider-9BwSziWj.js.map +0 -1
  52. package/dist/email-auth-provider-B3XaJw-G.mjs.map +0 -1
  53. package/dist/email-auth-provider-BttT2sZK.js.map +0 -1
  54. package/dist/email-auth-provider-Cb4VKA1E.mjs.map +0 -1
  55. package/dist/index-C_L497w1.js.map +0 -1
  56. package/dist/index-L2R6C5Vl.mjs.map +0 -1
  57. package/dist/index.native-BIUs8t7F.mjs.map +0 -1
  58. package/dist/index.native-BSheiGj2.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { assertCanonicalBoundedRuntimeWriteCalldata, getActiveSessionManager, getConfig, WebSessionManager, ReactNativeSessionManager, revokeSession, init as init$1, onSessionCleared, reconnectWithNewAuth, deriveUserIdentityFromIdToken, subscribe, getIdToken as getIdToken$1, convertRemainingAccounts, buildSetDocumentsTransaction, genAuthNonce, genSolanaMessage, createSessionWithSignature } from '@bounded-sh/core';
1
+ import { getConfig, getActiveSessionManager, WebSessionManager, ReactNativeSessionManager, revokeSession, init as init$1, onSessionCleared, reconnectWithNewAuth, deriveUserIdentityFromIdToken, subscribe, getIdToken as getIdToken$1, convertRemainingAccounts, buildSetDocumentsTransaction, genAuthNonce, genSolanaMessage, createSessionWithSignature } from '@bounded-sh/core';
2
2
  import * as React from 'react';
3
3
 
4
4
  /**
@@ -847,6 +847,8 @@ function getDefaultExportFromCjs (x) {
847
847
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
848
848
  }
849
849
 
850
+ var safeBuffer = {exports: {}};
851
+
850
852
  var buffer = {};
851
853
 
852
854
  var base64Js = {};
@@ -3216,10 +3218,6 @@ function requireBuffer () {
3216
3218
  return buffer;
3217
3219
  }
3218
3220
 
3219
- var bufferExports = requireBuffer();
3220
-
3221
- var safeBuffer = {exports: {}};
3222
-
3223
3221
  /*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
3224
3222
 
3225
3223
  var hasRequiredSafeBuffer;
@@ -3433,327 +3431,725 @@ var basex = /*@__PURE__*/getDefaultExportFromCjs(srcExports);
3433
3431
  var ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
3434
3432
  var bs58 = basex(ALPHABET);
3435
3433
 
3436
- // crossmint-signer-bridge.ts — routes embedded-wallet signing through the
3437
- // Bounded-owned signer popup (auth.bounded.sh/wallet/signer).
3434
+ var bufferExports = requireBuffer();
3435
+
3436
+ // bounded-widget-modal.ts - the shared in-app WIDGET shell for Bounded's login
3437
+ // and signing surfaces. One Shadow-DOM overlay + panel that matches the Bounded
3438
+ // widget (Seal) look and MOTION: a backdrop fade, a panel rise on desktop, a
3439
+ // bottom-sheet on mobile, a height tween as content swaps, and a clean close.
3438
3440
  //
3439
- // Slice-1 attached a non-custodial Crossmint smart wallet to email logins and
3440
- // populated `@user.address`. This bridge is the slice-2 counterpart: a user
3441
- // whose address came from `auth.wallets` can APPROVE transactions with that
3442
- // wallet from ANY app, non-custodially, by having the Crossmint client SDK run
3443
- // ONLY on the Bounded signer page (one whitelisted origin) and talking to it via
3444
- // postMessage.
3441
+ // It hosts EITHER:
3442
+ // - NATIVE content (the login card the SDK renders itself), or
3443
+ // - an IFRAME (the Bounded-owned signer page, which must run on the single
3444
+ // signer origin because a WebAuthn passkey is bound to one origin).
3445
3445
  //
3446
- // SEMANTICS (Crossmint Solana smart wallets): these wallets cannot produce raw
3447
- // detached signatures — there is no signMessage and no sign-without-submit. The
3448
- // only honest primitive is EXECUTION (sign + submit) via the wallet's email-OTP
3449
- // authorization. So this bridge implements `signAndSubmitTransaction` and a
3450
- // token-send; `signMessage` / `signTransaction` throw a clear unsupported error.
3451
- const EMBEDDED_WALLET_UNSUPPORTED = 'Unsupported for embedded smart wallets — Crossmint Solana smart wallets sign ' +
3452
- 'and submit atomically. Use signAndSubmitTransaction() (or bounded.sendWalletToken) ' +
3453
- 'instead of signMessage/signTransaction.';
3454
- const NO_EMBEDDED_WALLET = 'This login has no embedded wallet. Enable `"auth": { "wallets": true }` in ' +
3455
- 'the app policy so email logins get a non-custodial wallet (@user.address), ' +
3456
- 'then sign from a user gesture.';
3457
- function hasDOM$5() {
3458
- return typeof window !== 'undefined' && typeof document !== 'undefined';
3446
+ // The modal is a warm singleton: it stays mounted (hidden) between uses so a
3447
+ // second open - or a signature right after login - is instant and, when it is
3448
+ // already open, a clean in-place view swap rather than a reopen.
3449
+ function hasDOM$4() {
3450
+ return typeof window !== "undefined" && typeof document !== "undefined";
3459
3451
  }
3460
- function decodeJwt$1(token) {
3461
- try {
3462
- let b64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
3463
- const pad = b64.length % 4;
3464
- if (pad === 2)
3465
- b64 += '==';
3466
- else if (pad === 3)
3467
- b64 += '=';
3468
- return JSON.parse(bufferExports.Buffer.from(b64, 'base64').toString('utf8'));
3469
- }
3470
- catch (_a) {
3471
- return {};
3472
- }
3452
+ const OVERLAY_CSS = `
3453
+ :host{all:initial}
3454
+ *{box-sizing:border-box}
3455
+ .bw-ov{position:fixed;inset:0;z-index:2147483647;display:none;
3456
+ font-family:"Schibsted Grotesk","Helvetica Neue",Arial,sans-serif}
3457
+ .bw-ov.mounted{display:block}
3458
+ .bw-back{position:fixed;inset:0;background:rgba(4,4,5,.62);opacity:0;
3459
+ transition:opacity .24s ease;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}
3460
+ .bw-wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:20px}
3461
+ .bw-panel{position:relative;width:420px;max-width:calc(100vw - 28px);height:280px;
3462
+ border-radius:14px;overflow:hidden;background:#0e0f11;border:1px solid rgba(232,230,222,.08);
3463
+ box-shadow:0 1px 0 rgba(232,230,222,.03) inset,0 40px 90px -45px rgba(0,0,0,.85);
3464
+ opacity:0;transform:translateY(14px) scale(.98);will-change:transform,opacity,height;
3465
+ transition:opacity .28s cubic-bezier(.2,.8,.2,1),transform .28s cubic-bezier(.2,.8,.2,1),
3466
+ height .3s cubic-bezier(.2,.8,.2,1)}
3467
+ .bw-ov.show .bw-back{opacity:1}
3468
+ .bw-ov.show .bw-panel{opacity:1;transform:none}
3469
+ .bw-ov.closing .bw-back{opacity:0}
3470
+ .bw-ov.closing .bw-panel{opacity:0;transform:translateY(12px) scale(.99)}
3471
+ .bw-content{position:absolute;inset:0;overflow:hidden}
3472
+ .bw-frame{display:block;width:100%;height:100%;border:0;background:transparent;color-scheme:dark}
3473
+ /* editorial corner crop-marks, matching the reference frame */
3474
+ .bw-mk{position:absolute;width:13px;height:13px;pointer-events:none;z-index:2;
3475
+ border-color:rgba(232,230,222,.22);border-style:solid;border-width:0}
3476
+ .bw-mk.tl{top:11px;left:11px;border-top-width:1px;border-left-width:1px}
3477
+ .bw-mk.tr{top:11px;right:11px;border-top-width:1px;border-right-width:1px}
3478
+ .bw-mk.bl{bottom:11px;left:11px;border-bottom-width:1px;border-left-width:1px}
3479
+ .bw-mk.br{bottom:11px;right:11px;border-bottom-width:1px;border-right-width:1px}
3480
+ @media(max-width:560px){
3481
+ .bw-wrap{align-items:flex-end;padding:0}
3482
+ .bw-panel{width:100%;max-width:100%;border-radius:18px 18px 0 0;border-bottom:0}
3483
+ .bw-content{overflow-y:auto}
3484
+ .bw-ov.show .bw-panel{transform:none}
3485
+ .bw-ov:not(.show) .bw-panel,.bw-ov.closing .bw-panel{transform:translateY(18%)}
3473
3486
  }
3474
- // Pull the embedded-wallet identity out of the active session's id token.
3475
- // Returns null when this session has no wallet (so callers can throw the
3476
- // informative NO_EMBEDDED_WALLET error).
3477
- function walletClaimsFromSession(chain = 'solana') {
3478
- let idToken = null;
3479
- try {
3480
- idToken = getActiveSessionManager().getIdToken();
3481
- }
3482
- catch (_a) {
3483
- idToken = null;
3487
+ @media(prefers-reduced-motion:reduce){.bw-back,.bw-panel{transition:opacity .12s linear!important}}
3488
+ `;
3489
+ let modal = null;
3490
+ function getModal() {
3491
+ if (modal)
3492
+ return modal;
3493
+ const root = document.createElement("div");
3494
+ root.setAttribute("data-bounded-widget", "");
3495
+ const shadow = root.attachShadow({ mode: "open" });
3496
+ const style = document.createElement("style");
3497
+ style.textContent = OVERLAY_CSS;
3498
+ shadow.appendChild(style);
3499
+ const overlay = document.createElement("div");
3500
+ overlay.className = "bw-ov";
3501
+ const back = document.createElement("div");
3502
+ back.className = "bw-back";
3503
+ const wrap = document.createElement("div");
3504
+ wrap.className = "bw-wrap";
3505
+ const panel = document.createElement("div");
3506
+ panel.className = "bw-panel";
3507
+ const content = document.createElement("div");
3508
+ content.className = "bw-content";
3509
+ panel.appendChild(content);
3510
+ for (const corner of ["tl", "tr", "bl", "br"]) {
3511
+ const mk = document.createElement("span");
3512
+ mk.className = "bw-mk " + corner;
3513
+ panel.appendChild(mk);
3484
3514
  }
3485
- if (!idToken)
3486
- return null;
3487
- const c = decodeJwt$1(idToken);
3488
- const address = c['custom:walletAddress'];
3489
- const evmAddress = c['custom:evmWalletAddress'];
3490
- const email = c['email'];
3491
- if (typeof email !== 'string' || !email)
3492
- return null;
3493
- if (chain === 'solana' && (typeof address !== 'string' || !address))
3494
- return null;
3495
- if (chain === 'evm' && (typeof evmAddress !== 'string' || !/^0x[0-9a-fA-F]{40}$/.test(evmAddress)))
3496
- return null;
3497
- const envClaim = c['custom:walletEnvironment'];
3498
- const environment = envClaim === 'staging' || envClaim === 'production' ? envClaim : undefined;
3499
- return Object.assign(Object.assign(Object.assign({ email }, (typeof address === 'string' && address ? { address } : {})), (typeof evmAddress === 'string' ? { evmAddress: evmAddress.toLowerCase() } : {})), { environment });
3500
- }
3501
- async function signerPageUrl() {
3502
- const cfg = await getConfig();
3503
- const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
3504
- return { url: `${base}/wallet/signer`, origin: new URL(base).origin };
3505
- }
3506
- let requestSeq$1 = 0;
3507
- /**
3508
- * Open the Bounded signer popup, send `action` to it, and resolve with the
3509
- * submitted transaction result. Must be called from a USER GESTURE (click/tap) or
3510
- * the browser will block the popup — in which case this rejects with a clear
3511
- * "Popup blocked" error telling the app to call from a user gesture.
3512
- *
3513
- * Non-custodial: the popup gates signing behind an email OTP delivered to the
3514
- * wallet's owner, and validates that the wallet address matches the account.
3515
- */
3516
- async function signViaSignerPopup(action) {
3517
- if (!hasDOM$5()) {
3518
- throw new Error('Embedded-wallet signing is only available in the browser (popup-based).');
3519
- }
3520
- const isEvm = action.kind === 'evmTransaction';
3521
- const claims = walletClaimsFromSession(isEvm ? 'evm' : 'solana');
3522
- if (!claims)
3523
- throw new Error(NO_EMBEDDED_WALLET);
3524
- const { url, origin: signerOrigin } = await signerPageUrl();
3525
- const requestId = `bsign_${Date.now()}_${++requestSeq$1}`;
3526
- // Centered popup, sized for the OTP form (matches the OIDC login popup shape).
3527
- const w = 420, h = 600;
3528
- const left = window.screenX + Math.max(0, (window.outerWidth - w) / 2);
3529
- const top = window.screenY + Math.max(0, (window.outerHeight - h) / 2);
3530
- const popup = window.open(url, 'bounded-wallet-signer', `width=${w},height=${h},left=${left},top=${top}`);
3531
- if (!popup) {
3532
- throw new Error('Popup blocked — call signAndSubmitTransaction() from a user gesture (e.g. a click handler) so the wallet approval window can open.');
3533
- }
3534
- const request = {
3535
- type: 'bounded:sign',
3536
- requestId,
3537
- environment: claims.environment, // may be undefined → page auto-detects
3538
- email: claims.email,
3539
- address: isEvm ? claims.evmAddress : claims.address,
3540
- action,
3515
+ wrap.appendChild(panel);
3516
+ overlay.appendChild(back);
3517
+ overlay.appendChild(wrap);
3518
+ shadow.appendChild(overlay);
3519
+ (document.body || document.documentElement).appendChild(root);
3520
+ const m = {
3521
+ root,
3522
+ overlay,
3523
+ panel,
3524
+ content,
3525
+ iframe: null,
3526
+ iframeOrigin: "",
3527
+ iframeReady: false,
3528
+ onBackdrop: null,
3529
+ onIframeMessage: null,
3530
+ chain: Promise.resolve(),
3531
+ fitObserver: null,
3541
3532
  };
3542
- return await new Promise((resolve, reject) => {
3543
- let settled = false;
3544
- let closeTimer;
3545
- let sentRequest = false;
3546
- const cleanup = () => {
3547
- settled = true;
3548
- window.removeEventListener('message', onMsg);
3549
- if (closeTimer)
3550
- clearInterval(closeTimer);
3551
- };
3552
- const sendRequest = () => {
3553
- if (sentRequest)
3554
- return;
3555
- sentRequest = true;
3556
- try {
3557
- popup.postMessage(request, signerOrigin);
3558
- }
3559
- catch ( /* popup gone */_a) { /* popup gone */ }
3560
- };
3561
- const onMsg = (ev) => {
3562
- if (ev.origin !== signerOrigin)
3563
- return; // only trust the signer page origin
3564
- if (ev.source !== popup)
3565
- return; // only trust this signer popup window
3566
- const d = ev.data;
3567
- if (!d || typeof d !== 'object')
3568
- return;
3569
- if (d.type === 'bounded:signer:ready') {
3570
- sendRequest();
3571
- return;
3572
- }
3573
- if (d.type === 'bounded:signer:result' && d.requestId === requestId) {
3574
- cleanup();
3575
- try {
3576
- popup.close();
3577
- }
3578
- catch ( /* ignore */_a) { /* ignore */ }
3579
- if (d.ok)
3580
- resolve({ hash: d.hash, explorerLink: d.explorerLink, transactionId: d.transactionId });
3581
- else
3582
- reject(new Error(String(d.error || 'Wallet signing failed')));
3583
- }
3584
- };
3585
- window.addEventListener('message', onMsg);
3586
- // Fallback: if the ready ping was missed (fast load / focus race), push the
3587
- // request shortly after opening. The page ignores it until a bounded:sign
3588
- // arrives and only acts on the first one, so a duplicate is harmless.
3589
- setTimeout(sendRequest, 1200);
3590
- // If the user closes the popup before completing, reject (unless a result
3591
- // just arrived — give any in-flight message a beat to settle first).
3592
- closeTimer = setInterval(() => {
3593
- if (popup.closed) {
3594
- clearInterval(closeTimer);
3595
- setTimeout(() => { if (!settled) {
3596
- cleanup();
3597
- reject(new Error('Wallet approval window was closed before signing completed.'));
3598
- } }, 400);
3599
- }
3600
- }, 500);
3601
- // Overall safety timeout (OTP + signing). Generous — the user may take a
3602
- // while to fetch the code from their email.
3603
- setTimeout(() => { if (!settled) {
3604
- cleanup();
3605
- try {
3606
- popup.close();
3607
- }
3608
- catch ( /* ignore */_a) { /* ignore */ }
3609
- reject(new Error('Wallet approval timed out.'));
3610
- } }, 5 * 60 * 1000);
3533
+ // The dismiss handler lives on `.bw-wrap`: it is the full-viewport flex layer
3534
+ // that sits ABOVE `.bw-back`, so it (not the backdrop element) receives clicks
3535
+ // outside the panel. Fire only when the click lands on the wrap itself, never
3536
+ // when it bubbles up from the panel or its content.
3537
+ wrap.addEventListener("click", (event) => {
3538
+ if (event.target === wrap && m.onBackdrop)
3539
+ m.onBackdrop();
3540
+ });
3541
+ window.addEventListener("message", (event) => {
3542
+ if (!m.iframe)
3543
+ return;
3544
+ if (event.origin !== m.iframeOrigin)
3545
+ return;
3546
+ if (event.source !== m.iframe.contentWindow)
3547
+ return;
3548
+ const d = event.data;
3549
+ if (!d || typeof d !== "object")
3550
+ return;
3551
+ if (m.onIframeMessage)
3552
+ m.onIframeMessage(d);
3611
3553
  });
3554
+ modal = m;
3555
+ return m;
3612
3556
  }
3613
- // Serialize a legacy or versioned Solana transaction to BASE58 for the signer
3614
- // page. Crossmint's smart-wallet createTransaction endpoint requires the
3615
- // `serializedTransaction` string to be base58 — it rejects base64 with
3616
- // "Invalid base58 string" (verified against Crossmint staging 2026-07-07). The
3617
- // `wallets-sdk` object path bs58-encodes internally, so base58 is the contract.
3618
- //
3619
- // We do NOT require existing signatures (the smart wallet signs on the page);
3620
- // Crossmint extracts the instructions, overrides the fee payer to its sponsor,
3621
- // and re-wraps them for the smart wallet.
3622
- function serializeTransaction(transaction) {
3623
- const isLegacy = 'recentBlockhash' in transaction &&
3624
- !('message' in transaction && 'staticAccountKeys' in transaction.message);
3625
- const bytes = isLegacy
3626
- ? transaction.serialize({ requireAllSignatures: false, verifySignatures: false })
3627
- : transaction.serialize();
3628
- return bs58.encode(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
3557
+ function showModal(m) {
3558
+ m.overlay.classList.remove("closing");
3559
+ m.overlay.classList.add("mounted");
3560
+ // Force a reflow so the .show transition runs from the hidden state every open.
3561
+ void m.panel.offsetWidth;
3562
+ m.overlay.classList.add("show");
3563
+ }
3564
+ function hideModal(m, after) {
3565
+ m.overlay.classList.remove("show");
3566
+ m.overlay.classList.add("closing");
3567
+ window.setTimeout(() => {
3568
+ m.overlay.classList.remove("mounted", "closing");
3569
+ }, 260);
3570
+ }
3571
+ function setPanelHeight(m, px) {
3572
+ const h = Math.max(140, Math.min(680, Math.round(px)));
3573
+ m.panel.style.height = h + "px";
3574
+ }
3575
+ // True outer height of the mounted card: the max of its padding-box scroll height
3576
+ // (catches overflowing descendants) and its border-box height plus own vertical
3577
+ // margins (catches border + margins, which scrollHeight omits). Ceiled so a
3578
+ // sub-pixel row is never clipped under `overflow:hidden`.
3579
+ function measureOuterHeight(target) {
3580
+ var _a;
3581
+ const cs = (_a = target.ownerDocument.defaultView) === null || _a === void 0 ? void 0 : _a.getComputedStyle(target);
3582
+ const marginY = cs ? parseFloat(cs.marginTop) + parseFloat(cs.marginBottom) : 0;
3583
+ const borderBox = target.offsetHeight + (Number.isFinite(marginY) ? marginY : 0);
3584
+ const rect = target.getBoundingClientRect().height;
3585
+ return Math.ceil(Math.max(target.scrollHeight, borderBox, rect));
3629
3586
  }
3630
3587
  /**
3631
- * Whether the active session carries an embedded wallet (so the provider should
3632
- * route signing through the popup rather than throw "no wallet").
3588
+ * Measure the currently mounted native content and tween the panel to fit it.
3589
+ * Re-fits automatically when the card's intrinsic height changes - notably after
3590
+ * the Newsreader/Schibsted web fonts swap in (which grows line boxes a few px
3591
+ * after first paint) and on inline view swaps (email -> OTP code -> success).
3633
3592
  */
3634
- function hasEmbeddedWallet() {
3635
- return walletClaimsFromSession() != null;
3636
- }
3637
- /** Whether the active email/social session carries an embedded Crossmint EVM wallet. */
3638
- function hasEmbeddedEvmWallet() {
3639
- return walletClaimsFromSession('evm') != null;
3640
- }
3641
- /** signAndSubmitTransaction for embedded wallets: serialize + route through the
3642
- * signer popup, resolve with the on-chain transaction signature/hash. */
3643
- async function embeddedSignAndSubmit(transaction) {
3644
- const serializedTransaction = serializeTransaction(transaction);
3645
- const result = await signViaSignerPopup({ kind: 'transaction', serializedTransaction });
3646
- const hash = result.hash || result.transactionId;
3647
- if (!hash)
3648
- throw new Error('Signer returned no transaction hash.');
3649
- return hash;
3650
- }
3651
- /** Submit a validated BoundedRuntime EVM write through the user's email signer. */
3652
- async function embeddedRunEvmTransaction(transaction) {
3593
+ function fitPanelToContent(m) {
3653
3594
  var _a, _b;
3654
- if (!transaction.to || !transaction.data || !transaction.chainId)
3655
- throw new Error('Incomplete embedded EVM transaction');
3656
- if (!/^0x[0-9a-fA-F]{40}$/.test(transaction.to))
3657
- throw new Error('Invalid embedded EVM runtime address');
3658
- assertCanonicalBoundedRuntimeWriteCalldata(transaction.data);
3659
- if (!Number.isSafeInteger(transaction.chainId) || transaction.chainId <= 0)
3660
- throw new Error('Invalid embedded EVM chain id');
3661
- if (transaction.nonce !== undefined)
3662
- throw new Error('Server-supplied EVM nonces are not accepted');
3663
- try {
3664
- if (BigInt((_a = transaction.value) !== null && _a !== void 0 ? _a : '0') !== BigInt(0))
3665
- throw new Error('Bounded EVM writes cannot transfer native value');
3666
- }
3667
- catch (error) {
3668
- if (error instanceof Error && error.message.includes('cannot transfer'))
3669
- throw error;
3670
- throw new Error('Invalid embedded EVM transaction value');
3595
+ const first = m.content.firstElementChild;
3596
+ const target = first || m.content;
3597
+ const apply = () => {
3598
+ // Only fit while this exact node is still the mounted card.
3599
+ if (m.content.firstElementChild !== first && first)
3600
+ return;
3601
+ const h = measureOuterHeight(target);
3602
+ if (h > 0)
3603
+ setPanelHeight(m, h);
3604
+ };
3605
+ apply();
3606
+ const view = target.ownerDocument.defaultView;
3607
+ // Re-fit after fonts load (one-shot) and once the ResizeObserver-less browsers
3608
+ // have painted, so the height never lags a late reflow.
3609
+ (_a = view === null || view === void 0 ? void 0 : view.requestAnimationFrame) === null || _a === void 0 ? void 0 : _a.call(view, apply);
3610
+ const fonts = target.ownerDocument.fonts;
3611
+ if (fonts === null || fonts === void 0 ? void 0 : fonts.ready)
3612
+ fonts.ready.then(apply).catch(() => { });
3613
+ (_b = m.fitObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
3614
+ m.fitObserver = null;
3615
+ if (first && typeof (view === null || view === void 0 ? void 0 : view.ResizeObserver) === "function") {
3616
+ const ro = new view.ResizeObserver(() => apply());
3617
+ ro.observe(first);
3618
+ m.fitObserver = ro;
3671
3619
  }
3672
- const result = await signViaSignerPopup({
3673
- kind: 'evmTransaction',
3674
- transaction: { to: transaction.to, data: transaction.data, value: (_b = transaction.value) !== null && _b !== void 0 ? _b : '0', chainId: transaction.chainId },
3675
- });
3676
- const hash = result.hash || result.transactionId;
3677
- if (!hash)
3678
- throw new Error('Signer returned no EVM transaction hash.');
3679
- return { transactionSignature: hash, blockNumber: 0, gasUsed: '0', data: result };
3620
+ }
3621
+ /** Switch the modal into NATIVE-content mode: detach any iframe, mount `node`. */
3622
+ function mountContent(m, node) {
3623
+ var _a;
3624
+ detachIframe(m);
3625
+ (_a = m.fitObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
3626
+ m.fitObserver = null;
3627
+ m.content.replaceChildren(node);
3628
+ m.content.style.display = "block";
3629
+ }
3630
+ /** Get (or lazily create) the warm iframe for signer/hosted-page mode. */
3631
+ function ensureIframe(m, url, origin) {
3632
+ var _a;
3633
+ if (m.iframe && m.iframeOrigin === origin && m.iframe.src === url)
3634
+ return m.iframe;
3635
+ detachIframe(m);
3636
+ (_a = m.fitObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
3637
+ m.fitObserver = null;
3638
+ m.content.style.display = "none";
3639
+ const iframe = document.createElement("iframe");
3640
+ iframe.className = "bw-frame";
3641
+ iframe.setAttribute("title", "Bounded");
3642
+ // Grant the framed origin WebAuthn get + create so passkey enroll/sign run
3643
+ // inside the frame from the user's click (transient activation stays in-frame).
3644
+ iframe.setAttribute("allow", "publickey-credentials-get; publickey-credentials-create; clipboard-write");
3645
+ iframe.src = url;
3646
+ m.iframeOrigin = origin;
3647
+ m.iframeReady = false;
3648
+ m.panel.appendChild(iframe);
3649
+ m.iframe = iframe;
3650
+ return iframe;
3651
+ }
3652
+ function detachIframe(m) {
3653
+ if (m.iframe) {
3654
+ try {
3655
+ m.iframe.remove();
3656
+ }
3657
+ catch (_a) {
3658
+ /* ignore */
3659
+ }
3660
+ }
3661
+ m.iframe = null;
3662
+ m.iframeReady = false;
3663
+ m.iframeOrigin = "";
3664
+ m.onIframeMessage = null;
3665
+ }
3666
+ // Load the Bounded editorial typefaces (Newsreader display + Schibsted Grotesk body +
3667
+ // JetBrains Mono) once at the document level so they apply inside the shadow root. A
3668
+ // no-op if already present or off-DOM. Fonts loaded here match the hosted auth page.
3669
+ 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";
3670
+ function ensureFonts() {
3671
+ if (!hasDOM$4())
3672
+ return;
3673
+ if (document.querySelector('link[data-bounded-fonts="1"]'))
3674
+ return;
3675
+ const pre1 = document.createElement("link");
3676
+ pre1.rel = "preconnect";
3677
+ pre1.href = "https://fonts.googleapis.com";
3678
+ const pre2 = document.createElement("link");
3679
+ pre2.rel = "preconnect";
3680
+ pre2.href = "https://fonts.gstatic.com";
3681
+ pre2.crossOrigin = "anonymous";
3682
+ const link = document.createElement("link");
3683
+ link.rel = "stylesheet";
3684
+ link.href = FONTS_HREF;
3685
+ link.setAttribute("data-bounded-fonts", "1");
3686
+ document.head.append(pre1, pre2, link);
3680
3687
  }
3681
3688
 
3682
- // Same message the email provider uses when a hosted (OIDC) login has no
3683
- // embedded wallet — enable `auth.wallets` in policy to get one.
3684
- const OIDC_NO_WALLET = 'This login has no embedded wallet. Enable `"auth": { "wallets": true }` in ' +
3685
- 'the app policy so logins get a non-custodial Crossmint wallet (@user.address), ' +
3686
- 'then sign from a user gesture.';
3687
- const PKCE_STORE_KEY = 'bounded.oidc.pkce';
3688
- const STORED_AUTH_METHOD_KEY$1 = 'bounded_last_auth_method';
3689
- const ISSUER_SESSION_KIND_KEY$1 = 'bounded_issuer_session_kind';
3690
- // Active session store (Web on web, RN on React Native) — selected by config,
3691
- // not by `typeof window` (truthy on RN). OIDC redirect/popup is web-only, but the
3692
- // session it mints is read back through this same selector everywhere.
3693
- const sessionManager = getActiveSessionManager;
3694
- function hasDOM$4() {
3689
+ // turnkey-wallet-widget.ts - runs one Turnkey signer exchange inside the shared
3690
+ // in-app WIDGET (bounded-widget-modal.ts). The Bounded-owned signer page is loaded
3691
+ // in a warm iframe behind the modal so signing feels native: an in-app card plus
3692
+ // the OS Face ID / Touch ID sheet, never a jarring popup.
3693
+ //
3694
+ // WHY AN IFRAME: a WebAuthn passkey is bound to ONE origin, so passkey create/sign
3695
+ // MUST run on the single Bounded signer origin. The iframe carries the
3696
+ // publickey-credentials-{get,create} permission policy and the user's click lands
3697
+ // INSIDE it, giving WebAuthn the transient activation it needs.
3698
+ //
3699
+ // SAFARI / restricted embedders: cross-origin iframe passkey CREATE is unreliable
3700
+ // in Safari. The signer detects that and posts `fallbackPopup`; we then rerun the
3701
+ // SAME request through `popupFallback` (a real window). Callers pass a popup
3702
+ // implementation and never see the difference.
3703
+ /**
3704
+ * Run one signer exchange through the in-app widget. Resolves when `onResult`
3705
+ * accepts a result; falls back to `popupFallback` if the frame cannot run WebAuthn
3706
+ * (Safari cross-origin create) or cannot be mounted.
3707
+ */
3708
+ async function runWidget(url, signerOrigin, ex, popupFallback) {
3709
+ if (!hasDOM$4())
3710
+ return popupFallback();
3711
+ const m = getModal();
3712
+ const requestId = String(ex.request.requestId);
3713
+ // Serialize: WebAuthn is modal, and one warm frame handles one request at a time.
3714
+ const run = m.chain.then(() => new Promise((resolve, reject) => {
3715
+ let settled = false;
3716
+ let usedFallback = false;
3717
+ const iframe = ensureIframe(m, url, signerOrigin);
3718
+ const send = () => {
3719
+ if (!iframe.contentWindow)
3720
+ return;
3721
+ try {
3722
+ iframe.contentWindow.postMessage(ex.request, signerOrigin);
3723
+ }
3724
+ catch (_a) {
3725
+ /* frame gone */
3726
+ }
3727
+ };
3728
+ let loadTimer = 0;
3729
+ let ceilingTimer = 0;
3730
+ const finish = (fn) => {
3731
+ if (settled)
3732
+ return;
3733
+ settled = true;
3734
+ m.onIframeMessage = null;
3735
+ m.onBackdrop = null;
3736
+ window.clearTimeout(loadTimer);
3737
+ window.clearTimeout(ceilingTimer);
3738
+ fn();
3739
+ };
3740
+ const goFallback = () => {
3741
+ if (usedFallback || settled)
3742
+ return;
3743
+ usedFallback = true;
3744
+ hideModal(m);
3745
+ finish(() => popupFallback().then(resolve, reject));
3746
+ };
3747
+ m.onBackdrop = () => {
3748
+ hideModal(m);
3749
+ finish(() => reject(new Error("cancelled")));
3750
+ };
3751
+ m.onIframeMessage = (d) => {
3752
+ if (d.type === "bounded:turnkey:ready") {
3753
+ m.iframeReady = true;
3754
+ send();
3755
+ return;
3756
+ }
3757
+ if (d.type === "bounded:turnkey:resize" && typeof d.height === "number") {
3758
+ setPanelHeight(m, d.height);
3759
+ return;
3760
+ }
3761
+ if (d.type === "bounded:turnkey:fallbackPopup" && d.requestId === requestId) {
3762
+ goFallback();
3763
+ return;
3764
+ }
3765
+ if (d.type === "bounded:turnkey:result" && d.requestId === requestId) {
3766
+ const outcome = ex.onResult(d);
3767
+ if ("error" in outcome) {
3768
+ hideModal(m);
3769
+ finish(() => reject(new Error(outcome.error)));
3770
+ return;
3771
+ }
3772
+ if (outcome.done) {
3773
+ hideModal(m);
3774
+ finish(() => resolve(outcome.value));
3775
+ }
3776
+ }
3777
+ };
3778
+ showModal(m);
3779
+ if (m.iframeReady)
3780
+ send();
3781
+ // If the frame never loads, degrade to the popup (best-effort - a post-load
3782
+ // popup may be blocked without a gesture, in which case the fallback rejects).
3783
+ loadTimer = window.setTimeout(() => {
3784
+ if (!m.iframeReady && !settled)
3785
+ goFallback();
3786
+ }, 8000);
3787
+ // Absolute ceiling so a stuck flow cannot hang forever.
3788
+ ceilingTimer = window.setTimeout(() => {
3789
+ if (!settled) {
3790
+ hideModal(m);
3791
+ finish(() => reject(new Error("Wallet approval timed out.")));
3792
+ }
3793
+ }, 5 * 60 * 1000);
3794
+ }));
3795
+ m.chain = run.then(() => undefined, () => undefined);
3796
+ return run;
3797
+ }
3798
+
3799
+ // turnkey-signer-bridge.ts - routes NON-CUSTODIAL Turnkey embedded-wallet signing
3800
+ // through the Bounded-owned signer page (auth.bounded.sh/wallet/turnkey/signer).
3801
+ //
3802
+ // TRANSPORT: by default the signer runs inside the in-app WIDGET (turnkey-wallet-
3803
+ // widget.ts) - an iframe behind a Shadow-DOM modal that matches the Bounded widget
3804
+ // look + motion, so signing feels native. Safari (unreliable at iframe passkey
3805
+ // CREATE) and any environment where the frame cannot mount fall back to a real
3806
+ // popup window; both transports speak the same postMessage protocol.
3807
+ //
3808
+ // Turnkey wallets produce raw detached signatures. The signer runs the Turnkey
3809
+ // browser SDK on the single Bounded origin and stamps an ed25519 signRawPayload
3810
+ // with the user's passkey. The app retains the transaction itself.
3811
+ //
3812
+ // ONE ORIGIN, ONE PASSKEY: a WebAuthn passkey is bound to a single origin, so ALL
3813
+ // Bounded apps (any subdomain / custom domain) sign through this one origin - giving
3814
+ // every user one wallet + one passkey that roams across every app.
3815
+ function hasDOM$3() {
3695
3816
  return typeof window !== 'undefined' && typeof document !== 'undefined';
3696
3817
  }
3697
- function decodeJwt(token) {
3818
+ // Safari (incl. iOS WebViews) is unreliable at running a WebAuthn CREATE inside a
3819
+ // cross-origin iframe, so the widget's automatic in-iframe passkey enrollment can
3820
+ // fail - and a post-failure popup would no longer have a user gesture to open with.
3821
+ // There we pick the popup up front, from the original click, so enrollment is solid.
3822
+ // Everywhere else the inline widget is used (buttery in-app modal).
3823
+ function preferPopup() {
3824
+ if (!hasDOM$3() || typeof navigator === 'undefined')
3825
+ return true;
3826
+ const ua = navigator.userAgent || '';
3827
+ return /^((?!chrome|android|crios|fxios|edg|edga|opr).)*safari/i.test(ua);
3828
+ }
3829
+ // Dispatch one signer exchange through the in-app WIDGET, degrading to the POPUP
3830
+ // when we cannot embed (Safari enrollment, or the frame never loads).
3831
+ async function deliver(url, origin, ex) {
3832
+ const popup = () => runPopup(url, origin, ex);
3833
+ if (preferPopup())
3834
+ return popup();
3835
+ return runWidget(url, origin, { request: ex.request, onResult: ex.onResult }, popup);
3836
+ }
3837
+ async function turnkeySignerPage() {
3838
+ const cfg = await getConfig();
3839
+ const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
3840
+ return { url: `${base}/wallet/turnkey/signer`, origin: new URL(base).origin };
3841
+ }
3842
+ function toHex(bytes) {
3843
+ let h = '';
3844
+ for (const b of bytes)
3845
+ h += b.toString(16).padStart(2, '0');
3846
+ return h;
3847
+ }
3848
+ function fromHex(hex) {
3849
+ const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
3850
+ const out = new Uint8Array(clean.length / 2);
3851
+ for (let i = 0; i < out.length; i++)
3852
+ out[i] = parseInt(clean.substr(i * 2, 2), 16);
3853
+ return out;
3854
+ }
3855
+ function decodeJwt$1(token) {
3698
3856
  try {
3699
- let b64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
3700
- const pad = b64.length % 4;
3701
- if (pad === 2)
3702
- b64 += '==';
3703
- else if (pad === 3)
3704
- b64 += '=';
3705
- return JSON.parse(getPlatform().atob(b64));
3857
+ let value = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
3858
+ const padding = value.length % 4;
3859
+ if (padding === 2)
3860
+ value += '==';
3861
+ else if (padding === 3)
3862
+ value += '=';
3863
+ return JSON.parse(getPlatform().atob(value));
3706
3864
  }
3707
3865
  catch (_a) {
3708
3866
  return {};
3709
3867
  }
3710
3868
  }
3711
- // A minimal AuthProvider so getSession()/logout() work for OIDC sessions, mirroring email.
3712
- // Signing surface (slice 2): a hosted login on a wallets-enabled app carries an
3713
- // embedded Crossmint wallet (@user.address). signAndSubmitTransaction routes
3714
- // through the Bounded signer popup; signMessage/signTransaction are structurally
3715
- // unsupported for Crossmint smart wallets (sign+submit atomically, no raw sig).
3716
- const oidcProviderSingleton = {
3717
- async login() {
3718
- const idToken = sessionManager().getIdToken();
3719
- return idToken ? userFromIdToken(idToken) : null;
3720
- },
3721
- async getUser() {
3722
- const idToken = sessionManager().getIdToken();
3723
- return idToken ? userFromIdToken(idToken) : null;
3869
+ function activeTurnkeyAddress() {
3870
+ let token = null;
3871
+ try {
3872
+ token = getActiveSessionManager().getIdToken();
3873
+ }
3874
+ catch (_a) {
3875
+ token = null;
3876
+ }
3877
+ if (!token)
3878
+ return null;
3879
+ const claims = decodeJwt$1(token);
3880
+ const address = claims['custom:walletAddress'];
3881
+ const provider = claims['custom:walletProvider'];
3882
+ return provider === 'turnkey' && typeof address === 'string' && address
3883
+ ? address
3884
+ : null;
3885
+ }
3886
+ let requestSeq = 0;
3887
+ // Shared popup driver: open the signer page, do the ready/postMessage handshake, and
3888
+ // resolve when `onResult` accepts a result message. Origin + source are pinned.
3889
+ async function runPopup(url, signerOrigin, ex) {
3890
+ var _a, _b;
3891
+ const w = (_a = ex.width) !== null && _a !== void 0 ? _a : 420, h = (_b = ex.height) !== null && _b !== void 0 ? _b : 620;
3892
+ const left = window.screenX + Math.max(0, (window.outerWidth - w) / 2);
3893
+ const top = window.screenY + Math.max(0, (window.outerHeight - h) / 2);
3894
+ const popup = window.open(url, 'bounded-turnkey-signer', `width=${w},height=${h},left=${left},top=${top}`);
3895
+ if (!popup) {
3896
+ throw new Error('Popup blocked - call this from a user gesture (e.g. a click handler) so the passkey approval window can open.');
3897
+ }
3898
+ return await new Promise((resolve, reject) => {
3899
+ let settled = false;
3900
+ let sentRequest = false;
3901
+ let closeTimer;
3902
+ const cleanup = () => { settled = true; window.removeEventListener('message', onMsg); if (closeTimer)
3903
+ clearInterval(closeTimer); };
3904
+ const sendRequest = () => { if (sentRequest)
3905
+ return; sentRequest = true; try {
3906
+ popup.postMessage(ex.request, signerOrigin);
3907
+ }
3908
+ catch ( /* popup gone */_a) { /* popup gone */ } };
3909
+ const onMsg = (event) => {
3910
+ if (event.origin !== signerOrigin)
3911
+ return;
3912
+ if (event.source !== popup)
3913
+ return;
3914
+ const d = event.data;
3915
+ if (!d || typeof d !== 'object')
3916
+ return;
3917
+ if (d.type === 'bounded:turnkey:ready') {
3918
+ sendRequest();
3919
+ return;
3920
+ }
3921
+ if (d.type === 'bounded:turnkey:result' && d.requestId === ex.request.requestId) {
3922
+ const outcome = ex.onResult(d);
3923
+ if ('error' in outcome) {
3924
+ cleanup();
3925
+ try {
3926
+ popup.close();
3927
+ }
3928
+ catch ( /* ignore */_a) { /* ignore */ }
3929
+ reject(new Error(outcome.error));
3930
+ return;
3931
+ }
3932
+ if (outcome.done) {
3933
+ cleanup();
3934
+ try {
3935
+ popup.close();
3936
+ }
3937
+ catch ( /* ignore */_b) { /* ignore */ }
3938
+ resolve(outcome.value);
3939
+ }
3940
+ }
3941
+ };
3942
+ window.addEventListener('message', onMsg);
3943
+ setTimeout(sendRequest, 1200);
3944
+ closeTimer = setInterval(() => {
3945
+ if (popup.closed) {
3946
+ clearInterval(closeTimer);
3947
+ setTimeout(() => { if (!settled) {
3948
+ cleanup();
3949
+ reject(new Error('Wallet approval window was closed before it completed.'));
3950
+ } }, 400);
3951
+ }
3952
+ }, 500);
3953
+ setTimeout(() => { if (!settled) {
3954
+ cleanup();
3955
+ try {
3956
+ popup.close();
3957
+ }
3958
+ catch ( /* ignore */_a) { /* ignore */ }
3959
+ reject(new Error('Wallet approval timed out.'));
3960
+ } }, 5 * 60 * 1000);
3961
+ });
3962
+ }
3963
+ function hasTurnkeyWallet() {
3964
+ return activeTurnkeyAddress() !== null;
3965
+ }
3966
+ /**
3967
+ * Get-or-create the user's non-custodial Turnkey wallet, resolving with its stable
3968
+ * Solana address. On first use the popup enrolls a device passkey (Face ID / Touch ID)
3969
+ * and creates the wallet (lazy) with no email code; if the wallet already exists it
3970
+ * resolves immediately without a prompt. Call from a USER GESTURE.
3971
+ *
3972
+ * Use this to learn the fee-payer address BEFORE building a transaction to sign.
3973
+ */
3974
+ async function getOrCreateTurnkeyWallet() {
3975
+ if (!hasDOM$3())
3976
+ throw new Error('Turnkey embedded wallets are only available in the browser (popup-based).');
3977
+ const { url, origin } = await turnkeySignerPage();
3978
+ const requestId = `tkcreate_${Date.now()}_${++requestSeq}`;
3979
+ return await deliver(url, origin, {
3980
+ request: { type: 'bounded:turnkey:createWallet', requestId },
3981
+ onResult: (d) => {
3982
+ if (d.ok && typeof d.address === 'string')
3983
+ return { done: true, value: { address: d.address, subOrgId: String(d.subOrgId || '') } };
3984
+ return { error: String(d.error || 'Wallet setup failed') };
3985
+ },
3986
+ });
3987
+ }
3988
+ /**
3989
+ * Open the Bounded Turnkey signer popup and sign `message` (raw bytes) with the
3990
+ * user's non-custodial Turnkey wallet, resolving with the ed25519 signature +
3991
+ * the wallet address. MUST be called from a USER GESTURE (click/tap) or the
3992
+ * browser blocks the popup.
3993
+ *
3994
+ * On the user's FIRST signature the popup transparently enrolls a device passkey
3995
+ * (Face ID / Touch ID) and creates their wallet - no email code. Subsequent
3996
+ * signatures are just a passkey approval.
3997
+ */
3998
+ async function signSolanaMessageViaTurnkey(message) {
3999
+ if (!hasDOM$3()) {
4000
+ throw new Error('Turnkey embedded-wallet signing is only available in the browser (popup-based).');
4001
+ }
4002
+ const { url, origin } = await turnkeySignerPage();
4003
+ const requestId = `tksign_${Date.now()}_${++requestSeq}`;
4004
+ const messageHex = toHex(message);
4005
+ return await deliver(url, origin, {
4006
+ request: { type: 'bounded:turnkey:sign', requestId, messageHex },
4007
+ onResult: (d) => {
4008
+ if (d.ok && typeof d.signatureHex === 'string' && typeof d.address === 'string') {
4009
+ return { done: true, value: { signature: fromHex(d.signatureHex), address: d.address, subOrgId: String(d.subOrgId || '') } };
4010
+ }
4011
+ return { error: String(d.error || 'Turnkey signing failed') };
4012
+ },
4013
+ });
4014
+ }
4015
+ function requireMatchingAddress(result) {
4016
+ const expected = activeTurnkeyAddress();
4017
+ if (!expected)
4018
+ throw new Error('This session has no Turnkey wallet. Sign in again to provision one.');
4019
+ if (result.address !== expected)
4020
+ throw new Error('Turnkey signer returned a different wallet address than the active session.');
4021
+ return expected;
4022
+ }
4023
+ async function signMessageWithTurnkey(message) {
4024
+ const result = await signSolanaMessageViaTurnkey(getPlatform().textEncode(message));
4025
+ requireMatchingAddress(result);
4026
+ return bs58.encode(result.signature);
4027
+ }
4028
+ async function signTransactionWithTurnkey(transaction) {
4029
+ const isLegacy = 'serializeMessage' in transaction;
4030
+ const message = isLegacy
4031
+ ? transaction.serializeMessage()
4032
+ : transaction.message.serialize();
4033
+ const result = await signSolanaMessageViaTurnkey(new Uint8Array(message));
4034
+ const address = requireMatchingAddress(result);
4035
+ const { PublicKey } = await import('@solana/web3.js');
4036
+ if (isLegacy) {
4037
+ transaction.addSignature(new PublicKey(address), bufferExports.Buffer.from(result.signature));
4038
+ }
4039
+ else {
4040
+ transaction.addSignature(new PublicKey(address), result.signature);
4041
+ }
4042
+ return transaction;
4043
+ }
4044
+ async function signAndSubmitTransactionWithTurnkey(transaction, feePayer) {
4045
+ var _a;
4046
+ const config = await getConfig();
4047
+ const rpcUrl = (_a = config.rpcUrl) === null || _a === void 0 ? void 0 : _a.trim();
4048
+ if (!rpcUrl)
4049
+ throw new Error('Turnkey transaction submission requires init({ rpcUrl }).');
4050
+ const { Connection, PublicKey } = await import('@solana/web3.js');
4051
+ const connection = new Connection(rpcUrl, 'confirmed');
4052
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
4053
+ const isLegacy = 'serializeMessage' in transaction;
4054
+ if (isLegacy) {
4055
+ const legacy = transaction;
4056
+ legacy.recentBlockhash = blockhash;
4057
+ legacy.lastValidBlockHeight = lastValidBlockHeight;
4058
+ if (!legacy.feePayer) {
4059
+ const address = activeTurnkeyAddress();
4060
+ if (!address)
4061
+ throw new Error('This session has no Turnkey wallet. Sign in again to provision one.');
4062
+ legacy.feePayer = feePayer !== null && feePayer !== void 0 ? feePayer : new PublicKey(address);
4063
+ }
4064
+ }
4065
+ else {
4066
+ transaction.message.recentBlockhash = blockhash;
4067
+ }
4068
+ const signed = await signTransactionWithTurnkey(transaction);
4069
+ const signature = await connection.sendRawTransaction(signed.serialize(), {
4070
+ skipPreflight: false,
4071
+ maxRetries: 3,
4072
+ });
4073
+ const confirmation = await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, 'confirmed');
4074
+ if (confirmation.value.err) {
4075
+ throw new Error(`Turnkey transaction failed: ${JSON.stringify(confirmation.value.err)}`);
4076
+ }
4077
+ return signature;
4078
+ }
4079
+
4080
+ // Same message the email provider uses when a hosted (OIDC) login has no
4081
+ // embedded wallet — enable `auth.wallets` in policy to get one.
4082
+ const OIDC_NO_WALLET = 'This session has no Turnkey wallet. Sign in again to provision the default ' +
4083
+ 'embedded wallet, or check whether the app explicitly sets auth.wallets:false.';
4084
+ const PKCE_STORE_KEY = 'bounded.oidc.pkce';
4085
+ const STORED_AUTH_METHOD_KEY$1 = 'bounded_last_auth_method';
4086
+ const ISSUER_SESSION_KIND_KEY$1 = 'bounded_issuer_session_kind';
4087
+ // Active session store (Web on web, RN on React Native) — selected by config,
4088
+ // not by `typeof window` (truthy on RN). OIDC redirect/popup is web-only, but the
4089
+ // session it mints is read back through this same selector everywhere.
4090
+ const sessionManager = getActiveSessionManager;
4091
+ function hasDOM$2() {
4092
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
4093
+ }
4094
+ function decodeJwt(token) {
4095
+ try {
4096
+ let b64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
4097
+ const pad = b64.length % 4;
4098
+ if (pad === 2)
4099
+ b64 += '==';
4100
+ else if (pad === 3)
4101
+ b64 += '=';
4102
+ return JSON.parse(getPlatform().atob(b64));
4103
+ }
4104
+ catch (_a) {
4105
+ return {};
4106
+ }
4107
+ }
4108
+ // A minimal AuthProvider so getSession()/logout() work for OIDC sessions, mirroring email.
4109
+ // Turnkey supplies the default Solana wallet and supports raw message and transaction
4110
+ // signatures through the Bounded signer window.
4111
+ const oidcProviderSingleton = {
4112
+ async login() {
4113
+ const idToken = sessionManager().getIdToken();
4114
+ return idToken ? userFromIdToken(idToken) : null;
4115
+ },
4116
+ async getUser() {
4117
+ const idToken = sessionManager().getIdToken();
4118
+ return idToken ? userFromIdToken(idToken) : null;
3724
4119
  },
3725
4120
  async logout() { await sessionManager().clearSession(); },
3726
- async signMessage(_message) {
3727
- throw new Error(hasEmbeddedWallet() ? EMBEDDED_WALLET_UNSUPPORTED : OIDC_NO_WALLET);
4121
+ async signMessage(message) {
4122
+ if (!hasTurnkeyWallet())
4123
+ throw new Error(OIDC_NO_WALLET);
4124
+ return signMessageWithTurnkey(message);
3728
4125
  },
3729
- async signTransaction(_t) {
3730
- throw new Error(hasEmbeddedWallet() ? EMBEDDED_WALLET_UNSUPPORTED : OIDC_NO_WALLET);
4126
+ async signTransaction(transaction) {
4127
+ if (!hasTurnkeyWallet())
4128
+ throw new Error(OIDC_NO_WALLET);
4129
+ return signTransactionWithTurnkey(transaction);
3731
4130
  },
3732
- async runTransaction(evm, sol, options) {
4131
+ async runTransaction(evm, sol, _options) {
3733
4132
  if (evm && sol)
3734
4133
  throw new Error('A transaction cannot target EVM and Solana at the same time.');
3735
- if (!evm)
4134
+ if (!hasTurnkeyWallet())
3736
4135
  throw new Error(OIDC_NO_WALLET);
3737
- if ((options === null || options === void 0 ? void 0 : options.shouldSubmitTx) === false)
3738
- throw new Error('Embedded EVM wallets sign and submit atomically; shouldSubmitTx:false is unsupported.');
3739
- if (!hasEmbeddedEvmWallet())
3740
- throw new Error('This login has no embedded EVM wallet. Enable auth.wallets on an app with an EVM collection and sign in again.');
3741
- return embeddedRunEvmTransaction(evm);
4136
+ if (evm)
4137
+ throw new Error('Turnkey embedded wallets currently support Solana only. Connect an EVM wallet for EVM transactions.');
4138
+ throw new Error('This Solana transaction shape is not supported by the Turnkey provider. Use a pre-built transaction.');
3742
4139
  },
3743
- async signAndSubmitTransaction(transaction, _feePayer) {
3744
- if (!hasEmbeddedWallet())
4140
+ async signAndSubmitTransaction(transaction, feePayer) {
4141
+ if (!hasTurnkeyWallet())
3745
4142
  throw new Error(OIDC_NO_WALLET);
3746
- return embeddedSignAndSubmit(transaction);
4143
+ return signAndSubmitTransactionWithTurnkey(transaction, feePayer);
3747
4144
  },
3748
4145
  };
3749
4146
  function userFromIdToken(idToken) {
3750
4147
  const c = decodeJwt(idToken);
3751
4148
  const id = c['custom:userId'] || c.sub || c['custom:walletAddress'] || '';
3752
4149
  return {
3753
- id, address: (c['custom:walletAddress'] || null),
3754
- evmAddress: typeof c['custom:evmWalletAddress'] === 'string' && /^0x[0-9a-fA-F]{40}$/.test(c['custom:evmWalletAddress'])
3755
- ? c['custom:evmWalletAddress'].toLowerCase()
3756
- : null,
4150
+ id,
4151
+ address: (c['custom:walletProvider'] === 'turnkey' && c['custom:walletAddress'] || null),
4152
+ evmAddress: null,
3757
4153
  email: (c.email || null), provider: oidcProviderSingleton,
3758
4154
  };
3759
4155
  }
@@ -3831,7 +4227,7 @@ async function buildAuthorize(opts) {
3831
4227
  throw new Error('appId is not configured (init the SDK with your appId)');
3832
4228
  // Web: default to the current page so callers don't have to pass redirectUri. Native has no
3833
4229
  // window — it MUST pass an https universal link (the system auth session needs a real URL).
3834
- const redirectUri = opts.redirectUri || (hasDOM$4() ? window.location.origin + window.location.pathname : undefined);
4230
+ const redirectUri = opts.redirectUri || (hasDOM$2() ? window.location.origin + window.location.pathname : undefined);
3835
4231
  if (!redirectUri)
3836
4232
  throw new Error('redirectUri is required on React Native — pass the https universal link the app owner registered.');
3837
4233
  const { verifier, challenge } = await makePkce();
@@ -3939,7 +4335,7 @@ async function runNativeLogin(opts) {
3939
4335
  async function loginWithRedirect(opts) {
3940
4336
  if (isNative())
3941
4337
  return runNativeLogin(opts);
3942
- if (!hasDOM$4())
4338
+ if (!hasDOM$2())
3943
4339
  throw new Error('loginWithRedirect requires a browser or a configured React Native platform');
3944
4340
  const { url, pkce } = await buildAuthorize(opts);
3945
4341
  // Stash the verifier + state so the redirect_uri page can finish the exchange. sessionStorage
@@ -3955,7 +4351,7 @@ async function loginWithRedirect(opts) {
3955
4351
  * loginWithRedirect() (which resolves with the User), so there is no separate redirect
3956
4352
  * page to finish from. Calling it on RN is harmless. */
3957
4353
  async function completeLoginFromRedirect() {
3958
- if (!hasDOM$4())
4354
+ if (!hasDOM$2())
3959
4355
  return null;
3960
4356
  const url = new URL(window.location.href);
3961
4357
  const code = url.searchParams.get('code');
@@ -4001,7 +4397,7 @@ async function completeLoginFromRedirect() {
4001
4397
  * redirect_uri page posts the code back. The redirect_uri page just calls the usual
4002
4398
  * completeLoginFromRedirect() (which auto-detects the popup) — no separate call needed. */
4003
4399
  async function loginWithPopup(opts) {
4004
- if (!hasDOM$4())
4400
+ if (!hasDOM$2())
4005
4401
  throw new Error('loginWithPopup is web-only — on React Native use loginWithRedirect()');
4006
4402
  // Open synchronously, before PKCE/config awaits consume the click's user-
4007
4403
  // activation window. Opening only after buildAuthorize() intermittently
@@ -4141,7 +4537,7 @@ async function loginWithPopup(opts) {
4141
4537
  /** Call this on the redirect_uri page when login was opened via loginWithPopup(): posts the
4142
4538
  * code back to the opener and closes. (For the redirect variant use completeLoginFromRedirect.) */
4143
4539
  function completeLoginInPopup(openerOrigin) {
4144
- if (!hasDOM$4() || !window.opener)
4540
+ if (!hasDOM$2() || !window.opener)
4145
4541
  return;
4146
4542
  const url = new URL(window.location.href);
4147
4543
  const code = url.searchParams.get('code');
@@ -4477,14 +4873,13 @@ function getInitConfig() {
4477
4873
  return initConfig;
4478
4874
  }
4479
4875
  /**
4480
- * Which LOGIN mode the app runs (default "bounded"). "turnkey" opts the app into
4481
- * Turnkey-native email OTP: the unified widget runs the code entry INLINE (no OIDC
4482
- * popup) via startTurnkeyEmailLogin(), and Bounded mints the session from Turnkey's
4483
- * verification. The value comes from init() config (`authMode`); a `openBoundedWidget`
4484
- * option can override it per call. Anything other than "turnkey" is "bounded".
4876
+ * Which LOGIN mode the app runs (default "turnkey"). Turnkey-native email OTP keeps
4877
+ * code entry inline in the unified widget (no OIDC popup), and Bounded mints the
4878
+ * session from Turnkey's verification. Apps can explicitly select "bounded" for the
4879
+ * legacy Better Auth email flow. A per-call openBoundedWidget option can override it.
4485
4880
  */
4486
4881
  function resolveAuthMode(config) {
4487
- return config.authMode === 'turnkey' ? 'turnkey' : 'bounded';
4882
+ return config.authMode === 'bounded' ? 'bounded' : 'turnkey';
4488
4883
  }
4489
4884
  /**
4490
4885
  * Resolve the injected-wallet-config the provider should use, merging the
@@ -4545,7 +4940,7 @@ async function getAuthProvider(config) {
4545
4940
  // manager as legacy email sessions. Human login should be started
4546
4941
  // with loginWithRedirect(); this provider is kept as the restore and
4547
4942
  // logout surface for those sessions.
4548
- currentAuthProvider = new (await import('./email-auth-provider-B3XaJw-G.mjs')).EmailAuthProvider();
4943
+ currentAuthProvider = new (await import('./email-auth-provider-v9xvY0Ky.mjs')).EmailAuthProvider();
4549
4944
  break;
4550
4945
  case "wallet":
4551
4946
  case "phantom": {
@@ -4564,7 +4959,7 @@ async function getAuthProvider(config) {
4564
4959
  if (!isWalletLoginEnabled(config)) {
4565
4960
  throw walletLoginNotEnabledError(authMethod);
4566
4961
  }
4567
- const { InjectedWalletProvider } = await import('./injected-wallet-provider-DfJvN3sV.mjs');
4962
+ const { InjectedWalletProvider } = await import('./injected-wallet-provider-YHhhFzsj.mjs');
4568
4963
  currentAuthProvider = new InjectedWalletProvider(resolveInjectedWalletConfig(config), rpcUrl, configuredNetwork);
4569
4964
  break;
4570
4965
  }
@@ -4577,7 +4972,7 @@ async function getAuthProvider(config) {
4577
4972
  // Selecting authMethod:'evm-wallet' IS the opt-in — pass evmWalletConfig
4578
4973
  // to point at a specific wallet or set chainId/domain.
4579
4974
  const evmWalletConfig = ((_a = config.evmWalletConfig) !== null && _a !== void 0 ? _a : {});
4580
- const { InjectedEvmWalletProvider } = await import('./injected-evm-wallet-provider-DSFDC8fh.mjs');
4975
+ const { InjectedEvmWalletProvider } = await import('./injected-evm-wallet-provider-DlPsOmik.mjs');
4581
4976
  currentAuthProvider = new InjectedEvmWalletProvider(evmWalletConfig);
4582
4977
  break;
4583
4978
  }
@@ -4605,7 +5000,7 @@ async function getAuthProvider(config) {
4605
5000
  break;
4606
5001
  case "guest":
4607
5002
  // Anonymous auth: a device-local ed25519 keypair signs the challenge.
4608
- currentAuthProvider = new (await import('./guest-auth-provider-BBSFawAr.mjs')).GuestAuthProvider();
5003
+ currentAuthProvider = new (await import('./guest-auth-provider-BLRbjAv2.mjs')).GuestAuthProvider();
4609
5004
  break;
4610
5005
  }
4611
5006
  if (!currentAuthProvider) {
@@ -4661,7 +5056,7 @@ async function signInAnonymously() {
4661
5056
  // Lazy-load the guest provider so its ed25519/web3 dependencies stay out of
4662
5057
  // the main bundle for data-only apps. Specifier is a static string literal
4663
5058
  // so bundle-splitting can see it.
4664
- const provider = new (await import('./guest-auth-provider-BBSFawAr.mjs')).GuestAuthProvider();
5059
+ const provider = new (await import('./guest-auth-provider-BLRbjAv2.mjs')).GuestAuthProvider();
4665
5060
  setAuthLoading(true); // reflect "signing in…" while the guest keypair session is minted
4666
5061
  const commit = (user) => {
4667
5062
  // No await/interleaving inside this block: provider/method/config/marker
@@ -4812,7 +5207,7 @@ async function loginWithWallet(options) {
4812
5207
  ? Object.assign(Object.assign({}, base), { getProvider: options.getProvider }) : base;
4813
5208
  const rpcUrl = normalizeSolanaRpcUrl(cfg.rpcUrl);
4814
5209
  const network = getConfiguredSolanaNetwork(cfg);
4815
- const { InjectedWalletProvider } = await import('./injected-wallet-provider-DfJvN3sV.mjs');
5210
+ const { InjectedWalletProvider } = await import('./injected-wallet-provider-YHhhFzsj.mjs');
4816
5211
  let provider = new InjectedWalletProvider(injectedConfig, rpcUrl, network);
4817
5212
  if (coreConfig.chain === 'offchain')
4818
5213
  provider = new OffchainAuthProvider(provider);
@@ -5212,276 +5607,23 @@ function setAuthProviderInstance(provider) {
5212
5607
  authProviderInstance = provider;
5213
5608
  }
5214
5609
 
5215
- // bounded-widget-modal.ts - the shared in-app WIDGET shell for Bounded's login
5216
- // and signing surfaces. One Shadow-DOM overlay + panel that matches the Bounded
5217
- // widget (Seal) look and MOTION: a backdrop fade, a panel rise on desktop, a
5218
- // bottom-sheet on mobile, a height tween as content swaps, and a clean close.
5219
- //
5220
- // It hosts EITHER:
5221
- // - NATIVE content (the login card the SDK renders itself), or
5222
- // - an IFRAME (the Bounded-owned signer page, which must run on the single
5223
- // signer origin because a WebAuthn passkey is bound to one origin).
5224
- //
5225
- // The modal is a warm singleton: it stays mounted (hidden) between uses so a
5226
- // second open - or a signature right after login - is instant and, when it is
5227
- // already open, a clean in-place view swap rather than a reopen.
5228
- function hasDOM$3() {
5229
- return typeof window !== "undefined" && typeof document !== "undefined";
5230
- }
5231
- const OVERLAY_CSS = `
5232
- :host{all:initial}
5233
- *{box-sizing:border-box}
5234
- .bw-ov{position:fixed;inset:0;z-index:2147483647;display:none;
5235
- font-family:"Schibsted Grotesk","Helvetica Neue",Arial,sans-serif}
5236
- .bw-ov.mounted{display:block}
5237
- .bw-back{position:fixed;inset:0;background:rgba(4,4,5,.62);opacity:0;
5238
- transition:opacity .24s ease;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}
5239
- .bw-wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:20px}
5240
- .bw-panel{position:relative;width:420px;max-width:calc(100vw - 28px);height:280px;
5241
- border-radius:14px;overflow:hidden;background:#0e0f11;border:1px solid rgba(232,230,222,.08);
5242
- box-shadow:0 1px 0 rgba(232,230,222,.03) inset,0 40px 90px -45px rgba(0,0,0,.85);
5243
- opacity:0;transform:translateY(14px) scale(.98);will-change:transform,opacity,height;
5244
- transition:opacity .28s cubic-bezier(.2,.8,.2,1),transform .28s cubic-bezier(.2,.8,.2,1),
5245
- height .3s cubic-bezier(.2,.8,.2,1)}
5246
- .bw-ov.show .bw-back{opacity:1}
5247
- .bw-ov.show .bw-panel{opacity:1;transform:none}
5248
- .bw-ov.closing .bw-back{opacity:0}
5249
- .bw-ov.closing .bw-panel{opacity:0;transform:translateY(12px) scale(.99)}
5250
- .bw-content{position:absolute;inset:0;overflow:hidden}
5251
- .bw-frame{display:block;width:100%;height:100%;border:0;background:transparent;color-scheme:dark}
5252
- /* editorial corner crop-marks, matching the reference frame */
5253
- .bw-mk{position:absolute;width:13px;height:13px;pointer-events:none;z-index:2;
5254
- border-color:rgba(232,230,222,.22);border-style:solid;border-width:0}
5255
- .bw-mk.tl{top:11px;left:11px;border-top-width:1px;border-left-width:1px}
5256
- .bw-mk.tr{top:11px;right:11px;border-top-width:1px;border-right-width:1px}
5257
- .bw-mk.bl{bottom:11px;left:11px;border-bottom-width:1px;border-left-width:1px}
5258
- .bw-mk.br{bottom:11px;right:11px;border-bottom-width:1px;border-right-width:1px}
5259
- @media(max-width:560px){
5260
- .bw-wrap{align-items:flex-end;padding:0}
5261
- .bw-panel{width:100%;max-width:100%;border-radius:18px 18px 0 0;border-bottom:0}
5262
- .bw-content{overflow-y:auto}
5263
- .bw-ov.show .bw-panel{transform:none}
5264
- .bw-ov:not(.show) .bw-panel,.bw-ov.closing .bw-panel{transform:translateY(18%)}
5265
- }
5266
- @media(prefers-reduced-motion:reduce){.bw-back,.bw-panel{transition:opacity .12s linear!important}}
5267
- `;
5268
- let modal = null;
5269
- function getModal() {
5270
- if (modal)
5271
- return modal;
5272
- const root = document.createElement("div");
5273
- root.setAttribute("data-bounded-widget", "");
5274
- const shadow = root.attachShadow({ mode: "open" });
5275
- const style = document.createElement("style");
5276
- style.textContent = OVERLAY_CSS;
5277
- shadow.appendChild(style);
5278
- const overlay = document.createElement("div");
5279
- overlay.className = "bw-ov";
5280
- const back = document.createElement("div");
5281
- back.className = "bw-back";
5282
- const wrap = document.createElement("div");
5283
- wrap.className = "bw-wrap";
5284
- const panel = document.createElement("div");
5285
- panel.className = "bw-panel";
5286
- const content = document.createElement("div");
5287
- content.className = "bw-content";
5288
- panel.appendChild(content);
5289
- for (const corner of ["tl", "tr", "bl", "br"]) {
5290
- const mk = document.createElement("span");
5291
- mk.className = "bw-mk " + corner;
5292
- panel.appendChild(mk);
5293
- }
5294
- wrap.appendChild(panel);
5295
- overlay.appendChild(back);
5296
- overlay.appendChild(wrap);
5297
- shadow.appendChild(overlay);
5298
- (document.body || document.documentElement).appendChild(root);
5299
- const m = {
5300
- root,
5301
- overlay,
5302
- panel,
5303
- content,
5304
- iframe: null,
5305
- iframeOrigin: "",
5306
- iframeReady: false,
5307
- onBackdrop: null,
5308
- onIframeMessage: null,
5309
- chain: Promise.resolve(),
5310
- fitObserver: null,
5311
- };
5312
- // The dismiss handler lives on `.bw-wrap`: it is the full-viewport flex layer
5313
- // that sits ABOVE `.bw-back`, so it (not the backdrop element) receives clicks
5314
- // outside the panel. Fire only when the click lands on the wrap itself, never
5315
- // when it bubbles up from the panel or its content.
5316
- wrap.addEventListener("click", (event) => {
5317
- if (event.target === wrap && m.onBackdrop)
5318
- m.onBackdrop();
5319
- });
5320
- window.addEventListener("message", (event) => {
5321
- if (!m.iframe)
5322
- return;
5323
- if (event.origin !== m.iframeOrigin)
5324
- return;
5325
- if (event.source !== m.iframe.contentWindow)
5326
- return;
5327
- const d = event.data;
5328
- if (!d || typeof d !== "object")
5329
- return;
5330
- if (m.onIframeMessage)
5331
- m.onIframeMessage(d);
5332
- });
5333
- modal = m;
5334
- return m;
5335
- }
5336
- function showModal(m) {
5337
- m.overlay.classList.remove("closing");
5338
- m.overlay.classList.add("mounted");
5339
- // Force a reflow so the .show transition runs from the hidden state every open.
5340
- void m.panel.offsetWidth;
5341
- m.overlay.classList.add("show");
5342
- }
5343
- function hideModal(m, after) {
5344
- m.overlay.classList.remove("show");
5345
- m.overlay.classList.add("closing");
5346
- window.setTimeout(() => {
5347
- m.overlay.classList.remove("mounted", "closing");
5348
- }, 260);
5349
- }
5350
- function setPanelHeight(m, px) {
5351
- const h = Math.max(140, Math.min(680, Math.round(px)));
5352
- m.panel.style.height = h + "px";
5353
- }
5354
- // True outer height of the mounted card: the max of its padding-box scroll height
5355
- // (catches overflowing descendants) and its border-box height plus own vertical
5356
- // margins (catches border + margins, which scrollHeight omits). Ceiled so a
5357
- // sub-pixel row is never clipped under `overflow:hidden`.
5358
- function measureOuterHeight(target) {
5359
- var _a;
5360
- const cs = (_a = target.ownerDocument.defaultView) === null || _a === void 0 ? void 0 : _a.getComputedStyle(target);
5361
- const marginY = cs ? parseFloat(cs.marginTop) + parseFloat(cs.marginBottom) : 0;
5362
- const borderBox = target.offsetHeight + (Number.isFinite(marginY) ? marginY : 0);
5363
- const rect = target.getBoundingClientRect().height;
5364
- return Math.ceil(Math.max(target.scrollHeight, borderBox, rect));
5365
- }
5366
- /**
5367
- * Measure the currently mounted native content and tween the panel to fit it.
5368
- * Re-fits automatically when the card's intrinsic height changes - notably after
5369
- * the Newsreader/Schibsted web fonts swap in (which grows line boxes a few px
5370
- * after first paint) and on inline view swaps (email -> OTP code -> success).
5371
- */
5372
- function fitPanelToContent(m) {
5373
- var _a, _b;
5374
- const first = m.content.firstElementChild;
5375
- const target = first || m.content;
5376
- const apply = () => {
5377
- // Only fit while this exact node is still the mounted card.
5378
- if (m.content.firstElementChild !== first && first)
5379
- return;
5380
- const h = measureOuterHeight(target);
5381
- if (h > 0)
5382
- setPanelHeight(m, h);
5383
- };
5384
- apply();
5385
- const view = target.ownerDocument.defaultView;
5386
- // Re-fit after fonts load (one-shot) and once the ResizeObserver-less browsers
5387
- // have painted, so the height never lags a late reflow.
5388
- (_a = view === null || view === void 0 ? void 0 : view.requestAnimationFrame) === null || _a === void 0 ? void 0 : _a.call(view, apply);
5389
- const fonts = target.ownerDocument.fonts;
5390
- if (fonts === null || fonts === void 0 ? void 0 : fonts.ready)
5391
- fonts.ready.then(apply).catch(() => { });
5392
- (_b = m.fitObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
5393
- m.fitObserver = null;
5394
- if (first && typeof (view === null || view === void 0 ? void 0 : view.ResizeObserver) === "function") {
5395
- const ro = new view.ResizeObserver(() => apply());
5396
- ro.observe(first);
5397
- m.fitObserver = ro;
5398
- }
5399
- }
5400
- /** Switch the modal into NATIVE-content mode: detach any iframe, mount `node`. */
5401
- function mountContent(m, node) {
5402
- var _a;
5403
- detachIframe(m);
5404
- (_a = m.fitObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
5405
- m.fitObserver = null;
5406
- m.content.replaceChildren(node);
5407
- m.content.style.display = "block";
5408
- }
5409
- /** Get (or lazily create) the warm iframe for signer/hosted-page mode. */
5410
- function ensureIframe(m, url, origin) {
5411
- var _a;
5412
- if (m.iframe && m.iframeOrigin === origin && m.iframe.src === url)
5413
- return m.iframe;
5414
- detachIframe(m);
5415
- (_a = m.fitObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
5416
- m.fitObserver = null;
5417
- m.content.style.display = "none";
5418
- const iframe = document.createElement("iframe");
5419
- iframe.className = "bw-frame";
5420
- iframe.setAttribute("title", "Bounded");
5421
- // Grant the framed origin WebAuthn get + create so passkey enroll/sign run
5422
- // inside the frame from the user's click (transient activation stays in-frame).
5423
- iframe.setAttribute("allow", "publickey-credentials-get; publickey-credentials-create; clipboard-write");
5424
- iframe.src = url;
5425
- m.iframeOrigin = origin;
5426
- m.iframeReady = false;
5427
- m.panel.appendChild(iframe);
5428
- m.iframe = iframe;
5429
- return iframe;
5430
- }
5431
- function detachIframe(m) {
5432
- if (m.iframe) {
5433
- try {
5434
- m.iframe.remove();
5435
- }
5436
- catch (_a) {
5437
- /* ignore */
5438
- }
5439
- }
5440
- m.iframe = null;
5441
- m.iframeReady = false;
5442
- m.iframeOrigin = "";
5443
- m.onIframeMessage = null;
5444
- }
5445
- // Load the Bounded editorial typefaces (Newsreader display + Schibsted Grotesk body +
5446
- // JetBrains Mono) once at the document level so they apply inside the shadow root. A
5447
- // no-op if already present or off-DOM. Fonts loaded here match the hosted auth page.
5448
- 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";
5449
- function ensureFonts() {
5450
- if (!hasDOM$3())
5451
- return;
5452
- if (document.querySelector('link[data-bounded-fonts="1"]'))
5453
- return;
5454
- const pre1 = document.createElement("link");
5455
- pre1.rel = "preconnect";
5456
- pre1.href = "https://fonts.googleapis.com";
5457
- const pre2 = document.createElement("link");
5458
- pre2.rel = "preconnect";
5459
- pre2.href = "https://fonts.gstatic.com";
5460
- pre2.crossOrigin = "anonymous";
5461
- const link = document.createElement("link");
5462
- link.rel = "stylesheet";
5463
- link.href = FONTS_HREF;
5464
- link.setAttribute("data-bounded-fonts", "1");
5465
- document.head.append(pre1, pre2, link);
5466
- }
5467
-
5468
- var __classPrivateFieldGet = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
5469
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
5470
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
5471
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5472
- };
5473
- var __classPrivateFieldSet = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
5474
- if (kind === "m") throw new TypeError("Private method is not writable");
5475
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5476
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5477
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
5478
- };
5479
- var _AppReadyEvent_detail;
5480
- let wallets = undefined;
5481
- const registeredWalletsSet = new Set();
5482
- function addRegisteredWallet(wallet) {
5483
- cachedWalletsArray = undefined;
5484
- registeredWalletsSet.add(wallet);
5610
+ var __classPrivateFieldGet = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
5611
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
5612
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
5613
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5614
+ };
5615
+ var __classPrivateFieldSet = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
5616
+ if (kind === "m") throw new TypeError("Private method is not writable");
5617
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5618
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5619
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
5620
+ };
5621
+ var _AppReadyEvent_detail;
5622
+ let wallets = undefined;
5623
+ const registeredWalletsSet = new Set();
5624
+ function addRegisteredWallet(wallet) {
5625
+ cachedWalletsArray = undefined;
5626
+ registeredWalletsSet.add(wallet);
5485
5627
  }
5486
5628
  function removeRegisteredWallet(wallet) {
5487
5629
  cachedWalletsArray = undefined;
@@ -5753,7 +5895,7 @@ function discoverStandardWallets() {
5753
5895
  return out;
5754
5896
  }
5755
5897
 
5756
- function hasDOM$2() {
5898
+ function hasDOM$1() {
5757
5899
  return typeof window !== 'undefined' && typeof document !== 'undefined';
5758
5900
  }
5759
5901
  async function signerOrigin() {
@@ -5830,7 +5972,7 @@ function sealOtpInIframe(code, otpEncryptionTargetBundle) {
5830
5972
  * step needs the signer iframe); on React Native use loginWithRedirect() instead.
5831
5973
  */
5832
5974
  async function startTurnkeyEmailLogin(email) {
5833
- if (!hasDOM$2()) {
5975
+ if (!hasDOM$1()) {
5834
5976
  throw new Error('Turnkey-native email login is web-only - on React Native use loginWithRedirect()');
5835
5977
  }
5836
5978
  const trimmed = email.trim();
@@ -6019,7 +6161,7 @@ function el(tag, cls, text) {
6019
6161
  */
6020
6162
  async function openBoundedWidget(opts = {}) {
6021
6163
  var _a, _b, _c, _d, _e, _f, _g, _h;
6022
- if (!hasDOM$3()) {
6164
+ if (!hasDOM$4()) {
6023
6165
  throw new Error("openBoundedWidget is web-only - on React Native use loginWithRedirect()");
6024
6166
  }
6025
6167
  // Imported lazily to avoid a static cycle with the auth index barrel.
@@ -6392,1014 +6534,718 @@ function useAuth() {
6392
6534
  // Only log errors that aren't user-initiated cancellations
6393
6535
  if (error !== 'exited_auth_flow' && (error === null || error === void 0 ? void 0 : error.message) !== 'exited_auth_flow') {
6394
6536
  console.error('Error logging in:', error);
6395
- }
6396
- setUser(null);
6397
- }
6398
- finally {
6399
- setLoading(false);
6400
- }
6401
- };
6402
- const logout$1 = async () => {
6403
- try {
6404
- setLoading(true);
6405
- await logout();
6406
- setUser(null);
6407
- }
6408
- catch (error) {
6409
- console.error('Error logging out:', error);
6410
- }
6411
- finally {
6412
- setLoading(false);
6413
- }
6414
- };
6415
- return {
6416
- login: login$1,
6417
- logout: logout$1,
6418
- loading: loading || sdkLoading,
6419
- user,
6420
- };
6421
- }
6422
-
6423
- var _a$1;
6424
- // SSR guard (mirrors useAuth): no hooks/subscriptions during Node server render.
6425
- // React Native has no `window` but DOES support hooks, so detect Node SSR
6426
- // specifically via process.versions.node.
6427
- const isSSR$1 = typeof window === 'undefined'
6428
- && typeof process !== 'undefined'
6429
- && !!((_a$1 = process.versions) === null || _a$1 === void 0 ? void 0 : _a$1.node);
6430
- function useQuery(path, options) {
6431
- if (isSSR$1) {
6432
- return { data: undefined, loading: true, error: null };
6433
- }
6434
- const [data, setData] = React.useState(undefined);
6435
- const [loading, setLoading] = React.useState(true);
6436
- const [error, setError] = React.useState(null);
6437
- // Re-subscribe only when the query actually changes (not on every render).
6438
- const optionsKey = options ? JSON.stringify(options) : '';
6439
- React.useEffect(() => {
6440
- if (!path) {
6441
- setData(undefined);
6442
- setLoading(false);
6443
- setError(null);
6444
- return;
6445
- }
6446
- let active = true;
6447
- let unsubscribe;
6448
- setLoading(true);
6449
- setError(null);
6450
- subscribe(path, Object.assign(Object.assign({}, options), { onData: (d) => {
6451
- if (!active)
6452
- return;
6453
- // A reconnect can legitimately surface a transport error before the
6454
- // socket re-authenticates and sends a new authorized snapshot. That
6455
- // fresh snapshot is the recovery boundary: keep the error visible
6456
- // until data arrives, then clear it so React does not remain stuck on
6457
- // a failure page after realtime has recovered.
6458
- setError(null);
6459
- setData(d);
6460
- setLoading(false);
6461
- }, onError: (e) => {
6462
- if (!active)
6463
- return;
6464
- setError(e instanceof Error ? e : new Error(String(e)));
6465
- setLoading(false);
6466
- } }))
6467
- .then((unsub) => {
6468
- if (active)
6469
- unsubscribe = unsub;
6470
- else
6471
- void unsub(); // unmounted before subscribe resolved
6472
- })
6473
- .catch((e) => {
6474
- if (!active)
6475
- return;
6476
- setError(e instanceof Error ? e : new Error(String(e)));
6477
- setLoading(false);
6478
- });
6479
- return () => {
6480
- active = false;
6481
- void (unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe());
6482
- };
6483
- // optionsKey captures the option object's contents; path is the other input.
6484
- // eslint-disable-next-line react-hooks/exhaustive-deps
6485
- }, [path, optionsKey]);
6486
- return { data, loading, error };
6487
- }
6488
-
6489
- // Wrapper for getIdToken - passes isServer=false for client-side usage
6490
- async function getIdToken() {
6491
- return getIdToken$1(false);
6492
- }
6493
-
6494
- var __rest = (undefined && undefined.__rest) || function (s, e) {
6495
- var t = {};
6496
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
6497
- t[p] = s[p];
6498
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
6499
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
6500
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
6501
- t[p[i]] = s[p[i]];
6502
- }
6503
- return t;
6504
- };
6505
- /** A structured non-2xx response from the developer API. */
6506
- class BuildsApiError extends Error {
6507
- constructor(init) {
6508
- super(init.message);
6509
- this.name = 'BuildsApiError';
6510
- this.code = init.code;
6511
- this.retryable = init.retryable;
6512
- this.status = init.status;
6513
- this.details = init.details;
6514
- this.retryAfterMs = init.retryAfterMs;
6515
- }
6516
- }
6517
- const memoryIdempotencyKeys = new Map();
6518
- const IDEMPOTENCY_STORAGE_PREFIX = 'bounded:build:idempotency:';
6519
- function retryAfterMs(response) {
6520
- var _a, _b;
6521
- const value = (_b = (_a = response.headers) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.call(_a, 'Retry-After');
6522
- if (!value)
6523
- return undefined;
6524
- const seconds = Number(value);
6525
- if (Number.isFinite(seconds) && seconds >= 0)
6526
- return Math.ceil(seconds * 1000);
6527
- const at = Date.parse(value);
6528
- if (!Number.isFinite(at))
6529
- return undefined;
6530
- return Math.max(0, at - Date.now());
6531
- }
6532
- async function responseBody(response) {
6533
- const text = await response.text();
6534
- if (!text)
6535
- return {};
6536
- try {
6537
- return JSON.parse(text);
6538
- }
6539
- catch (_a) {
6540
- return { raw: text };
6541
- }
6542
- }
6543
- function apiError(response, body) {
6544
- const envelope = body;
6545
- const nested = (envelope === null || envelope === void 0 ? void 0 : envelope.error) && typeof envelope.error === 'object'
6546
- ? envelope.error
6547
- : undefined;
6548
- const hasNestedDetails = !!nested && Object.prototype.hasOwnProperty.call(nested, 'details');
6549
- const hasTopLevelDetails = !!body
6550
- && typeof body === 'object'
6551
- && Object.prototype.hasOwnProperty.call(body, 'details');
6552
- const legacyCode = typeof (body === null || body === void 0 ? void 0 : body.error) === 'string' ? body.error : undefined;
6553
- const code = typeof (nested === null || nested === void 0 ? void 0 : nested.code) === 'string'
6554
- ? nested.code
6555
- : legacyCode !== null && legacyCode !== void 0 ? legacyCode : `http_${response.status}`;
6556
- const message = typeof (nested === null || nested === void 0 ? void 0 : nested.message) === 'string'
6557
- ? nested.message
6558
- : typeof (body === null || body === void 0 ? void 0 : body.message) === 'string'
6559
- ? body.message
6560
- : `Developer API request failed with HTTP ${response.status}`;
6561
- const retryable = typeof (nested === null || nested === void 0 ? void 0 : nested.retryable) === 'boolean'
6562
- ? nested.retryable
6563
- : response.status === 429 || response.status >= 500;
6564
- return new BuildsApiError(Object.assign(Object.assign({ code,
6565
- message,
6566
- retryable, status: response.status }, (hasNestedDetails
6567
- ? { details: nested === null || nested === void 0 ? void 0 : nested.details }
6568
- : hasTopLevelDetails
6569
- ? { details: body.details }
6570
- : {})), (response.status === 429 ? { retryAfterMs: retryAfterMs(response) } : {})));
6571
- }
6572
- /** @internal Shared authenticated developer-api transport for builds and apps. */
6573
- async function requestDevApi(path, request = {}) {
6574
- var _a, _b;
6575
- const config = await getConfig();
6576
- const base = (_a = config.devApiUrl) === null || _a === void 0 ? void 0 : _a.replace(/\/$/, '');
6577
- if (!base) {
6578
- throw new Error('Bounded developer API is not configured. Pass devApiUrl or a Bounded network to init().');
6579
- }
6580
- // Reuse the exact accessor exposed publicly as getIdToken().
6581
- const token = await getIdToken();
6582
- if (!token) {
6583
- throw new BuildsApiError({
6584
- code: 'auth_required',
6585
- message: 'Sign in before calling the Bounded developer API.',
6586
- retryable: false,
6587
- status: 401,
6588
- });
6589
- }
6590
- const hasBody = request.body !== undefined;
6591
- const response = await fetch(`${base}${path}`, Object.assign({ method: (_b = request.method) !== null && _b !== void 0 ? _b : 'GET', headers: Object.assign({ Authorization: `Bearer ${token}` }, (hasBody ? { 'Content-Type': 'application/json' } : {})) }, (hasBody ? { body: JSON.stringify(request.body) } : {})));
6592
- const body = await responseBody(response);
6593
- if (!response.ok)
6594
- throw apiError(response, body);
6595
- return body;
6596
- }
6597
- function intentStorageKey(intentKey) {
6598
- return `${IDEMPOTENCY_STORAGE_PREFIX}${intentKey}`;
6599
- }
6600
- function isValidIdempotencyKey(value) {
6601
- return typeof value === 'string' && value.length > 0 && value.length <= 256;
6602
- }
6603
- function readPersistedIdempotencyKey(intentKey) {
6604
- var _a;
6605
- const storageKey = intentStorageKey(intentKey);
6606
- try {
6607
- const stored = (_a = globalThis.sessionStorage) === null || _a === void 0 ? void 0 : _a.getItem(storageKey);
6608
- if (isValidIdempotencyKey(stored))
6609
- return stored;
6610
- }
6611
- catch (_b) {
6612
- // Storage can be disabled by browser privacy policy; use module memory.
6613
- }
6614
- const inMemory = memoryIdempotencyKeys.get(storageKey);
6615
- return isValidIdempotencyKey(inMemory) ? inMemory : undefined;
6616
- }
6617
- function persistIdempotencyKey(intentKey, idempotencyKey) {
6618
- var _a;
6619
- const storageKey = intentStorageKey(intentKey);
6620
- memoryIdempotencyKeys.set(storageKey, idempotencyKey);
6621
- try {
6622
- (_a = globalThis.sessionStorage) === null || _a === void 0 ? void 0 : _a.setItem(storageKey, idempotencyKey);
6623
- }
6624
- catch (_b) {
6625
- // The in-memory write above is the React Native / disabled-storage fallback.
6626
- }
6627
- }
6628
- function randomHex(bytes) {
6629
- const cryptoObject = globalThis.crypto;
6630
- if (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.getRandomValues) {
6631
- const values = new Uint8Array(bytes);
6632
- cryptoObject.getRandomValues(values);
6633
- return Array.from(values, (value) => value.toString(16).padStart(2, '0')).join('');
6634
- }
6635
- let value = '';
6636
- for (let index = 0; index < bytes; index += 1) {
6637
- value += Math.floor(Math.random() * 256).toString(16).padStart(2, '0');
6638
- }
6639
- return value;
6640
- }
6641
- function mintIdempotencyKey() {
6642
- const cryptoObject = globalThis.crypto;
6643
- if (typeof (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.randomUUID) === 'function') {
6644
- return `build_${cryptoObject.randomUUID()}`;
6645
- }
6646
- return `build_${Date.now().toString(36)}_${randomHex(16)}`;
6647
- }
6648
- function actionIdempotencyKey(options, action) {
6649
- const supplied = options.idempotencyKey;
6650
- if (supplied !== undefined) {
6651
- if (!isValidIdempotencyKey(supplied)) {
6652
- throw new TypeError('idempotencyKey must be between 1 and 256 characters.');
6653
- }
6654
- if (options.intentKey)
6655
- persistIdempotencyKey(options.intentKey, supplied);
6656
- return supplied;
6657
- }
6658
- if (!options.intentKey) {
6659
- throw new TypeError(`intentKey is required when ${action} auto-mints an idempotency key.`);
6660
- }
6661
- const existing = readPersistedIdempotencyKey(options.intentKey);
6662
- if (existing)
6663
- return existing;
6664
- const minted = mintIdempotencyKey();
6665
- // This is deliberately synchronous and happens before requestDevApi can fetch.
6666
- persistIdempotencyKey(options.intentKey, minted);
6667
- return minted;
6668
- }
6669
- function appPath(appId) {
6670
- return `/app/${encodeURIComponent(appId)}/builds`;
6671
- }
6672
- async function submit(appId, options) {
6673
- const idempotencyKey = actionIdempotencyKey(options, 'submit()');
6674
- const { intentKey: _intentKey, idempotencyKey: _suppliedKey } = options, submission = __rest(options, ["intentKey", "idempotencyKey"]);
6675
- return requestDevApi(appPath(appId), {
6676
- method: 'POST',
6677
- body: Object.assign(Object.assign({}, submission), { idempotencyKey }),
6678
- });
6679
- }
6680
- async function get(appId, runId) {
6681
- return requestDevApi(`${appPath(appId)}/${encodeURIComponent(runId)}`);
6682
- }
6683
- async function events(appId, runId, after = 0) {
6684
- const cursor = Number.isFinite(after) ? Math.max(0, Math.floor(after)) : 0;
6685
- return requestDevApi(`${appPath(appId)}/${encodeURIComponent(runId)}/events?after=${cursor}`);
6686
- }
6687
- async function cancel(appId, runId) {
6688
- return requestDevApi(`${appPath(appId)}/${encodeURIComponent(runId)}/cancel`, { method: 'POST' });
6689
- }
6690
- async function decideGate(appId, runId, gateId, options) {
6691
- const idempotencyKey = actionIdempotencyKey(options, 'decideGate()');
6692
- const { intentKey: _intentKey, idempotencyKey: _suppliedKey } = options, decision = __rest(options, ["intentKey", "idempotencyKey"]);
6693
- return requestDevApi(`${appPath(appId)}/${encodeURIComponent(runId)}/gates/${encodeURIComponent(gateId)}/decision`, { method: 'POST', body: Object.assign(Object.assign({}, decision), { idempotencyKey }) });
6694
- }
6695
- async function refreshReviewCapability(appId, runId, gateId, options) {
6696
- const idempotencyKey = actionIdempotencyKey(options, 'refreshReviewCapability()');
6697
- return requestDevApi(`${appPath(appId)}/${encodeURIComponent(runId)}/gates/${encodeURIComponent(gateId)}/review-capability/refresh`, {
6698
- method: 'POST',
6699
- body: {
6700
- expectedGeneration: options.expectedGeneration,
6701
- expectedExpiresAtMs: options.expectedExpiresAtMs,
6702
- expectedRecoveryUntilMs: options.expectedRecoveryUntilMs,
6703
- expectedAudience: options.expectedAudience,
6704
- expectedReviewerBindingHash: options.expectedReviewerBindingHash,
6705
- idempotencyKey,
6706
- },
6707
- });
6708
- }
6709
- /**
6710
- * Read one exact delegated-review capability and its narrow lifecycle result.
6711
- * This does not refresh or extend the window and exposes no generic run-view
6712
- * authority.
6713
- */
6714
- async function getReviewCapability(appId, runId, gateId, options) {
6715
- return requestDevApi(`${appPath(appId)}/${encodeURIComponent(runId)}/gates/${encodeURIComponent(gateId)}/review-capability`, {
6716
- method: 'POST',
6717
- body: { expectedPreviewUrl: options.expectedPreviewUrl },
6718
- });
6719
- }
6720
- async function topup(appId, runId, options) {
6721
- return requestDevApi(`${appPath(appId)}/${encodeURIComponent(runId)}/topup`, { method: 'POST', body: options });
6722
- }
6723
- async function list$1(appId, limit = 20) {
6724
- const requestedLimit = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 20;
6725
- return requestDevApi(`${appPath(appId)}?limit=${requestedLimit}`);
6726
- }
6727
-
6728
- // Source of truth for these strings:
6729
- // packages/cdk/cloudflare/bounded-build/src/types.ts. Keep them string-pinned;
6730
- // wire:1 remains additive and callers must tolerate unknown response strings.
6731
- const NON_TERMINAL_STATES = [
6732
- 'queued',
6733
- 'admitted',
6734
- 'executing',
6735
- 'preview_ready',
6736
- 'parked',
6737
- 'resuming',
6738
- 'promoting',
6739
- 'quarantined',
6740
- ];
6741
- const TERMINAL_STATES = [
6742
- 'promoted',
6743
- 'failed',
6744
- 'canceled',
6745
- 'rejected',
6746
- 'expired',
6747
- 'rebase_required',
6748
- 'reported',
6749
- ];
6750
- const RUN_STATES = [
6751
- ...NON_TERMINAL_STATES,
6752
- ...TERMINAL_STATES,
6753
- ];
6754
- const PARK_REASONS = ['gate', 'funding'];
6755
- const FAILURE_REASONS = [
6756
- 'admission_rejected',
6757
- 'dispatch_timeout',
6758
- 'auth_revoked',
6759
- 'owner_changed',
6760
- 'policy_changed',
6761
- 'quarantine_forced',
6762
- 'bind_failed',
6763
- 'internal_error',
6764
- ];
6765
- const BUILD_REVIEW_CAPABILITY_SCHEMA = 'bounded.build-review-capability.v1';
6766
- const BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA = 'bounded.build-review-capability-refresh.v1';
6767
- const BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA = 'bounded.build-delegated-review-capability.v1';
6768
-
6769
- const FAST_POLL_MS = 1000;
6770
- const NORMAL_POLL_MS = 2000;
6771
- const PARKED_POLL_MS = 5000;
6772
- const LONG_RUNNING_POLL_MS = 10000;
6773
- const THIRTY_SECONDS_MS = 30000;
6774
- const TWO_MINUTES_MS = 2 * 60000;
6775
- const TEN_MINUTES_MS = 10 * 60000;
6776
- const terminalStates = new Set(TERMINAL_STATES);
6777
- function pollCadence(state, elapsedMs) {
6778
- if (elapsedMs >= TEN_MINUTES_MS)
6779
- return LONG_RUNNING_POLL_MS;
6780
- if ((state === null || state === void 0 ? void 0 : state.startsWith('parked')) || elapsedMs >= TWO_MINUTES_MS)
6781
- return PARKED_POLL_MS;
6782
- // The first 30s is where the user is staring at "waiting for a build slot" /
6783
- // "building" and every transition matters most; poll fast, then relax.
6784
- if (elapsedMs < THIRTY_SECONDS_MS)
6785
- return FAST_POLL_MS;
6786
- return NORMAL_POLL_MS;
6787
- }
6788
- function retryCadence(error, state, elapsedMs) {
6789
- const cadence = pollCadence(state, elapsedMs);
6790
- if (error instanceof BuildsApiError && error.status === 429 && error.retryAfterMs !== undefined) {
6791
- return Math.max(cadence, error.retryAfterMs);
6792
- }
6793
- return cadence;
6794
- }
6795
- function hasRetryAfter(error) {
6796
- return error instanceof BuildsApiError
6797
- && error.status === 429
6798
- && error.retryAfterMs !== undefined;
6799
- }
6800
- // A permanent API refusal (expired session, revoked access, unknown run) will
6801
- // never heal by re-polling; the loop must stop and surface it instead of
6802
- // retrying forever. 429 stays retryable regardless of the server envelope.
6803
- function isPermanentError(error) {
6804
- return error instanceof BuildsApiError
6805
- && error.status !== 429
6806
- && error.retryable === false;
6807
- }
6808
- function newEvents(rows, lastSeq, seen) {
6809
- const cursorBeforePoll = lastSeq;
6810
- const fresh = [];
6811
- let nextCursor = lastSeq;
6812
- for (const event of rows) {
6813
- if (!Number.isFinite(event === null || event === void 0 ? void 0 : event.seq))
6814
- continue;
6815
- nextCursor = Math.max(nextCursor, event.seq);
6816
- if (event.seq <= cursorBeforePoll || seen.has(event.seq))
6817
- continue;
6818
- seen.add(event.seq);
6819
- fresh.push(event);
6820
- }
6821
- return { events: fresh, lastSeq: nextCursor };
6822
- }
6823
- /**
6824
- * Poll a build view and its event cursor until stopped or the run reaches a
6825
- * known terminal state. Unknown state strings remain untouched and nonterminal.
6826
- */
6827
- function watch(appId, runId, onUpdate, options = {}) {
6828
- const startedAt = Date.now();
6829
- const seenSeqs = new Set();
6830
- let lastSeq = Number.isFinite(options.after)
6831
- ? Math.max(0, Math.floor(options.after))
6832
- : 0;
6833
- let timer;
6834
- let active = true;
6835
- let lastState;
6836
- const stop = () => {
6837
- active = false;
6838
- if (timer !== undefined) {
6839
- clearTimeout(timer);
6840
- timer = undefined;
6841
- }
6842
- };
6843
- const schedule = (delayMs) => {
6844
- if (!active)
6845
- return;
6846
- timer = setTimeout(() => {
6847
- timer = undefined;
6848
- void poll();
6849
- }, delayMs);
6850
- };
6851
- const reportError = (error) => {
6852
- var _a;
6853
- try {
6854
- (_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, error);
6855
- }
6856
- catch (_b) {
6857
- // A consumer error callback must not break the transport loop.
6858
- }
6859
- };
6860
- const publish = (run, events) => {
6861
- try {
6862
- onUpdate({ run, events });
6863
- }
6864
- catch (_a) {
6865
- // Polling is transport state; consumer render errors must not strand it.
6866
- }
6867
- };
6868
- const poll = async () => {
6869
- if (!active)
6870
- return;
6871
- // Fire the view and the events page concurrently: the events cursor (lastSeq)
6872
- // does not depend on the run view, so there is no reason to pay two serial
6873
- // round-trips per tick. Error semantics below are unchanged — the run result
6874
- // still gates, and a failed events page still publishes the run with no rows.
6875
- const runPromise = get(appId, runId);
6876
- const eventsPromise = events(appId, runId, lastSeq);
6877
- // Keep the in-flight events page from becoming an unhandled rejection if the
6878
- // run result makes us bail before awaiting it; the real await below still sees it.
6879
- eventsPromise.catch(() => { });
6880
- let run;
6881
- try {
6882
- const view = await runPromise;
6883
- if (!active)
6884
- return;
6885
- run = view.run;
6886
- lastState = run.state;
6887
- }
6888
- catch (error) {
6889
- if (!active)
6890
- return;
6891
- reportError(error);
6892
- if (isPermanentError(error)) {
6893
- stop();
6894
- return;
6895
- }
6896
- schedule(retryCadence(error, lastState, Date.now() - startedAt));
6897
- return;
6898
- }
6899
- try {
6900
- const response = await eventsPromise;
6901
- if (!active)
6902
- return;
6903
- const deduped = newEvents(Array.isArray(response.events) ? response.events : [], lastSeq, seenSeqs);
6904
- lastSeq = deduped.lastSeq;
6905
- publish(run, deduped.events);
6906
- }
6907
- catch (error) {
6908
- if (!active)
6909
- return;
6910
- // The view still advances UI state even when event replay is rate-limited.
6911
- publish(run, []);
6912
- reportError(error);
6913
- if (terminalStates.has(run.state)) {
6914
- // The run itself is done, but a rate-limited final event replay still
6915
- // owes the caller its unseen seq rows. Honor Retry-After, retry the
6916
- // piggyback once the limiter permits it, then stop after a successful
6917
- // replay. Other terminal replay failures stop normally.
6918
- if (hasRetryAfter(error)) {
6919
- schedule(retryCadence(error, run.state, Date.now() - startedAt));
6920
- return;
6921
- }
6922
- stop();
6923
- return;
6924
- }
6925
- if (isPermanentError(error)) {
6926
- stop();
6927
- return;
6928
- }
6929
- schedule(retryCadence(error, run.state, Date.now() - startedAt));
6930
- return;
6931
- }
6932
- if (!active)
6933
- return;
6934
- if (terminalStates.has(run.state)) {
6935
- // Drain the remaining event pages before stopping: a resumed terminal
6936
- // watch may owe the caller more rows than one page returns. Each page
6937
- // advances the cursor, so the loop terminates; the cap is a backstop.
6938
- for (let page = 0; page < 50 && active; page += 1) {
6939
- let drained;
6940
- try {
6941
- const response = await events(appId, runId, lastSeq);
6942
- if (!active)
6943
- return;
6944
- drained = newEvents(Array.isArray(response.events) ? response.events : [], lastSeq, seenSeqs);
6945
- }
6946
- catch (error) {
6947
- reportError(error);
6948
- break;
6949
- }
6950
- if (drained.events.length === 0)
6951
- break;
6952
- lastSeq = drained.lastSeq;
6953
- publish(run, drained.events);
6954
- }
6955
- stop();
6956
- return;
6537
+ }
6538
+ setUser(null);
6539
+ }
6540
+ finally {
6541
+ setLoading(false);
6957
6542
  }
6958
- schedule(pollCadence(run.state, Date.now() - startedAt));
6959
6543
  };
6960
- void poll();
6961
- return { stop };
6544
+ const logout$1 = async () => {
6545
+ try {
6546
+ setLoading(true);
6547
+ await logout();
6548
+ setUser(null);
6549
+ }
6550
+ catch (error) {
6551
+ console.error('Error logging out:', error);
6552
+ }
6553
+ finally {
6554
+ setLoading(false);
6555
+ }
6556
+ };
6557
+ return {
6558
+ login: login$1,
6559
+ logout: logout$1,
6560
+ loading: loading || sdkLoading,
6561
+ user,
6562
+ };
6962
6563
  }
6963
6564
 
6964
- var _a;
6565
+ var _a$1;
6965
6566
  // SSR guard (mirrors useAuth): no hooks/subscriptions during Node server render.
6966
6567
  // React Native has no `window` but DOES support hooks, so detect Node SSR
6967
6568
  // specifically via process.versions.node.
6968
- const isSSR = typeof window === 'undefined'
6569
+ const isSSR$1 = typeof window === 'undefined'
6969
6570
  && typeof process !== 'undefined'
6970
- && !!((_a = process.versions) === null || _a === void 0 ? void 0 : _a.node);
6971
- function useBuildRun(appId, runId, options) {
6972
- if (isSSR) {
6973
- return { run: null, events: [], loading: true, error: null };
6571
+ && !!((_a$1 = process.versions) === null || _a$1 === void 0 ? void 0 : _a$1.node);
6572
+ function useQuery(path, options) {
6573
+ if (isSSR$1) {
6574
+ return { data: undefined, loading: true, error: null };
6974
6575
  }
6975
- const [run, setRun] = React.useState(null);
6976
- const [events, setEvents] = React.useState([]);
6576
+ const [data, setData] = React.useState(undefined);
6977
6577
  const [loading, setLoading] = React.useState(true);
6978
6578
  const [error, setError] = React.useState(null);
6579
+ // Re-subscribe only when the query actually changes (not on every render).
6979
6580
  const optionsKey = options ? JSON.stringify(options) : '';
6980
6581
  React.useEffect(() => {
6981
- setRun(null);
6982
- setEvents([]);
6983
- setError(null);
6984
- if (!appId || !runId) {
6582
+ if (!path) {
6583
+ setData(undefined);
6985
6584
  setLoading(false);
6585
+ setError(null);
6986
6586
  return;
6987
6587
  }
6988
6588
  let active = true;
6589
+ let unsubscribe;
6989
6590
  setLoading(true);
6990
- const handle = watch(appId, runId, ({ run: nextRun, events: nextEvents }) => {
6991
- if (!active)
6992
- return;
6993
- setRun(nextRun);
6994
- if (nextEvents.length > 0) {
6995
- setEvents((current) => {
6996
- const seen = new Set(current.map((event) => event.seq));
6997
- return [...current, ...nextEvents.filter((event) => !seen.has(event.seq))];
6998
- });
6999
- }
7000
- setError(null);
7001
- setLoading(false);
7002
- }, Object.assign(Object.assign({}, options), { onError: (value) => {
6591
+ setError(null);
6592
+ subscribe(path, Object.assign(Object.assign({}, options), { onData: (d) => {
7003
6593
  if (!active)
7004
6594
  return;
7005
- setError(value instanceof Error ? value : new Error(String(value)));
6595
+ // A reconnect can legitimately surface a transport error before the
6596
+ // socket re-authenticates and sends a new authorized snapshot. That
6597
+ // fresh snapshot is the recovery boundary: keep the error visible
6598
+ // until data arrives, then clear it so React does not remain stuck on
6599
+ // a failure page after realtime has recovered.
6600
+ setError(null);
6601
+ setData(d);
7006
6602
  setLoading(false);
7007
- } }));
6603
+ }, onError: (e) => {
6604
+ if (!active)
6605
+ return;
6606
+ setError(e instanceof Error ? e : new Error(String(e)));
6607
+ setLoading(false);
6608
+ } }))
6609
+ .then((unsub) => {
6610
+ if (active)
6611
+ unsubscribe = unsub;
6612
+ else
6613
+ void unsub(); // unmounted before subscribe resolved
6614
+ })
6615
+ .catch((e) => {
6616
+ if (!active)
6617
+ return;
6618
+ setError(e instanceof Error ? e : new Error(String(e)));
6619
+ setLoading(false);
6620
+ });
7008
6621
  return () => {
7009
6622
  active = false;
7010
- handle.stop();
6623
+ void (unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe());
7011
6624
  };
7012
- // optionsKey captures the option object's contents; ids are the other inputs.
6625
+ // optionsKey captures the option object's contents; path is the other input.
7013
6626
  // eslint-disable-next-line react-hooks/exhaustive-deps
7014
- }, [appId, runId, optionsKey]);
7015
- return { run, events, loading, error };
6627
+ }, [path, optionsKey]);
6628
+ return { data, loading, error };
6629
+ }
6630
+
6631
+ // Wrapper for getIdToken - passes isServer=false for client-side usage
6632
+ async function getIdToken() {
6633
+ return getIdToken$1(false);
6634
+ }
6635
+
6636
+ var __rest = (undefined && undefined.__rest) || function (s, e) {
6637
+ var t = {};
6638
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
6639
+ t[p] = s[p];
6640
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6641
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
6642
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
6643
+ t[p[i]] = s[p[i]];
6644
+ }
6645
+ return t;
6646
+ };
6647
+ /** A structured non-2xx response from the developer API. */
6648
+ class BuildsApiError extends Error {
6649
+ constructor(init) {
6650
+ super(init.message);
6651
+ this.name = 'BuildsApiError';
6652
+ this.code = init.code;
6653
+ this.retryable = init.retryable;
6654
+ this.status = init.status;
6655
+ this.details = init.details;
6656
+ this.retryAfterMs = init.retryAfterMs;
6657
+ }
6658
+ }
6659
+ const memoryIdempotencyKeys = new Map();
6660
+ const IDEMPOTENCY_STORAGE_PREFIX = 'bounded:build:idempotency:';
6661
+ function retryAfterMs(response) {
6662
+ var _a, _b;
6663
+ const value = (_b = (_a = response.headers) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.call(_a, 'Retry-After');
6664
+ if (!value)
6665
+ return undefined;
6666
+ const seconds = Number(value);
6667
+ if (Number.isFinite(seconds) && seconds >= 0)
6668
+ return Math.ceil(seconds * 1000);
6669
+ const at = Date.parse(value);
6670
+ if (!Number.isFinite(at))
6671
+ return undefined;
6672
+ return Math.max(0, at - Date.now());
6673
+ }
6674
+ async function responseBody(response) {
6675
+ const text = await response.text();
6676
+ if (!text)
6677
+ return {};
6678
+ try {
6679
+ return JSON.parse(text);
6680
+ }
6681
+ catch (_a) {
6682
+ return { raw: text };
6683
+ }
6684
+ }
6685
+ function apiError(response, body) {
6686
+ const envelope = body;
6687
+ const nested = (envelope === null || envelope === void 0 ? void 0 : envelope.error) && typeof envelope.error === 'object'
6688
+ ? envelope.error
6689
+ : undefined;
6690
+ const hasNestedDetails = !!nested && Object.prototype.hasOwnProperty.call(nested, 'details');
6691
+ const hasTopLevelDetails = !!body
6692
+ && typeof body === 'object'
6693
+ && Object.prototype.hasOwnProperty.call(body, 'details');
6694
+ const legacyCode = typeof (body === null || body === void 0 ? void 0 : body.error) === 'string' ? body.error : undefined;
6695
+ const code = typeof (nested === null || nested === void 0 ? void 0 : nested.code) === 'string'
6696
+ ? nested.code
6697
+ : legacyCode !== null && legacyCode !== void 0 ? legacyCode : `http_${response.status}`;
6698
+ const message = typeof (nested === null || nested === void 0 ? void 0 : nested.message) === 'string'
6699
+ ? nested.message
6700
+ : typeof (body === null || body === void 0 ? void 0 : body.message) === 'string'
6701
+ ? body.message
6702
+ : `Developer API request failed with HTTP ${response.status}`;
6703
+ const retryable = typeof (nested === null || nested === void 0 ? void 0 : nested.retryable) === 'boolean'
6704
+ ? nested.retryable
6705
+ : response.status === 429 || response.status >= 500;
6706
+ return new BuildsApiError(Object.assign(Object.assign({ code,
6707
+ message,
6708
+ retryable, status: response.status }, (hasNestedDetails
6709
+ ? { details: nested === null || nested === void 0 ? void 0 : nested.details }
6710
+ : hasTopLevelDetails
6711
+ ? { details: body.details }
6712
+ : {})), (response.status === 429 ? { retryAfterMs: retryAfterMs(response) } : {})));
6713
+ }
6714
+ /** @internal Shared authenticated developer-api transport for builds and apps. */
6715
+ async function requestDevApi(path, request = {}) {
6716
+ var _a, _b;
6717
+ const config = await getConfig();
6718
+ const base = (_a = config.devApiUrl) === null || _a === void 0 ? void 0 : _a.replace(/\/$/, '');
6719
+ if (!base) {
6720
+ throw new Error('Bounded developer API is not configured. Pass devApiUrl or a Bounded network to init().');
6721
+ }
6722
+ // Reuse the exact accessor exposed publicly as getIdToken().
6723
+ const token = await getIdToken();
6724
+ if (!token) {
6725
+ throw new BuildsApiError({
6726
+ code: 'auth_required',
6727
+ message: 'Sign in before calling the Bounded developer API.',
6728
+ retryable: false,
6729
+ status: 401,
6730
+ });
6731
+ }
6732
+ const hasBody = request.body !== undefined;
6733
+ const response = await fetch(`${base}${path}`, Object.assign({ method: (_b = request.method) !== null && _b !== void 0 ? _b : 'GET', headers: Object.assign({ Authorization: `Bearer ${token}` }, (hasBody ? { 'Content-Type': 'application/json' } : {})) }, (hasBody ? { body: JSON.stringify(request.body) } : {})));
6734
+ const body = await responseBody(response);
6735
+ if (!response.ok)
6736
+ throw apiError(response, body);
6737
+ return body;
6738
+ }
6739
+ function intentStorageKey(intentKey) {
6740
+ return `${IDEMPOTENCY_STORAGE_PREFIX}${intentKey}`;
6741
+ }
6742
+ function isValidIdempotencyKey(value) {
6743
+ return typeof value === 'string' && value.length > 0 && value.length <= 256;
6744
+ }
6745
+ function readPersistedIdempotencyKey(intentKey) {
6746
+ var _a;
6747
+ const storageKey = intentStorageKey(intentKey);
6748
+ try {
6749
+ const stored = (_a = globalThis.sessionStorage) === null || _a === void 0 ? void 0 : _a.getItem(storageKey);
6750
+ if (isValidIdempotencyKey(stored))
6751
+ return stored;
6752
+ }
6753
+ catch (_b) {
6754
+ // Storage can be disabled by browser privacy policy; use module memory.
6755
+ }
6756
+ const inMemory = memoryIdempotencyKeys.get(storageKey);
6757
+ return isValidIdempotencyKey(inMemory) ? inMemory : undefined;
6758
+ }
6759
+ function persistIdempotencyKey(intentKey, idempotencyKey) {
6760
+ var _a;
6761
+ const storageKey = intentStorageKey(intentKey);
6762
+ memoryIdempotencyKeys.set(storageKey, idempotencyKey);
6763
+ try {
6764
+ (_a = globalThis.sessionStorage) === null || _a === void 0 ? void 0 : _a.setItem(storageKey, idempotencyKey);
6765
+ }
6766
+ catch (_b) {
6767
+ // The in-memory write above is the React Native / disabled-storage fallback.
6768
+ }
6769
+ }
6770
+ function randomHex(bytes) {
6771
+ const cryptoObject = globalThis.crypto;
6772
+ if (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.getRandomValues) {
6773
+ const values = new Uint8Array(bytes);
6774
+ cryptoObject.getRandomValues(values);
6775
+ return Array.from(values, (value) => value.toString(16).padStart(2, '0')).join('');
6776
+ }
6777
+ let value = '';
6778
+ for (let index = 0; index < bytes; index += 1) {
6779
+ value += Math.floor(Math.random() * 256).toString(16).padStart(2, '0');
6780
+ }
6781
+ return value;
6782
+ }
6783
+ function mintIdempotencyKey() {
6784
+ const cryptoObject = globalThis.crypto;
6785
+ if (typeof (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.randomUUID) === 'function') {
6786
+ return `build_${cryptoObject.randomUUID()}`;
6787
+ }
6788
+ return `build_${Date.now().toString(36)}_${randomHex(16)}`;
7016
6789
  }
7017
-
7018
- /**
7019
- * Platform first-party surfaces only (bounded.sh, oapps.fun, localhost), with
7020
- * one exact Drafted exception for delegated review/decision routes. Other
7021
- * generated-app origins remain blocked; server-side callers are unaffected.
7022
- */
7023
- const builds = {
7024
- submit,
7025
- get,
7026
- getReviewCapability,
7027
- events,
7028
- cancel,
7029
- decideGate,
7030
- refreshReviewCapability,
7031
- topup,
7032
- list: list$1,
7033
- watch,
7034
- };
7035
-
7036
- async function create(options = {}) {
7037
- const body = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (options.name !== undefined ? { name: options.name } : {})), (options.prompt !== undefined ? { prompt: options.prompt } : {})), (options.idempotencyKey !== undefined ? { idempotencyKey: options.idempotencyKey } : {})), (options.intent !== undefined ? { intent: options.intent } : {})), (options.oapp === true ? { oapp: true } : {}));
7038
- const app = await requestDevApi('/apps/new', {
6790
+ function actionIdempotencyKey(options, action) {
6791
+ const supplied = options.idempotencyKey;
6792
+ if (supplied !== undefined) {
6793
+ if (!isValidIdempotencyKey(supplied)) {
6794
+ throw new TypeError('idempotencyKey must be between 1 and 256 characters.');
6795
+ }
6796
+ if (options.intentKey)
6797
+ persistIdempotencyKey(options.intentKey, supplied);
6798
+ return supplied;
6799
+ }
6800
+ if (!options.intentKey) {
6801
+ throw new TypeError(`intentKey is required when ${action} auto-mints an idempotency key.`);
6802
+ }
6803
+ const existing = readPersistedIdempotencyKey(options.intentKey);
6804
+ if (existing)
6805
+ return existing;
6806
+ const minted = mintIdempotencyKey();
6807
+ // This is deliberately synchronous and happens before requestDevApi can fetch.
6808
+ persistIdempotencyKey(options.intentKey, minted);
6809
+ return minted;
6810
+ }
6811
+ function appPath(appId) {
6812
+ return `/app/${encodeURIComponent(appId)}/builds`;
6813
+ }
6814
+ async function submit(appId, options) {
6815
+ const idempotencyKey = actionIdempotencyKey(options, 'submit()');
6816
+ const { intentKey: _intentKey, idempotencyKey: _suppliedKey } = options, submission = __rest(options, ["intentKey", "idempotencyKey"]);
6817
+ return requestDevApi(appPath(appId), {
7039
6818
  method: 'POST',
7040
- body,
6819
+ body: Object.assign(Object.assign({}, submission), { idempotencyKey }),
7041
6820
  });
7042
- return Object.assign(Object.assign(Object.assign(Object.assign({ appId: app.appId }, (typeof app.slug === 'string' && app.slug ? { slug: app.slug } : {})), { url: app.url }), (typeof app.prompt === 'string' && app.prompt ? { prompt: app.prompt } : {})), (typeof app.handoff === 'string' && app.handoff ? { handoff: app.handoff } : {}));
7043
6821
  }
7044
- /**
7045
- * F-1200: build the create-flow redirect the owner is sent to after `create()`, with the
7046
- * prompt and handoff carried in the URL FRAGMENT (never the query - fragments never reach
7047
- * servers, logs, or Referer). Encoding is byte-exact so the widget's `URLSearchParams`
7048
- * parse + `.trim()` reproduces the SAME prompt string dev-api hashed into the handoff;
7049
- * getting this wrong would make the widget reject a genuine token and degrade to prefill.
7050
- * Callers (bounded.sh, oapps.fun) should use this instead of hand-building the URL.
7051
- *
7052
- * Returns `${url}#bounded_prompt=<prompt>` with no `&bounded_handoff=` when the server
7053
- * minted no handoff (HANDOFF_SECRET unset) - the widget then prefills without auto-running.
7054
- */
7055
- function buildCreateRedirectUrl(app) {
7056
- const base = app.url;
7057
- const prompt = typeof app.prompt === 'string' ? app.prompt.trim() : '';
7058
- if (!prompt)
7059
- return base;
7060
- const params = `bounded_prompt=${encodeURIComponent(prompt)}`
7061
- + (app.handoff ? `&bounded_handoff=${encodeURIComponent(app.handoff)}` : '');
7062
- return `${base}#${params}`;
6822
+ async function get(appId, runId) {
6823
+ return requestDevApi(`${appPath(appId)}/${encodeURIComponent(runId)}`);
7063
6824
  }
7064
- async function list() {
7065
- // This legacy-compatible action shape is also used by oapps.fun listMyApps.
7066
- const response = await requestDevApi('/', {
6825
+ async function events(appId, runId, after = 0) {
6826
+ const cursor = Number.isFinite(after) ? Math.max(0, Math.floor(after)) : 0;
6827
+ return requestDevApi(`${appPath(appId)}/${encodeURIComponent(runId)}/events?after=${cursor}`);
6828
+ }
6829
+ async function cancel(appId, runId) {
6830
+ return requestDevApi(`${appPath(appId)}/${encodeURIComponent(runId)}/cancel`, { method: 'POST' });
6831
+ }
6832
+ async function decideGate(appId, runId, gateId, options) {
6833
+ const idempotencyKey = actionIdempotencyKey(options, 'decideGate()');
6834
+ const { intentKey: _intentKey, idempotencyKey: _suppliedKey } = options, decision = __rest(options, ["intentKey", "idempotencyKey"]);
6835
+ return requestDevApi(`${appPath(appId)}/${encodeURIComponent(runId)}/gates/${encodeURIComponent(gateId)}/decision`, { method: 'POST', body: Object.assign(Object.assign({}, decision), { idempotencyKey }) });
6836
+ }
6837
+ async function refreshReviewCapability(appId, runId, gateId, options) {
6838
+ const idempotencyKey = actionIdempotencyKey(options, 'refreshReviewCapability()');
6839
+ return requestDevApi(`${appPath(appId)}/${encodeURIComponent(runId)}/gates/${encodeURIComponent(gateId)}/review-capability/refresh`, {
7067
6840
  method: 'POST',
7068
- body: { action: 'listApps' },
6841
+ body: {
6842
+ expectedGeneration: options.expectedGeneration,
6843
+ expectedExpiresAtMs: options.expectedExpiresAtMs,
6844
+ expectedRecoveryUntilMs: options.expectedRecoveryUntilMs,
6845
+ expectedAudience: options.expectedAudience,
6846
+ expectedReviewerBindingHash: options.expectedReviewerBindingHash,
6847
+ idempotencyKey,
6848
+ },
7069
6849
  });
7070
- const rows = Array.isArray(response.apps) ? response.apps : [];
7071
- return rows
7072
- .map((value) => {
7073
- var _a, _b, _c, _d;
7074
- const row = value && typeof value === 'object'
7075
- ? value
7076
- : {};
7077
- const appId = String((_c = (_b = (_a = row._id) !== null && _a !== void 0 ? _a : row.id) !== null && _b !== void 0 ? _b : row.appId) !== null && _c !== void 0 ? _c : '');
7078
- if (!/^[a-f0-9]{24}$/i.test(appId))
7079
- return null;
7080
- const name = String((_d = row.name) !== null && _d !== void 0 ? _d : '').trim() || '(unnamed app)';
7081
- const slug = typeof row.slug === 'string' && row.slug ? row.slug : undefined;
7082
- const url = typeof row.url === 'string' && row.url.trim() ? row.url.trim() : undefined;
7083
- const buildStatus = row.buildStatus === 'building' || row.buildStatus === 'build_failed'
7084
- ? row.buildStatus
7085
- : undefined;
7086
- return Object.assign(Object.assign(Object.assign({ appId,
7087
- name }, (slug ? { slug } : {})), (url ? { url } : {})), (buildStatus ? { buildStatus } : {}));
7088
- })
7089
- .filter((app) => app !== null);
7090
- }
7091
- /**
7092
- * Platform first-party surfaces only (bounded.sh, oapps.fun, localhost) —
7093
- * dev-api CORS blocks calls from generated-app origins. Server-side callers unaffected.
7094
- */
7095
- const apps = { create, list, buildCreateRedirectUrl };
7096
-
7097
- async function deserializeTransaction(tx) {
7098
- // Literal specifier so bundlers can code-split the lazy chunk.
7099
- const { Transaction, VersionedTransaction } = await import('@solana/web3.js');
7100
- const buf = Buffer.isBuffer(tx) ? tx : Buffer.from(tx, 'base64');
7101
- if ((buf[0] & 0x80) !== 0) {
7102
- return VersionedTransaction.deserialize(buf);
7103
- }
7104
- else {
7105
- return Transaction.from(buf);
7106
- }
7107
6850
  }
7108
-
7109
- // turnkey-wallet-widget.ts - runs one Turnkey signer exchange inside the shared
7110
- // in-app WIDGET (bounded-widget-modal.ts). The Bounded-owned signer page is loaded
7111
- // in a warm iframe behind the modal so signing feels native: an in-app card plus
7112
- // the OS Face ID / Touch ID sheet, never a jarring popup.
7113
- //
7114
- // WHY AN IFRAME: a WebAuthn passkey is bound to ONE origin, so passkey create/sign
7115
- // MUST run on the single Bounded signer origin. The iframe carries the
7116
- // publickey-credentials-{get,create} permission policy and the user's click lands
7117
- // INSIDE it, giving WebAuthn the transient activation it needs.
7118
- //
7119
- // SAFARI / restricted embedders: cross-origin iframe passkey CREATE is unreliable
7120
- // in Safari. The signer detects that and posts `fallbackPopup`; we then rerun the
7121
- // SAME request through `popupFallback` (a real window). Callers pass a popup
7122
- // implementation and never see the difference.
7123
6851
  /**
7124
- * Run one signer exchange through the in-app widget. Resolves when `onResult`
7125
- * accepts a result; falls back to `popupFallback` if the frame cannot run WebAuthn
7126
- * (Safari cross-origin create) or cannot be mounted.
6852
+ * Read one exact delegated-review capability and its narrow lifecycle result.
6853
+ * This does not refresh or extend the window and exposes no generic run-view
6854
+ * authority.
7127
6855
  */
7128
- async function runWidget(url, signerOrigin, ex, popupFallback) {
7129
- if (!hasDOM$3())
7130
- return popupFallback();
7131
- const m = getModal();
7132
- const requestId = String(ex.request.requestId);
7133
- // Serialize: WebAuthn is modal, and one warm frame handles one request at a time.
7134
- const run = m.chain.then(() => new Promise((resolve, reject) => {
7135
- let settled = false;
7136
- let usedFallback = false;
7137
- const iframe = ensureIframe(m, url, signerOrigin);
7138
- const send = () => {
7139
- if (!iframe.contentWindow)
7140
- return;
7141
- try {
7142
- iframe.contentWindow.postMessage(ex.request, signerOrigin);
7143
- }
7144
- catch (_a) {
7145
- /* frame gone */
7146
- }
7147
- };
7148
- let loadTimer = 0;
7149
- let ceilingTimer = 0;
7150
- const finish = (fn) => {
7151
- if (settled)
7152
- return;
7153
- settled = true;
7154
- m.onIframeMessage = null;
7155
- m.onBackdrop = null;
7156
- window.clearTimeout(loadTimer);
7157
- window.clearTimeout(ceilingTimer);
7158
- fn();
7159
- };
7160
- const goFallback = () => {
7161
- if (usedFallback || settled)
7162
- return;
7163
- usedFallback = true;
7164
- hideModal(m);
7165
- finish(() => popupFallback().then(resolve, reject));
7166
- };
7167
- m.onBackdrop = () => {
7168
- hideModal(m);
7169
- finish(() => reject(new Error("cancelled")));
7170
- };
7171
- m.onIframeMessage = (d) => {
7172
- if (d.type === "bounded:turnkey:ready") {
7173
- m.iframeReady = true;
7174
- send();
7175
- return;
7176
- }
7177
- if (d.type === "bounded:turnkey:resize" && typeof d.height === "number") {
7178
- setPanelHeight(m, d.height);
7179
- return;
7180
- }
7181
- if (d.type === "bounded:turnkey:fallbackPopup" && d.requestId === requestId) {
7182
- goFallback();
7183
- return;
7184
- }
7185
- if (d.type === "bounded:turnkey:result" && d.requestId === requestId) {
7186
- const outcome = ex.onResult(d);
7187
- if ("error" in outcome) {
7188
- hideModal(m);
7189
- finish(() => reject(new Error(outcome.error)));
7190
- return;
7191
- }
7192
- if (outcome.done) {
7193
- hideModal(m);
7194
- finish(() => resolve(outcome.value));
7195
- }
7196
- }
7197
- };
7198
- showModal(m);
7199
- if (m.iframeReady)
7200
- send();
7201
- // If the frame never loads, degrade to the popup (best-effort - a post-load
7202
- // popup may be blocked without a gesture, in which case the fallback rejects).
7203
- loadTimer = window.setTimeout(() => {
7204
- if (!m.iframeReady && !settled)
7205
- goFallback();
7206
- }, 8000);
7207
- // Absolute ceiling so a stuck flow cannot hang forever.
7208
- ceilingTimer = window.setTimeout(() => {
7209
- if (!settled) {
7210
- hideModal(m);
7211
- finish(() => reject(new Error("Wallet approval timed out.")));
7212
- }
7213
- }, 5 * 60 * 1000);
7214
- }));
7215
- m.chain = run.then(() => undefined, () => undefined);
7216
- return run;
6856
+ async function getReviewCapability(appId, runId, gateId, options) {
6857
+ return requestDevApi(`${appPath(appId)}/${encodeURIComponent(runId)}/gates/${encodeURIComponent(gateId)}/review-capability`, {
6858
+ method: 'POST',
6859
+ body: { expectedPreviewUrl: options.expectedPreviewUrl },
6860
+ });
7217
6861
  }
7218
-
7219
- // turnkey-signer-bridge.ts - routes NON-CUSTODIAL Turnkey embedded-wallet signing
7220
- // through the Bounded-owned signer page (auth.bounded.sh/wallet/turnkey/signer).
7221
- //
7222
- // TRANSPORT: by default the signer runs inside the in-app WIDGET (turnkey-wallet-
7223
- // widget.ts) - an iframe behind a Shadow-DOM modal that matches the Bounded widget
7224
- // look + motion, so signing feels native. Safari (unreliable at iframe passkey
7225
- // CREATE) and any environment where the frame cannot mount fall back to a real
7226
- // popup window; both transports speak the same postMessage protocol.
7227
- //
7228
- // Unlike the Crossmint bridge (crossmint-signer-bridge.ts), Turnkey wallets DO
7229
- // produce raw detached signatures: the signer runs the Turnkey browser SDK on the
7230
- // single Bounded origin, enrolls the user's passkey on first use (lazy - the
7231
- // sub-org is created with the passkey + email attached at that moment), and stamps
7232
- // an ed25519 signRawPayload with that passkey. So this bridge exposes a
7233
- // `signSolanaMessage` primitive; the app builds + submits the transaction itself.
7234
- //
7235
- // ONE ORIGIN, ONE PASSKEY: a WebAuthn passkey is bound to a single origin, so ALL
7236
- // Bounded apps (any subdomain / custom domain) sign through this one origin - giving
7237
- // every user one wallet + one passkey that roams across every app.
7238
- function hasDOM$1() {
7239
- return typeof window !== 'undefined' && typeof document !== 'undefined';
6862
+ async function topup(appId, runId, options) {
6863
+ return requestDevApi(`${appPath(appId)}/${encodeURIComponent(runId)}/topup`, { method: 'POST', body: options });
7240
6864
  }
7241
- // Safari (incl. iOS WebViews) is unreliable at running a WebAuthn CREATE inside a
7242
- // cross-origin iframe, so the widget's automatic in-iframe passkey enrollment can
7243
- // fail - and a post-failure popup would no longer have a user gesture to open with.
7244
- // There we pick the popup up front, from the original click, so enrollment is solid.
7245
- // Everywhere else the inline widget is used (buttery in-app modal).
7246
- function preferPopup() {
7247
- if (!hasDOM$1() || typeof navigator === 'undefined')
7248
- return true;
7249
- const ua = navigator.userAgent || '';
7250
- return /^((?!chrome|android|crios|fxios|edg|edga|opr).)*safari/i.test(ua);
6865
+ async function list$1(appId, limit = 20) {
6866
+ const requestedLimit = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 20;
6867
+ return requestDevApi(`${appPath(appId)}?limit=${requestedLimit}`);
7251
6868
  }
7252
- // Dispatch one signer exchange through the in-app WIDGET, degrading to the POPUP
7253
- // when we cannot embed (Safari enrollment, or the frame never loads).
7254
- async function deliver(url, origin, ex) {
7255
- const popup = () => runPopup(url, origin, ex);
7256
- if (preferPopup())
7257
- return popup();
7258
- return runWidget(url, origin, { request: ex.request, onResult: ex.onResult }, popup);
6869
+
6870
+ // Source of truth for these strings:
6871
+ // packages/cdk/cloudflare/bounded-build/src/types.ts. Keep them string-pinned;
6872
+ // wire:1 remains additive and callers must tolerate unknown response strings.
6873
+ const NON_TERMINAL_STATES = [
6874
+ 'queued',
6875
+ 'admitted',
6876
+ 'executing',
6877
+ 'preview_ready',
6878
+ 'parked',
6879
+ 'resuming',
6880
+ 'promoting',
6881
+ 'quarantined',
6882
+ ];
6883
+ const TERMINAL_STATES = [
6884
+ 'promoted',
6885
+ 'failed',
6886
+ 'canceled',
6887
+ 'rejected',
6888
+ 'expired',
6889
+ 'rebase_required',
6890
+ 'reported',
6891
+ ];
6892
+ const RUN_STATES = [
6893
+ ...NON_TERMINAL_STATES,
6894
+ ...TERMINAL_STATES,
6895
+ ];
6896
+ const PARK_REASONS = ['gate', 'funding'];
6897
+ const FAILURE_REASONS = [
6898
+ 'admission_rejected',
6899
+ 'dispatch_timeout',
6900
+ 'auth_revoked',
6901
+ 'owner_changed',
6902
+ 'policy_changed',
6903
+ 'quarantine_forced',
6904
+ 'bind_failed',
6905
+ 'internal_error',
6906
+ ];
6907
+ const BUILD_REVIEW_CAPABILITY_SCHEMA = 'bounded.build-review-capability.v1';
6908
+ const BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA = 'bounded.build-review-capability-refresh.v1';
6909
+ const BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA = 'bounded.build-delegated-review-capability.v1';
6910
+
6911
+ const FAST_POLL_MS = 1000;
6912
+ const NORMAL_POLL_MS = 2000;
6913
+ const PARKED_POLL_MS = 5000;
6914
+ const LONG_RUNNING_POLL_MS = 10000;
6915
+ const THIRTY_SECONDS_MS = 30000;
6916
+ const TWO_MINUTES_MS = 2 * 60000;
6917
+ const TEN_MINUTES_MS = 10 * 60000;
6918
+ const terminalStates = new Set(TERMINAL_STATES);
6919
+ function pollCadence(state, elapsedMs) {
6920
+ if (elapsedMs >= TEN_MINUTES_MS)
6921
+ return LONG_RUNNING_POLL_MS;
6922
+ if ((state === null || state === void 0 ? void 0 : state.startsWith('parked')) || elapsedMs >= TWO_MINUTES_MS)
6923
+ return PARKED_POLL_MS;
6924
+ // The first 30s is where the user is staring at "waiting for a build slot" /
6925
+ // "building" and every transition matters most; poll fast, then relax.
6926
+ if (elapsedMs < THIRTY_SECONDS_MS)
6927
+ return FAST_POLL_MS;
6928
+ return NORMAL_POLL_MS;
7259
6929
  }
7260
- async function turnkeySignerPage() {
7261
- const cfg = await getConfig();
7262
- const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
7263
- return { url: `${base}/wallet/turnkey/signer`, origin: new URL(base).origin };
6930
+ function retryCadence(error, state, elapsedMs) {
6931
+ const cadence = pollCadence(state, elapsedMs);
6932
+ if (error instanceof BuildsApiError && error.status === 429 && error.retryAfterMs !== undefined) {
6933
+ return Math.max(cadence, error.retryAfterMs);
6934
+ }
6935
+ return cadence;
7264
6936
  }
7265
- function toHex(bytes) {
7266
- let h = '';
7267
- for (const b of bytes)
7268
- h += b.toString(16).padStart(2, '0');
7269
- return h;
6937
+ function hasRetryAfter(error) {
6938
+ return error instanceof BuildsApiError
6939
+ && error.status === 429
6940
+ && error.retryAfterMs !== undefined;
7270
6941
  }
7271
- function fromHex(hex) {
7272
- const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
7273
- const out = new Uint8Array(clean.length / 2);
7274
- for (let i = 0; i < out.length; i++)
7275
- out[i] = parseInt(clean.substr(i * 2, 2), 16);
7276
- return out;
6942
+ // A permanent API refusal (expired session, revoked access, unknown run) will
6943
+ // never heal by re-polling; the loop must stop and surface it instead of
6944
+ // retrying forever. 429 stays retryable regardless of the server envelope.
6945
+ function isPermanentError(error) {
6946
+ return error instanceof BuildsApiError
6947
+ && error.status !== 429
6948
+ && error.retryable === false;
7277
6949
  }
7278
- let requestSeq = 0;
7279
- // Shared popup driver: open the signer page, do the ready/postMessage handshake, and
7280
- // resolve when `onResult` accepts a result message. Origin + source are pinned.
7281
- async function runPopup(url, signerOrigin, ex) {
7282
- var _a, _b;
7283
- const w = (_a = ex.width) !== null && _a !== void 0 ? _a : 420, h = (_b = ex.height) !== null && _b !== void 0 ? _b : 620;
7284
- const left = window.screenX + Math.max(0, (window.outerWidth - w) / 2);
7285
- const top = window.screenY + Math.max(0, (window.outerHeight - h) / 2);
7286
- const popup = window.open(url, 'bounded-turnkey-signer', `width=${w},height=${h},left=${left},top=${top}`);
7287
- if (!popup) {
7288
- throw new Error('Popup blocked - call this from a user gesture (e.g. a click handler) so the passkey approval window can open.');
6950
+ function newEvents(rows, lastSeq, seen) {
6951
+ const cursorBeforePoll = lastSeq;
6952
+ const fresh = [];
6953
+ let nextCursor = lastSeq;
6954
+ for (const event of rows) {
6955
+ if (!Number.isFinite(event === null || event === void 0 ? void 0 : event.seq))
6956
+ continue;
6957
+ nextCursor = Math.max(nextCursor, event.seq);
6958
+ if (event.seq <= cursorBeforePoll || seen.has(event.seq))
6959
+ continue;
6960
+ seen.add(event.seq);
6961
+ fresh.push(event);
7289
6962
  }
7290
- return await new Promise((resolve, reject) => {
7291
- let settled = false;
7292
- let sentRequest = false;
7293
- let closeTimer;
7294
- const cleanup = () => { settled = true; window.removeEventListener('message', onMsg); if (closeTimer)
7295
- clearInterval(closeTimer); };
7296
- const sendRequest = () => { if (sentRequest)
7297
- return; sentRequest = true; try {
7298
- popup.postMessage(ex.request, signerOrigin);
6963
+ return { events: fresh, lastSeq: nextCursor };
6964
+ }
6965
+ /**
6966
+ * Poll a build view and its event cursor until stopped or the run reaches a
6967
+ * known terminal state. Unknown state strings remain untouched and nonterminal.
6968
+ */
6969
+ function watch(appId, runId, onUpdate, options = {}) {
6970
+ const startedAt = Date.now();
6971
+ const seenSeqs = new Set();
6972
+ let lastSeq = Number.isFinite(options.after)
6973
+ ? Math.max(0, Math.floor(options.after))
6974
+ : 0;
6975
+ let timer;
6976
+ let active = true;
6977
+ let lastState;
6978
+ const stop = () => {
6979
+ active = false;
6980
+ if (timer !== undefined) {
6981
+ clearTimeout(timer);
6982
+ timer = undefined;
7299
6983
  }
7300
- catch ( /* popup gone */_a) { /* popup gone */ } };
7301
- const onMsg = (event) => {
7302
- if (event.origin !== signerOrigin)
6984
+ };
6985
+ const schedule = (delayMs) => {
6986
+ if (!active)
6987
+ return;
6988
+ timer = setTimeout(() => {
6989
+ timer = undefined;
6990
+ void poll();
6991
+ }, delayMs);
6992
+ };
6993
+ const reportError = (error) => {
6994
+ var _a;
6995
+ try {
6996
+ (_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, error);
6997
+ }
6998
+ catch (_b) {
6999
+ // A consumer error callback must not break the transport loop.
7000
+ }
7001
+ };
7002
+ const publish = (run, events) => {
7003
+ try {
7004
+ onUpdate({ run, events });
7005
+ }
7006
+ catch (_a) {
7007
+ // Polling is transport state; consumer render errors must not strand it.
7008
+ }
7009
+ };
7010
+ const poll = async () => {
7011
+ if (!active)
7012
+ return;
7013
+ // Fire the view and the events page concurrently: the events cursor (lastSeq)
7014
+ // does not depend on the run view, so there is no reason to pay two serial
7015
+ // round-trips per tick. Error semantics below are unchanged — the run result
7016
+ // still gates, and a failed events page still publishes the run with no rows.
7017
+ const runPromise = get(appId, runId);
7018
+ const eventsPromise = events(appId, runId, lastSeq);
7019
+ // Keep the in-flight events page from becoming an unhandled rejection if the
7020
+ // run result makes us bail before awaiting it; the real await below still sees it.
7021
+ eventsPromise.catch(() => { });
7022
+ let run;
7023
+ try {
7024
+ const view = await runPromise;
7025
+ if (!active)
7303
7026
  return;
7304
- if (event.source !== popup)
7027
+ run = view.run;
7028
+ lastState = run.state;
7029
+ }
7030
+ catch (error) {
7031
+ if (!active)
7305
7032
  return;
7306
- const d = event.data;
7307
- if (!d || typeof d !== 'object')
7033
+ reportError(error);
7034
+ if (isPermanentError(error)) {
7035
+ stop();
7036
+ return;
7037
+ }
7038
+ schedule(retryCadence(error, lastState, Date.now() - startedAt));
7039
+ return;
7040
+ }
7041
+ try {
7042
+ const response = await eventsPromise;
7043
+ if (!active)
7044
+ return;
7045
+ const deduped = newEvents(Array.isArray(response.events) ? response.events : [], lastSeq, seenSeqs);
7046
+ lastSeq = deduped.lastSeq;
7047
+ publish(run, deduped.events);
7048
+ }
7049
+ catch (error) {
7050
+ if (!active)
7051
+ return;
7052
+ // The view still advances UI state even when event replay is rate-limited.
7053
+ publish(run, []);
7054
+ reportError(error);
7055
+ if (terminalStates.has(run.state)) {
7056
+ // The run itself is done, but a rate-limited final event replay still
7057
+ // owes the caller its unseen seq rows. Honor Retry-After, retry the
7058
+ // piggyback once the limiter permits it, then stop after a successful
7059
+ // replay. Other terminal replay failures stop normally.
7060
+ if (hasRetryAfter(error)) {
7061
+ schedule(retryCadence(error, run.state, Date.now() - startedAt));
7062
+ return;
7063
+ }
7064
+ stop();
7308
7065
  return;
7309
- if (d.type === 'bounded:turnkey:ready') {
7310
- sendRequest();
7066
+ }
7067
+ if (isPermanentError(error)) {
7068
+ stop();
7311
7069
  return;
7312
7070
  }
7313
- if (d.type === 'bounded:turnkey:result' && d.requestId === ex.request.requestId) {
7314
- const outcome = ex.onResult(d);
7315
- if ('error' in outcome) {
7316
- cleanup();
7317
- try {
7318
- popup.close();
7319
- }
7320
- catch ( /* ignore */_a) { /* ignore */ }
7321
- reject(new Error(outcome.error));
7322
- return;
7071
+ schedule(retryCadence(error, run.state, Date.now() - startedAt));
7072
+ return;
7073
+ }
7074
+ if (!active)
7075
+ return;
7076
+ if (terminalStates.has(run.state)) {
7077
+ // Drain the remaining event pages before stopping: a resumed terminal
7078
+ // watch may owe the caller more rows than one page returns. Each page
7079
+ // advances the cursor, so the loop terminates; the cap is a backstop.
7080
+ for (let page = 0; page < 50 && active; page += 1) {
7081
+ let drained;
7082
+ try {
7083
+ const response = await events(appId, runId, lastSeq);
7084
+ if (!active)
7085
+ return;
7086
+ drained = newEvents(Array.isArray(response.events) ? response.events : [], lastSeq, seenSeqs);
7323
7087
  }
7324
- if (outcome.done) {
7325
- cleanup();
7326
- try {
7327
- popup.close();
7328
- }
7329
- catch ( /* ignore */_b) { /* ignore */ }
7330
- resolve(outcome.value);
7088
+ catch (error) {
7089
+ reportError(error);
7090
+ break;
7331
7091
  }
7092
+ if (drained.events.length === 0)
7093
+ break;
7094
+ lastSeq = drained.lastSeq;
7095
+ publish(run, drained.events);
7332
7096
  }
7333
- };
7334
- window.addEventListener('message', onMsg);
7335
- setTimeout(sendRequest, 1200);
7336
- closeTimer = setInterval(() => {
7337
- if (popup.closed) {
7338
- clearInterval(closeTimer);
7339
- setTimeout(() => { if (!settled) {
7340
- cleanup();
7341
- reject(new Error('Wallet approval window was closed before it completed.'));
7342
- } }, 400);
7343
- }
7344
- }, 500);
7345
- setTimeout(() => { if (!settled) {
7346
- cleanup();
7347
- try {
7348
- popup.close();
7097
+ stop();
7098
+ return;
7099
+ }
7100
+ schedule(pollCadence(run.state, Date.now() - startedAt));
7101
+ };
7102
+ void poll();
7103
+ return { stop };
7104
+ }
7105
+
7106
+ var _a;
7107
+ // SSR guard (mirrors useAuth): no hooks/subscriptions during Node server render.
7108
+ // React Native has no `window` but DOES support hooks, so detect Node SSR
7109
+ // specifically via process.versions.node.
7110
+ const isSSR = typeof window === 'undefined'
7111
+ && typeof process !== 'undefined'
7112
+ && !!((_a = process.versions) === null || _a === void 0 ? void 0 : _a.node);
7113
+ function useBuildRun(appId, runId, options) {
7114
+ if (isSSR) {
7115
+ return { run: null, events: [], loading: true, error: null };
7116
+ }
7117
+ const [run, setRun] = React.useState(null);
7118
+ const [events, setEvents] = React.useState([]);
7119
+ const [loading, setLoading] = React.useState(true);
7120
+ const [error, setError] = React.useState(null);
7121
+ const optionsKey = options ? JSON.stringify(options) : '';
7122
+ React.useEffect(() => {
7123
+ setRun(null);
7124
+ setEvents([]);
7125
+ setError(null);
7126
+ if (!appId || !runId) {
7127
+ setLoading(false);
7128
+ return;
7129
+ }
7130
+ let active = true;
7131
+ setLoading(true);
7132
+ const handle = watch(appId, runId, ({ run: nextRun, events: nextEvents }) => {
7133
+ if (!active)
7134
+ return;
7135
+ setRun(nextRun);
7136
+ if (nextEvents.length > 0) {
7137
+ setEvents((current) => {
7138
+ const seen = new Set(current.map((event) => event.seq));
7139
+ return [...current, ...nextEvents.filter((event) => !seen.has(event.seq))];
7140
+ });
7349
7141
  }
7350
- catch ( /* ignore */_a) { /* ignore */ }
7351
- reject(new Error('Wallet approval timed out.'));
7352
- } }, 5 * 60 * 1000);
7142
+ setError(null);
7143
+ setLoading(false);
7144
+ }, Object.assign(Object.assign({}, options), { onError: (value) => {
7145
+ if (!active)
7146
+ return;
7147
+ setError(value instanceof Error ? value : new Error(String(value)));
7148
+ setLoading(false);
7149
+ } }));
7150
+ return () => {
7151
+ active = false;
7152
+ handle.stop();
7153
+ };
7154
+ // optionsKey captures the option object's contents; ids are the other inputs.
7155
+ // eslint-disable-next-line react-hooks/exhaustive-deps
7156
+ }, [appId, runId, optionsKey]);
7157
+ return { run, events, loading, error };
7158
+ }
7159
+
7160
+ /**
7161
+ * Platform first-party surfaces only (bounded.sh, oapps.fun, localhost), with
7162
+ * one exact Drafted exception for delegated review/decision routes. Other
7163
+ * generated-app origins remain blocked; server-side callers are unaffected.
7164
+ */
7165
+ const builds = {
7166
+ submit,
7167
+ get,
7168
+ getReviewCapability,
7169
+ events,
7170
+ cancel,
7171
+ decideGate,
7172
+ refreshReviewCapability,
7173
+ topup,
7174
+ list: list$1,
7175
+ watch,
7176
+ };
7177
+
7178
+ async function create(options = {}) {
7179
+ const body = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (options.name !== undefined ? { name: options.name } : {})), (options.prompt !== undefined ? { prompt: options.prompt } : {})), (options.idempotencyKey !== undefined ? { idempotencyKey: options.idempotencyKey } : {})), (options.intent !== undefined ? { intent: options.intent } : {})), (options.oapp === true ? { oapp: true } : {}));
7180
+ const app = await requestDevApi('/apps/new', {
7181
+ method: 'POST',
7182
+ body,
7353
7183
  });
7184
+ return Object.assign(Object.assign(Object.assign(Object.assign({ appId: app.appId }, (typeof app.slug === 'string' && app.slug ? { slug: app.slug } : {})), { url: app.url }), (typeof app.prompt === 'string' && app.prompt ? { prompt: app.prompt } : {})), (typeof app.handoff === 'string' && app.handoff ? { handoff: app.handoff } : {}));
7354
7185
  }
7355
7186
  /**
7356
- * Get-or-create the user's non-custodial Turnkey wallet, resolving with its stable
7357
- * Solana address. On first use the popup enrolls a device passkey (Face ID / Touch ID)
7358
- * and creates the wallet (lazy) with no email code; if the wallet already exists it
7359
- * resolves immediately without a prompt. Call from a USER GESTURE.
7187
+ * F-1200: build the create-flow redirect the owner is sent to after `create()`, with the
7188
+ * prompt and handoff carried in the URL FRAGMENT (never the query - fragments never reach
7189
+ * servers, logs, or Referer). Encoding is byte-exact so the widget's `URLSearchParams`
7190
+ * parse + `.trim()` reproduces the SAME prompt string dev-api hashed into the handoff;
7191
+ * getting this wrong would make the widget reject a genuine token and degrade to prefill.
7192
+ * Callers (bounded.sh, oapps.fun) should use this instead of hand-building the URL.
7360
7193
  *
7361
- * Use this to learn the fee-payer address BEFORE building a transaction to sign.
7194
+ * Returns `${url}#bounded_prompt=<prompt>` with no `&bounded_handoff=` when the server
7195
+ * minted no handoff (HANDOFF_SECRET unset) - the widget then prefills without auto-running.
7362
7196
  */
7363
- async function getOrCreateTurnkeyWallet() {
7364
- if (!hasDOM$1())
7365
- throw new Error('Turnkey embedded wallets are only available in the browser (popup-based).');
7366
- const { url, origin } = await turnkeySignerPage();
7367
- const requestId = `tkcreate_${Date.now()}_${++requestSeq}`;
7368
- return await deliver(url, origin, {
7369
- request: { type: 'bounded:turnkey:createWallet', requestId },
7370
- onResult: (d) => {
7371
- if (d.ok && typeof d.address === 'string')
7372
- return { done: true, value: { address: d.address, subOrgId: String(d.subOrgId || '') } };
7373
- return { error: String(d.error || 'Wallet setup failed') };
7374
- },
7197
+ function buildCreateRedirectUrl(app) {
7198
+ const base = app.url;
7199
+ const prompt = typeof app.prompt === 'string' ? app.prompt.trim() : '';
7200
+ if (!prompt)
7201
+ return base;
7202
+ const params = `bounded_prompt=${encodeURIComponent(prompt)}`
7203
+ + (app.handoff ? `&bounded_handoff=${encodeURIComponent(app.handoff)}` : '');
7204
+ return `${base}#${params}`;
7205
+ }
7206
+ async function list() {
7207
+ // This legacy-compatible action shape is also used by oapps.fun listMyApps.
7208
+ const response = await requestDevApi('/', {
7209
+ method: 'POST',
7210
+ body: { action: 'listApps' },
7375
7211
  });
7212
+ const rows = Array.isArray(response.apps) ? response.apps : [];
7213
+ return rows
7214
+ .map((value) => {
7215
+ var _a, _b, _c, _d;
7216
+ const row = value && typeof value === 'object'
7217
+ ? value
7218
+ : {};
7219
+ const appId = String((_c = (_b = (_a = row._id) !== null && _a !== void 0 ? _a : row.id) !== null && _b !== void 0 ? _b : row.appId) !== null && _c !== void 0 ? _c : '');
7220
+ if (!/^[a-f0-9]{24}$/i.test(appId))
7221
+ return null;
7222
+ const name = String((_d = row.name) !== null && _d !== void 0 ? _d : '').trim() || '(unnamed app)';
7223
+ const slug = typeof row.slug === 'string' && row.slug ? row.slug : undefined;
7224
+ const url = typeof row.url === 'string' && row.url.trim() ? row.url.trim() : undefined;
7225
+ const buildStatus = row.buildStatus === 'building' || row.buildStatus === 'build_failed'
7226
+ ? row.buildStatus
7227
+ : undefined;
7228
+ return Object.assign(Object.assign(Object.assign({ appId,
7229
+ name }, (slug ? { slug } : {})), (url ? { url } : {})), (buildStatus ? { buildStatus } : {}));
7230
+ })
7231
+ .filter((app) => app !== null);
7376
7232
  }
7377
7233
  /**
7378
- * Open the Bounded Turnkey signer popup and sign `message` (raw bytes) with the
7379
- * user's non-custodial Turnkey wallet, resolving with the ed25519 signature +
7380
- * the wallet address. MUST be called from a USER GESTURE (click/tap) or the
7381
- * browser blocks the popup.
7382
- *
7383
- * On the user's FIRST signature the popup transparently enrolls a device passkey
7384
- * (Face ID / Touch ID) and creates their wallet - no email code. Subsequent
7385
- * signatures are just a passkey approval.
7234
+ * Platform first-party surfaces only (bounded.sh, oapps.fun, localhost) —
7235
+ * dev-api CORS blocks calls from generated-app origins. Server-side callers unaffected.
7386
7236
  */
7387
- async function signSolanaMessageViaTurnkey(message) {
7388
- if (!hasDOM$1()) {
7389
- throw new Error('Turnkey embedded-wallet signing is only available in the browser (popup-based).');
7237
+ const apps = { create, list, buildCreateRedirectUrl };
7238
+
7239
+ async function deserializeTransaction(tx) {
7240
+ // Literal specifier so bundlers can code-split the lazy chunk.
7241
+ const { Transaction, VersionedTransaction } = await import('@solana/web3.js');
7242
+ const buf = Buffer.isBuffer(tx) ? tx : Buffer.from(tx, 'base64');
7243
+ if ((buf[0] & 0x80) !== 0) {
7244
+ return VersionedTransaction.deserialize(buf);
7245
+ }
7246
+ else {
7247
+ return Transaction.from(buf);
7390
7248
  }
7391
- const { url, origin } = await turnkeySignerPage();
7392
- const requestId = `tksign_${Date.now()}_${++requestSeq}`;
7393
- const messageHex = toHex(message);
7394
- return await deliver(url, origin, {
7395
- request: { type: 'bounded:turnkey:sign', requestId, messageHex },
7396
- onResult: (d) => {
7397
- if (d.ok && typeof d.signatureHex === 'string' && typeof d.address === 'string') {
7398
- return { done: true, value: { signature: fromHex(d.signatureHex), address: d.address, subOrgId: String(d.subOrgId || '') } };
7399
- }
7400
- return { error: String(d.error || 'Turnkey signing failed') };
7401
- },
7402
- });
7403
7249
  }
7404
7250
 
7405
7251
  // bounded.onramp() - fund the signed-in user's wallet with Coinbase Onramp.
@@ -8063,5 +7909,5 @@ function defaultInjectedEvmProvider() {
8063
7909
  return null;
8064
7910
  }
8065
7911
 
8066
- export { FAILURE_REASONS as $, completeLoginFromRedirect as A, loginWithPopup as B, completeLoginInPopup as C, openBoundedWidget as D, EMBEDDED_WALLET_UNSUPPORTED as E, useAuth as F, useQuery as G, useBuildRun as H, apps as I, deserializeTransaction as J, signSolanaMessageViaTurnkey as K, getOrCreateTurnkeyWallet as L, startTurnkeyEmailLogin as M, getIdToken as N, onramp as O, onrampQuote as P, onrampStatus as Q, setPlatform as R, OffchainAuthProvider as S, PrivyExpoProvider as T, requestEip6963Providers as U, builds as V, BuildsApiError as W, NON_TERMINAL_STATES as X, TERMINAL_STATES as Y, RUN_STATES as Z, PARK_REASONS as _, hasEmbeddedEvmWallet as a, BUILD_REVIEW_CAPABILITY_SCHEMA as a0, BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA as a1, BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA as a2, embeddedSignAndSubmit as b, setAuthLoading as c, defaultInjectedProvider as d, embeddedRunEvmTransaction as e, bufferExports as f, getPlatform as g, hasEmbeddedWallet as h, bs58 as i, confirmAndCheckTransaction as j, defaultInjectedEvmProvider as k, init as l, getCurrentUser as m, normalizeSolanaRpcUrl as n, onAuthStateChanged as o, onAuthLoadingChanged as p, getAuthLoading as q, resolveSolanaRpcUrl as r, setCurrentUser as s, login as t, logout as u, getAuthProvider as v, signInAnonymously as w, loginWithPrivy as x, loginWithWallet as y, loginWithRedirect as z };
8067
- //# sourceMappingURL=index-L2R6C5Vl.mjs.map
7912
+ export { BUILD_REVIEW_CAPABILITY_SCHEMA as $, completeLoginFromRedirect as A, loginWithPopup as B, completeLoginInPopup as C, openBoundedWidget as D, useAuth as E, useQuery as F, useBuildRun as G, apps as H, deserializeTransaction as I, signSolanaMessageViaTurnkey as J, getOrCreateTurnkeyWallet as K, startTurnkeyEmailLogin as L, getIdToken as M, onramp as N, onrampQuote as O, onrampStatus as P, setPlatform as Q, OffchainAuthProvider as R, PrivyExpoProvider as S, requestEip6963Providers as T, builds as U, BuildsApiError as V, NON_TERMINAL_STATES as W, TERMINAL_STATES as X, RUN_STATES as Y, PARK_REASONS as Z, FAILURE_REASONS as _, signMessageWithTurnkey as a, BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA as a0, BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA as a1, signTransactionWithTurnkey as b, signAndSubmitTransactionWithTurnkey as c, defaultInjectedProvider as d, setAuthLoading as e, bufferExports as f, getPlatform as g, hasTurnkeyWallet as h, bs58 as i, confirmAndCheckTransaction as j, defaultInjectedEvmProvider as k, init as l, getCurrentUser as m, normalizeSolanaRpcUrl as n, onAuthStateChanged as o, onAuthLoadingChanged as p, getAuthLoading as q, resolveSolanaRpcUrl as r, setCurrentUser as s, login as t, logout as u, getAuthProvider as v, signInAnonymously as w, loginWithPrivy as x, loginWithWallet as y, loginWithRedirect as z };
7913
+ //# sourceMappingURL=index-7yz-Olrm.mjs.map