@bounded-sh/client 0.0.35 → 0.0.37

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.
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getActiveSessionManager, genAuthNonce, genSolanaMessage, createSessionWithSignature, getConfig, WebSessionManager, ReactNativeSessionManager, revokeSession, init as init$1, reconnectWithNewAuth, deriveUserIdentityFromIdToken, subscribe, getIdToken as getIdToken$1, convertRemainingAccounts, buildSetDocumentsTransaction } from '@bounded-sh/core';
2
2
  export * from '@bounded-sh/core';
3
- export { aggregate, count, docId, get, getConfig, getFiles, getMany, queryAggregate, runExpression, runExpressionMany, runQuery, runQueryMany, search, set, setFile, setMany, signAndSubmitTransaction, signMessage, signTransaction, subscribe } from '@bounded-sh/core';
3
+ export { BoundedDeclineError, DECLINE_EVENT_NAME, aggregate, count, deriveDecline, dispatchDeclineEvent, docId, get, getConfig, getFiles, getMany, queryAggregate, runExpression, runExpressionMany, runQuery, runQueryMany, search, set, setFile, setMany, signAndSubmitTransaction, signMessage, signTransaction, subscribe } from '@bounded-sh/core';
4
4
  import * as React from 'react';
5
5
  import { VersionedTransaction, Transaction, Connection, PublicKey } from '@solana/web3.js';
6
6
  import * as anchor from '@coral-xyz/anchor';
@@ -145,6 +145,15 @@ class OffchainAuthProvider {
145
145
  }
146
146
  return null;
147
147
  }
148
+ // Forward per-call login overrides (theme/title/subtitle and the `method`
149
+ // directive) to the wrapped provider. Without this, the top-level login()
150
+ // forwarder (which duck-types `setLoginOverrides` on the active provider)
151
+ // skips the wrapper and the override — including `method` — is dropped on
152
+ // offchain/Poofnet builds.
153
+ setLoginOverrides(opts) {
154
+ var _a, _b;
155
+ (_b = (_a = this.wrappedProvider).setLoginOverrides) === null || _b === void 0 ? void 0 : _b.call(_a, opts);
156
+ }
148
157
  async logout() {
149
158
  await this.wrappedProvider.logout();
150
159
  }
@@ -178,6 +187,15 @@ class OffchainAuthProvider {
178
187
  async getNativeMethods() {
179
188
  return this.wrappedProvider.getNativeMethods();
180
189
  }
190
+ // Forward wallet export to the wrapped provider (only Privy embedded wallets
191
+ // implement it). Duck-typed so wrappers around non-embedded providers throw
192
+ // a clear error rather than a "not a function" crash.
193
+ async exportWallet(address) {
194
+ if (typeof this.wrappedProvider.exportWallet !== 'function') {
195
+ throw new Error('Wallet export is not available for this wallet.');
196
+ }
197
+ await this.wrappedProvider.exportWallet(address);
198
+ }
181
199
  // ============ signMessage delegates to wrapped provider ============
182
200
  async signMessage(message) {
183
201
  var _a, _b, _c, _d, _e, _f;
@@ -3407,7 +3425,7 @@ const IDB_STORE = 'keys';
3407
3425
  const IDB_KEY = 'ed25519';
3408
3426
  const GUEST_KEY_REQUIREMENT = 'Guest auth requires non-extractable WebCrypto Ed25519 keys persisted in IndexedDB; ' +
3409
3427
  'stored-secret guest credentials are disabled.';
3410
- function toBase64(bytes) {
3428
+ function toBase64$1(bytes) {
3411
3429
  let bin = '';
3412
3430
  for (let i = 0; i < bytes.length; i++)
3413
3431
  bin += String.fromCharCode(bytes[i]);
@@ -3471,7 +3489,7 @@ async function webCryptoKey() {
3471
3489
  async sign(message) {
3472
3490
  const data = new TextEncoder().encode(message);
3473
3491
  const sig = new Uint8Array(await s.sign({ name: 'Ed25519' }, pair.privateKey, data));
3474
- return toBase64(sig);
3492
+ return toBase64$1(sig);
3475
3493
  },
3476
3494
  };
3477
3495
  }
@@ -3574,14 +3592,221 @@ class GuestAuthProvider {
3574
3592
  }
3575
3593
  }
3576
3594
 
3595
+ var bufferExports = requireBuffer();
3596
+
3597
+ // crossmint-signer-bridge.ts — routes embedded-wallet signing through the
3598
+ // Bounded-owned signer popup (auth.bounded.sh/wallet/signer).
3599
+ //
3600
+ // Slice-1 attached a non-custodial Crossmint smart wallet to email logins and
3601
+ // populated `@user.address`. This bridge is the slice-2 counterpart: a user
3602
+ // whose address came from `auth.wallets` can APPROVE transactions with that
3603
+ // wallet from ANY app, non-custodially, by having the Crossmint client SDK run
3604
+ // ONLY on the Bounded signer page (one whitelisted origin) and talking to it via
3605
+ // postMessage.
3606
+ //
3607
+ // SEMANTICS (Crossmint Solana smart wallets): these wallets cannot produce raw
3608
+ // detached signatures — there is no signMessage and no sign-without-submit. The
3609
+ // only honest primitive is EXECUTION (sign + submit) via the wallet's email-OTP
3610
+ // authorization. So this bridge implements `signAndSubmitTransaction` and a
3611
+ // token-send; `signMessage` / `signTransaction` throw a clear unsupported error.
3612
+ const EMBEDDED_WALLET_UNSUPPORTED = 'Unsupported for embedded smart wallets — Crossmint Solana smart wallets sign ' +
3613
+ 'and submit atomically. Use signAndSubmitTransaction() (or bounded.sendWalletToken) ' +
3614
+ 'instead of signMessage/signTransaction.';
3615
+ const NO_EMBEDDED_WALLET = 'This login has no embedded wallet. Enable `"auth": { "wallets": true }` in ' +
3616
+ 'the app policy so email logins get a non-custodial wallet (@user.address), ' +
3617
+ 'then sign from a user gesture.';
3618
+ function hasDOM$1() {
3619
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
3620
+ }
3621
+ function decodeJwt$2(token) {
3622
+ try {
3623
+ let b64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
3624
+ const pad = b64.length % 4;
3625
+ if (pad === 2)
3626
+ b64 += '==';
3627
+ else if (pad === 3)
3628
+ b64 += '=';
3629
+ return JSON.parse(bufferExports.Buffer.from(b64, 'base64').toString('utf8'));
3630
+ }
3631
+ catch (_a) {
3632
+ return {};
3633
+ }
3634
+ }
3635
+ // Pull the embedded-wallet identity out of the active session's id token.
3636
+ // Returns null when this session has no wallet (so callers can throw the
3637
+ // informative NO_EMBEDDED_WALLET error).
3638
+ function walletClaimsFromSession() {
3639
+ let idToken = null;
3640
+ try {
3641
+ idToken = getActiveSessionManager().getIdToken();
3642
+ }
3643
+ catch (_a) {
3644
+ idToken = null;
3645
+ }
3646
+ if (!idToken)
3647
+ return null;
3648
+ const c = decodeJwt$2(idToken);
3649
+ const address = c['custom:walletAddress'];
3650
+ const email = c['email'];
3651
+ if (typeof address !== 'string' || !address)
3652
+ return null;
3653
+ if (typeof email !== 'string' || !email)
3654
+ return null;
3655
+ const envClaim = c['custom:walletEnvironment'];
3656
+ const environment = envClaim === 'staging' || envClaim === 'production' ? envClaim : undefined;
3657
+ return { email, address, environment };
3658
+ }
3659
+ async function signerPageUrl() {
3660
+ const cfg = await getConfig();
3661
+ const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
3662
+ return { url: `${base}/wallet/signer`, origin: new URL(base).origin };
3663
+ }
3664
+ let requestSeq = 0;
3665
+ /**
3666
+ * Open the Bounded signer popup, send `action` to it, and resolve with the
3667
+ * submitted transaction result. Must be called from a USER GESTURE (click/tap) or
3668
+ * the browser will block the popup — in which case this rejects with a clear
3669
+ * "Popup blocked" error telling the app to call from a user gesture.
3670
+ *
3671
+ * Non-custodial: the popup gates signing behind an email OTP delivered to the
3672
+ * wallet's owner, and validates that the wallet address matches the account.
3673
+ */
3674
+ async function signViaSignerPopup(action) {
3675
+ if (!hasDOM$1()) {
3676
+ throw new Error('Embedded-wallet signing is only available in the browser (popup-based).');
3677
+ }
3678
+ const claims = walletClaimsFromSession();
3679
+ if (!claims)
3680
+ throw new Error(NO_EMBEDDED_WALLET);
3681
+ const { url, origin: signerOrigin } = await signerPageUrl();
3682
+ const requestId = `bsign_${Date.now()}_${++requestSeq}`;
3683
+ // Centered popup, sized for the OTP form (matches the OIDC login popup shape).
3684
+ const w = 420, h = 600;
3685
+ const left = window.screenX + Math.max(0, (window.outerWidth - w) / 2);
3686
+ const top = window.screenY + Math.max(0, (window.outerHeight - h) / 2);
3687
+ const popup = window.open(url, 'bounded-wallet-signer', `width=${w},height=${h},left=${left},top=${top}`);
3688
+ if (!popup) {
3689
+ throw new Error('Popup blocked — call signAndSubmitTransaction() from a user gesture (e.g. a click handler) so the wallet approval window can open.');
3690
+ }
3691
+ const request = {
3692
+ type: 'bounded:sign',
3693
+ requestId,
3694
+ environment: claims.environment, // may be undefined → page auto-detects
3695
+ email: claims.email,
3696
+ address: claims.address,
3697
+ action,
3698
+ };
3699
+ return await new Promise((resolve, reject) => {
3700
+ let settled = false;
3701
+ let closeTimer;
3702
+ let sentRequest = false;
3703
+ const cleanup = () => {
3704
+ settled = true;
3705
+ window.removeEventListener('message', onMsg);
3706
+ if (closeTimer)
3707
+ clearInterval(closeTimer);
3708
+ };
3709
+ const sendRequest = () => {
3710
+ if (sentRequest)
3711
+ return;
3712
+ sentRequest = true;
3713
+ try {
3714
+ popup.postMessage(request, signerOrigin);
3715
+ }
3716
+ catch ( /* popup gone */_a) { /* popup gone */ }
3717
+ };
3718
+ const onMsg = (ev) => {
3719
+ if (ev.origin !== signerOrigin)
3720
+ return; // only trust the signer page origin
3721
+ const d = ev.data;
3722
+ if (!d || typeof d !== 'object')
3723
+ return;
3724
+ if (d.type === 'bounded:signer:ready') {
3725
+ sendRequest();
3726
+ return;
3727
+ }
3728
+ if (d.type === 'bounded:signer:result' && d.requestId === requestId) {
3729
+ cleanup();
3730
+ try {
3731
+ popup.close();
3732
+ }
3733
+ catch ( /* ignore */_a) { /* ignore */ }
3734
+ if (d.ok)
3735
+ resolve({ hash: d.hash, explorerLink: d.explorerLink, transactionId: d.transactionId });
3736
+ else
3737
+ reject(new Error(String(d.error || 'Wallet signing failed')));
3738
+ }
3739
+ };
3740
+ window.addEventListener('message', onMsg);
3741
+ // Fallback: if the ready ping was missed (fast load / focus race), push the
3742
+ // request shortly after opening. The page ignores it until a bounded:sign
3743
+ // arrives and only acts on the first one, so a duplicate is harmless.
3744
+ setTimeout(sendRequest, 1200);
3745
+ // If the user closes the popup before completing, reject (unless a result
3746
+ // just arrived — give any in-flight message a beat to settle first).
3747
+ closeTimer = setInterval(() => {
3748
+ if (popup.closed) {
3749
+ clearInterval(closeTimer);
3750
+ setTimeout(() => { if (!settled) {
3751
+ cleanup();
3752
+ reject(new Error('Wallet approval window was closed before signing completed.'));
3753
+ } }, 400);
3754
+ }
3755
+ }, 500);
3756
+ // Overall safety timeout (OTP + signing). Generous — the user may take a
3757
+ // while to fetch the code from their email.
3758
+ setTimeout(() => { if (!settled) {
3759
+ cleanup();
3760
+ try {
3761
+ popup.close();
3762
+ }
3763
+ catch ( /* ignore */_a) { /* ignore */ }
3764
+ reject(new Error('Wallet approval timed out.'));
3765
+ } }, 5 * 60 * 1000);
3766
+ });
3767
+ }
3768
+ function toBase64(bytes) {
3769
+ return bufferExports.Buffer.from(bytes).toString('base64');
3770
+ }
3771
+ // Serialize a legacy or versioned Solana transaction to base64 for the signer
3772
+ // page. We do NOT require existing signatures (the smart wallet signs on the
3773
+ // page); Crossmint extracts the instructions and wraps them for the smart wallet.
3774
+ function serializeTransaction(transaction) {
3775
+ const isLegacy = 'recentBlockhash' in transaction &&
3776
+ !('message' in transaction && 'staticAccountKeys' in transaction.message);
3777
+ const bytes = isLegacy
3778
+ ? transaction.serialize({ requireAllSignatures: false, verifySignatures: false })
3779
+ : transaction.serialize();
3780
+ return toBase64(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
3781
+ }
3782
+ /**
3783
+ * Whether the active session carries an embedded wallet (so the provider should
3784
+ * route signing through the popup rather than throw "no wallet").
3785
+ */
3786
+ function hasEmbeddedWallet() {
3787
+ return walletClaimsFromSession() != null;
3788
+ }
3789
+ /** signAndSubmitTransaction for embedded wallets: serialize + route through the
3790
+ * signer popup, resolve with the on-chain transaction signature/hash. */
3791
+ async function embeddedSignAndSubmit(transaction) {
3792
+ const serializedTransaction = serializeTransaction(transaction);
3793
+ const result = await signViaSignerPopup({ kind: 'transaction', serializedTransaction });
3794
+ const hash = result.hash || result.transactionId;
3795
+ if (!hash)
3796
+ throw new Error('Signer returned no transaction hash.');
3797
+ return hash;
3798
+ }
3799
+
3577
3800
  // Web uses localStorage (WebSessionManager); React Native uses the configured
3578
3801
  // ReactNativeSessionManager. getActiveSessionManager() picks the right one by
3579
3802
  // whether the RN manager was configured (NOT `typeof window`, which is truthy on
3580
3803
  // RN too and would wrongly route to localStorage). Same selector the core
3581
3804
  // request path uses, so hosted Bounded Auth sessions persist + authenticate on BOTH.
3582
3805
  const sessionManager$1 = getActiveSessionManager;
3583
- const NO_WALLET = 'Email login is offchain — it has no Solana wallet and cannot sign/submit ' +
3584
- 'transactions. Use a wallet provider for onchain actions, or link a wallet later.';
3806
+ const NO_WALLET = 'Email login has no embedded wallet. Enable `"auth": { "wallets": true }` in ' +
3807
+ 'the app policy so email logins get a non-custodial Crossmint wallet ' +
3808
+ '(@user.address), then sign from a user gesture. Otherwise use a wallet ' +
3809
+ 'provider (e.g. Phantom) for onchain actions.';
3585
3810
  // ---------------------------------------------------------------------------
3586
3811
  // token / session helpers
3587
3812
  // ---------------------------------------------------------------------------
@@ -3631,10 +3856,23 @@ class EmailAuthProvider {
3631
3856
  sessionManager$1().clearSession();
3632
3857
  setCurrentUser(null);
3633
3858
  }
3634
- async signMessage(_message) { throw new Error(NO_WALLET); }
3859
+ // Crossmint Solana smart wallets sign+submit atomically and cannot produce a
3860
+ // raw signature — signMessage/signTransaction are structurally unsupported
3861
+ // (throw the clear "use signAndSubmitTransaction" error when a wallet exists;
3862
+ // the no-wallet error otherwise). signAndSubmitTransaction routes through the
3863
+ // Bounded signer popup for wallets-enabled email logins.
3864
+ async signMessage(_message) {
3865
+ throw new Error(hasEmbeddedWallet() ? EMBEDDED_WALLET_UNSUPPORTED : NO_WALLET);
3866
+ }
3635
3867
  async runTransaction(_evm, _sol, _options) { throw new Error(NO_WALLET); }
3636
- async signTransaction(_t) { throw new Error(NO_WALLET); }
3637
- async signAndSubmitTransaction() { throw new Error(NO_WALLET); }
3868
+ async signTransaction(_t) {
3869
+ throw new Error(hasEmbeddedWallet() ? EMBEDDED_WALLET_UNSUPPORTED : NO_WALLET);
3870
+ }
3871
+ async signAndSubmitTransaction(transaction, _feePayer) {
3872
+ if (!hasEmbeddedWallet())
3873
+ throw new Error(NO_WALLET);
3874
+ return embeddedSignAndSubmit(transaction);
3875
+ }
3638
3876
  async getNativeMethods() { return {}; }
3639
3877
  }
3640
3878
  const emailProviderSingleton = new EmailAuthProvider();
@@ -3665,6 +3903,11 @@ function resolveSolanaRpcUrl(explicitRpcUrl, network, context) {
3665
3903
  throw new Error(`${context} requires an explicit Solana RPC URL. Pass config.rpcUrl before signing or submitting onchain transactions.`);
3666
3904
  }
3667
3905
 
3906
+ // Same message the email provider uses when a hosted (OIDC) login has no
3907
+ // embedded wallet — enable `auth.wallets` in policy to get one.
3908
+ const OIDC_NO_WALLET = 'This login has no embedded wallet. Enable `"auth": { "wallets": true }` in ' +
3909
+ 'the app policy so logins get a non-custodial Crossmint wallet (@user.address), ' +
3910
+ 'then sign from a user gesture.';
3668
3911
  const PKCE_STORE_KEY = 'bounded.oidc.pkce';
3669
3912
  const STORED_AUTH_METHOD_KEY$1 = 'bounded_last_auth_method';
3670
3913
  // Active session store (Web on web, RN on React Native) — selected by config,
@@ -3689,6 +3932,10 @@ function decodeJwt(token) {
3689
3932
  }
3690
3933
  }
3691
3934
  // A minimal AuthProvider so getSession()/logout() work for OIDC sessions, mirroring email.
3935
+ // Signing surface (slice 2): a hosted login on a wallets-enabled app carries an
3936
+ // embedded Crossmint wallet (@user.address). signAndSubmitTransaction routes
3937
+ // through the Bounded signer popup; signMessage/signTransaction are structurally
3938
+ // unsupported for Crossmint smart wallets (sign+submit atomically, no raw sig).
3692
3939
  const oidcProviderSingleton = {
3693
3940
  async login() {
3694
3941
  const idToken = sessionManager().getIdToken();
@@ -3699,6 +3946,17 @@ const oidcProviderSingleton = {
3699
3946
  return idToken ? userFromIdToken(idToken) : null;
3700
3947
  },
3701
3948
  async logout() { sessionManager().clearSession(); },
3949
+ async signMessage(_message) {
3950
+ throw new Error(hasEmbeddedWallet() ? EMBEDDED_WALLET_UNSUPPORTED : OIDC_NO_WALLET);
3951
+ },
3952
+ async signTransaction(_t) {
3953
+ throw new Error(hasEmbeddedWallet() ? EMBEDDED_WALLET_UNSUPPORTED : OIDC_NO_WALLET);
3954
+ },
3955
+ async signAndSubmitTransaction(transaction, _feePayer) {
3956
+ if (!hasEmbeddedWallet())
3957
+ throw new Error(OIDC_NO_WALLET);
3958
+ return embeddedSignAndSubmit(transaction);
3959
+ },
3702
3960
  };
3703
3961
  function userFromIdToken(idToken) {
3704
3962
  const c = decodeJwt(idToken);
@@ -4604,7 +4862,7 @@ function useAuth() {
4604
4862
  // Provide a fallback so server render doesn't break
4605
4863
  if (isSSR$1) {
4606
4864
  return {
4607
- login: async () => undefined,
4865
+ login: async (_options) => undefined,
4608
4866
  logout: async () => undefined,
4609
4867
  loading: true,
4610
4868
  user: null,
@@ -4626,10 +4884,10 @@ function useAuth() {
4626
4884
  stopLoading();
4627
4885
  };
4628
4886
  }, []);
4629
- const login$1 = async () => {
4887
+ const login$1 = async (options) => {
4630
4888
  try {
4631
4889
  setLoading(true);
4632
- const user = await login();
4890
+ const user = await login(options);
4633
4891
  setUser(user);
4634
4892
  }
4635
4893
  catch (error) {
@@ -4739,8 +4997,6 @@ async function getIdToken() {
4739
4997
  return getIdToken$1(false);
4740
4998
  }
4741
4999
 
4742
- var bufferExports = requireBuffer();
4743
-
4744
5000
  /**
4745
5001
  * Extracts a human-readable error message from Solana transaction logs.
4746
5002
  */