@bounded-sh/client 0.0.34 → 0.0.36

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.
@@ -0,0 +1,37 @@
1
+ import { Transaction, VersionedTransaction } from '@solana/web3.js';
2
+ export type SignerAction = {
3
+ kind: 'sendToken';
4
+ recipient: string;
5
+ token: string;
6
+ amount: string;
7
+ } | {
8
+ kind: 'transaction';
9
+ serializedTransaction: string;
10
+ };
11
+ export interface SignerResult {
12
+ hash?: string;
13
+ explorerLink?: string;
14
+ transactionId?: string;
15
+ }
16
+ export declare const EMBEDDED_WALLET_UNSUPPORTED: string;
17
+ /**
18
+ * Open the Bounded signer popup, send `action` to it, and resolve with the
19
+ * submitted transaction result. Must be called from a USER GESTURE (click/tap) or
20
+ * the browser will block the popup — in which case this rejects with a clear
21
+ * "Popup blocked" error telling the app to call from a user gesture.
22
+ *
23
+ * Non-custodial: the popup gates signing behind an email OTP delivered to the
24
+ * wallet's owner, and validates that the wallet address matches the account.
25
+ */
26
+ export declare function signViaSignerPopup(action: SignerAction): Promise<SignerResult>;
27
+ /**
28
+ * Whether the active session carries an embedded wallet (so the provider should
29
+ * route signing through the popup rather than throw "no wallet").
30
+ */
31
+ export declare function hasEmbeddedWallet(): boolean;
32
+ /** signAndSubmitTransaction for embedded wallets: serialize + route through the
33
+ * signer popup, resolve with the on-chain transaction signature/hash. */
34
+ export declare function embeddedSignAndSubmit(transaction: Transaction | VersionedTransaction): Promise<string>;
35
+ /** Send a token from the embedded wallet (Crossmint `wallet.send`). Convenience
36
+ * primitive for the common "send crypto" case; returns the tx result. */
37
+ export declare function embeddedSendToken(recipient: string, token: string, amount: string): Promise<SignerResult>;
@@ -1,5 +1,6 @@
1
1
  import type { AuthProvider, User, TransactionResult, SolTransaction, EVMTransaction, SetOptions } from '@bounded-sh/core';
2
2
  import { Transaction, VersionedTransaction } from '@solana/web3.js';
3
+ import { PublicKey } from '@solana/web3.js';
3
4
  export declare class EmailAuthProvider implements AuthProvider {
4
5
  login(): Promise<User | null>;
5
6
  restoreSession(): Promise<User | null>;
@@ -7,7 +8,7 @@ export declare class EmailAuthProvider implements AuthProvider {
7
8
  signMessage(_message: string): Promise<string>;
8
9
  runTransaction(_evm?: EVMTransaction, _sol?: SolTransaction, _options?: SetOptions): Promise<TransactionResult>;
9
10
  signTransaction(_t: Transaction | VersionedTransaction): Promise<Transaction | VersionedTransaction>;
10
- signAndSubmitTransaction(): Promise<string>;
11
+ signAndSubmitTransaction(transaction: Transaction | VersionedTransaction, _feePayer?: PublicKey): Promise<string>;
11
12
  getNativeMethods(): Promise<any>;
12
13
  }
13
14
  declare const emailProviderSingleton: EmailAuthProvider;
package/dist/index.js CHANGED
@@ -3427,7 +3427,7 @@ const IDB_STORE = 'keys';
3427
3427
  const IDB_KEY = 'ed25519';
3428
3428
  const GUEST_KEY_REQUIREMENT = 'Guest auth requires non-extractable WebCrypto Ed25519 keys persisted in IndexedDB; ' +
3429
3429
  'stored-secret guest credentials are disabled.';
3430
- function toBase64(bytes) {
3430
+ function toBase64$1(bytes) {
3431
3431
  let bin = '';
3432
3432
  for (let i = 0; i < bytes.length; i++)
3433
3433
  bin += String.fromCharCode(bytes[i]);
@@ -3491,7 +3491,7 @@ async function webCryptoKey() {
3491
3491
  async sign(message) {
3492
3492
  const data = new TextEncoder().encode(message);
3493
3493
  const sig = new Uint8Array(await s.sign({ name: 'Ed25519' }, pair.privateKey, data));
3494
- return toBase64(sig);
3494
+ return toBase64$1(sig);
3495
3495
  },
3496
3496
  };
3497
3497
  }
@@ -3594,14 +3594,221 @@ class GuestAuthProvider {
3594
3594
  }
3595
3595
  }
3596
3596
 
3597
+ var bufferExports = requireBuffer();
3598
+
3599
+ // crossmint-signer-bridge.ts — routes embedded-wallet signing through the
3600
+ // Bounded-owned signer popup (auth.bounded.sh/wallet/signer).
3601
+ //
3602
+ // Slice-1 attached a non-custodial Crossmint smart wallet to email logins and
3603
+ // populated `@user.address`. This bridge is the slice-2 counterpart: a user
3604
+ // whose address came from `auth.wallets` can APPROVE transactions with that
3605
+ // wallet from ANY app, non-custodially, by having the Crossmint client SDK run
3606
+ // ONLY on the Bounded signer page (one whitelisted origin) and talking to it via
3607
+ // postMessage.
3608
+ //
3609
+ // SEMANTICS (Crossmint Solana smart wallets): these wallets cannot produce raw
3610
+ // detached signatures — there is no signMessage and no sign-without-submit. The
3611
+ // only honest primitive is EXECUTION (sign + submit) via the wallet's email-OTP
3612
+ // authorization. So this bridge implements `signAndSubmitTransaction` and a
3613
+ // token-send; `signMessage` / `signTransaction` throw a clear unsupported error.
3614
+ const EMBEDDED_WALLET_UNSUPPORTED = 'Unsupported for embedded smart wallets — Crossmint Solana smart wallets sign ' +
3615
+ 'and submit atomically. Use signAndSubmitTransaction() (or bounded.sendWalletToken) ' +
3616
+ 'instead of signMessage/signTransaction.';
3617
+ const NO_EMBEDDED_WALLET = 'This login has no embedded wallet. Enable `"auth": { "wallets": true }` in ' +
3618
+ 'the app policy so email logins get a non-custodial wallet (@user.address), ' +
3619
+ 'then sign from a user gesture.';
3620
+ function hasDOM$1() {
3621
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
3622
+ }
3623
+ function decodeJwt$2(token) {
3624
+ try {
3625
+ let b64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
3626
+ const pad = b64.length % 4;
3627
+ if (pad === 2)
3628
+ b64 += '==';
3629
+ else if (pad === 3)
3630
+ b64 += '=';
3631
+ return JSON.parse(bufferExports.Buffer.from(b64, 'base64').toString('utf8'));
3632
+ }
3633
+ catch (_a) {
3634
+ return {};
3635
+ }
3636
+ }
3637
+ // Pull the embedded-wallet identity out of the active session's id token.
3638
+ // Returns null when this session has no wallet (so callers can throw the
3639
+ // informative NO_EMBEDDED_WALLET error).
3640
+ function walletClaimsFromSession() {
3641
+ let idToken = null;
3642
+ try {
3643
+ idToken = core.getActiveSessionManager().getIdToken();
3644
+ }
3645
+ catch (_a) {
3646
+ idToken = null;
3647
+ }
3648
+ if (!idToken)
3649
+ return null;
3650
+ const c = decodeJwt$2(idToken);
3651
+ const address = c['custom:walletAddress'];
3652
+ const email = c['email'];
3653
+ if (typeof address !== 'string' || !address)
3654
+ return null;
3655
+ if (typeof email !== 'string' || !email)
3656
+ return null;
3657
+ const envClaim = c['custom:walletEnvironment'];
3658
+ const environment = envClaim === 'staging' || envClaim === 'production' ? envClaim : undefined;
3659
+ return { email, address, environment };
3660
+ }
3661
+ async function signerPageUrl() {
3662
+ const cfg = await core.getConfig();
3663
+ const base = (cfg.humanAuthApiUrl || 'https://auth.bounded.sh').replace(/\/$/, '');
3664
+ return { url: `${base}/wallet/signer`, origin: new URL(base).origin };
3665
+ }
3666
+ let requestSeq = 0;
3667
+ /**
3668
+ * Open the Bounded signer popup, send `action` to it, and resolve with the
3669
+ * submitted transaction result. Must be called from a USER GESTURE (click/tap) or
3670
+ * the browser will block the popup — in which case this rejects with a clear
3671
+ * "Popup blocked" error telling the app to call from a user gesture.
3672
+ *
3673
+ * Non-custodial: the popup gates signing behind an email OTP delivered to the
3674
+ * wallet's owner, and validates that the wallet address matches the account.
3675
+ */
3676
+ async function signViaSignerPopup(action) {
3677
+ if (!hasDOM$1()) {
3678
+ throw new Error('Embedded-wallet signing is only available in the browser (popup-based).');
3679
+ }
3680
+ const claims = walletClaimsFromSession();
3681
+ if (!claims)
3682
+ throw new Error(NO_EMBEDDED_WALLET);
3683
+ const { url, origin: signerOrigin } = await signerPageUrl();
3684
+ const requestId = `bsign_${Date.now()}_${++requestSeq}`;
3685
+ // Centered popup, sized for the OTP form (matches the OIDC login popup shape).
3686
+ const w = 420, h = 600;
3687
+ const left = window.screenX + Math.max(0, (window.outerWidth - w) / 2);
3688
+ const top = window.screenY + Math.max(0, (window.outerHeight - h) / 2);
3689
+ const popup = window.open(url, 'bounded-wallet-signer', `width=${w},height=${h},left=${left},top=${top}`);
3690
+ if (!popup) {
3691
+ throw new Error('Popup blocked — call signAndSubmitTransaction() from a user gesture (e.g. a click handler) so the wallet approval window can open.');
3692
+ }
3693
+ const request = {
3694
+ type: 'bounded:sign',
3695
+ requestId,
3696
+ environment: claims.environment, // may be undefined → page auto-detects
3697
+ email: claims.email,
3698
+ address: claims.address,
3699
+ action,
3700
+ };
3701
+ return await new Promise((resolve, reject) => {
3702
+ let settled = false;
3703
+ let closeTimer;
3704
+ let sentRequest = false;
3705
+ const cleanup = () => {
3706
+ settled = true;
3707
+ window.removeEventListener('message', onMsg);
3708
+ if (closeTimer)
3709
+ clearInterval(closeTimer);
3710
+ };
3711
+ const sendRequest = () => {
3712
+ if (sentRequest)
3713
+ return;
3714
+ sentRequest = true;
3715
+ try {
3716
+ popup.postMessage(request, signerOrigin);
3717
+ }
3718
+ catch ( /* popup gone */_a) { /* popup gone */ }
3719
+ };
3720
+ const onMsg = (ev) => {
3721
+ if (ev.origin !== signerOrigin)
3722
+ return; // only trust the signer page origin
3723
+ const d = ev.data;
3724
+ if (!d || typeof d !== 'object')
3725
+ return;
3726
+ if (d.type === 'bounded:signer:ready') {
3727
+ sendRequest();
3728
+ return;
3729
+ }
3730
+ if (d.type === 'bounded:signer:result' && d.requestId === requestId) {
3731
+ cleanup();
3732
+ try {
3733
+ popup.close();
3734
+ }
3735
+ catch ( /* ignore */_a) { /* ignore */ }
3736
+ if (d.ok)
3737
+ resolve({ hash: d.hash, explorerLink: d.explorerLink, transactionId: d.transactionId });
3738
+ else
3739
+ reject(new Error(String(d.error || 'Wallet signing failed')));
3740
+ }
3741
+ };
3742
+ window.addEventListener('message', onMsg);
3743
+ // Fallback: if the ready ping was missed (fast load / focus race), push the
3744
+ // request shortly after opening. The page ignores it until a bounded:sign
3745
+ // arrives and only acts on the first one, so a duplicate is harmless.
3746
+ setTimeout(sendRequest, 1200);
3747
+ // If the user closes the popup before completing, reject (unless a result
3748
+ // just arrived — give any in-flight message a beat to settle first).
3749
+ closeTimer = setInterval(() => {
3750
+ if (popup.closed) {
3751
+ clearInterval(closeTimer);
3752
+ setTimeout(() => { if (!settled) {
3753
+ cleanup();
3754
+ reject(new Error('Wallet approval window was closed before signing completed.'));
3755
+ } }, 400);
3756
+ }
3757
+ }, 500);
3758
+ // Overall safety timeout (OTP + signing). Generous — the user may take a
3759
+ // while to fetch the code from their email.
3760
+ setTimeout(() => { if (!settled) {
3761
+ cleanup();
3762
+ try {
3763
+ popup.close();
3764
+ }
3765
+ catch ( /* ignore */_a) { /* ignore */ }
3766
+ reject(new Error('Wallet approval timed out.'));
3767
+ } }, 5 * 60 * 1000);
3768
+ });
3769
+ }
3770
+ function toBase64(bytes) {
3771
+ return bufferExports.Buffer.from(bytes).toString('base64');
3772
+ }
3773
+ // Serialize a legacy or versioned Solana transaction to base64 for the signer
3774
+ // page. We do NOT require existing signatures (the smart wallet signs on the
3775
+ // page); Crossmint extracts the instructions and wraps them for the smart wallet.
3776
+ function serializeTransaction(transaction) {
3777
+ const isLegacy = 'recentBlockhash' in transaction &&
3778
+ !('message' in transaction && 'staticAccountKeys' in transaction.message);
3779
+ const bytes = isLegacy
3780
+ ? transaction.serialize({ requireAllSignatures: false, verifySignatures: false })
3781
+ : transaction.serialize();
3782
+ return toBase64(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
3783
+ }
3784
+ /**
3785
+ * Whether the active session carries an embedded wallet (so the provider should
3786
+ * route signing through the popup rather than throw "no wallet").
3787
+ */
3788
+ function hasEmbeddedWallet() {
3789
+ return walletClaimsFromSession() != null;
3790
+ }
3791
+ /** signAndSubmitTransaction for embedded wallets: serialize + route through the
3792
+ * signer popup, resolve with the on-chain transaction signature/hash. */
3793
+ async function embeddedSignAndSubmit(transaction) {
3794
+ const serializedTransaction = serializeTransaction(transaction);
3795
+ const result = await signViaSignerPopup({ kind: 'transaction', serializedTransaction });
3796
+ const hash = result.hash || result.transactionId;
3797
+ if (!hash)
3798
+ throw new Error('Signer returned no transaction hash.');
3799
+ return hash;
3800
+ }
3801
+
3597
3802
  // Web uses localStorage (WebSessionManager); React Native uses the configured
3598
3803
  // ReactNativeSessionManager. getActiveSessionManager() picks the right one by
3599
3804
  // whether the RN manager was configured (NOT `typeof window`, which is truthy on
3600
3805
  // RN too and would wrongly route to localStorage). Same selector the core
3601
3806
  // request path uses, so hosted Bounded Auth sessions persist + authenticate on BOTH.
3602
3807
  const sessionManager$1 = core.getActiveSessionManager;
3603
- const NO_WALLET = 'Email login is offchain — it has no Solana wallet and cannot sign/submit ' +
3604
- 'transactions. Use a wallet provider for onchain actions, or link a wallet later.';
3808
+ const NO_WALLET = 'Email login has no embedded wallet. Enable `"auth": { "wallets": true }` in ' +
3809
+ 'the app policy so email logins get a non-custodial Crossmint wallet ' +
3810
+ '(@user.address), then sign from a user gesture. Otherwise use a wallet ' +
3811
+ 'provider (e.g. Phantom) for onchain actions.';
3605
3812
  // ---------------------------------------------------------------------------
3606
3813
  // token / session helpers
3607
3814
  // ---------------------------------------------------------------------------
@@ -3651,10 +3858,23 @@ class EmailAuthProvider {
3651
3858
  sessionManager$1().clearSession();
3652
3859
  setCurrentUser(null);
3653
3860
  }
3654
- async signMessage(_message) { throw new Error(NO_WALLET); }
3861
+ // Crossmint Solana smart wallets sign+submit atomically and cannot produce a
3862
+ // raw signature — signMessage/signTransaction are structurally unsupported
3863
+ // (throw the clear "use signAndSubmitTransaction" error when a wallet exists;
3864
+ // the no-wallet error otherwise). signAndSubmitTransaction routes through the
3865
+ // Bounded signer popup for wallets-enabled email logins.
3866
+ async signMessage(_message) {
3867
+ throw new Error(hasEmbeddedWallet() ? EMBEDDED_WALLET_UNSUPPORTED : NO_WALLET);
3868
+ }
3655
3869
  async runTransaction(_evm, _sol, _options) { throw new Error(NO_WALLET); }
3656
- async signTransaction(_t) { throw new Error(NO_WALLET); }
3657
- async signAndSubmitTransaction() { throw new Error(NO_WALLET); }
3870
+ async signTransaction(_t) {
3871
+ throw new Error(hasEmbeddedWallet() ? EMBEDDED_WALLET_UNSUPPORTED : NO_WALLET);
3872
+ }
3873
+ async signAndSubmitTransaction(transaction, _feePayer) {
3874
+ if (!hasEmbeddedWallet())
3875
+ throw new Error(NO_WALLET);
3876
+ return embeddedSignAndSubmit(transaction);
3877
+ }
3658
3878
  async getNativeMethods() { return {}; }
3659
3879
  }
3660
3880
  const emailProviderSingleton = new EmailAuthProvider();
@@ -3685,6 +3905,11 @@ function resolveSolanaRpcUrl(explicitRpcUrl, network, context) {
3685
3905
  throw new Error(`${context} requires an explicit Solana RPC URL. Pass config.rpcUrl before signing or submitting onchain transactions.`);
3686
3906
  }
3687
3907
 
3908
+ // Same message the email provider uses when a hosted (OIDC) login has no
3909
+ // embedded wallet — enable `auth.wallets` in policy to get one.
3910
+ const OIDC_NO_WALLET = 'This login has no embedded wallet. Enable `"auth": { "wallets": true }` in ' +
3911
+ 'the app policy so logins get a non-custodial Crossmint wallet (@user.address), ' +
3912
+ 'then sign from a user gesture.';
3688
3913
  const PKCE_STORE_KEY = 'bounded.oidc.pkce';
3689
3914
  const STORED_AUTH_METHOD_KEY$1 = 'bounded_last_auth_method';
3690
3915
  // Active session store (Web on web, RN on React Native) — selected by config,
@@ -3709,6 +3934,10 @@ function decodeJwt(token) {
3709
3934
  }
3710
3935
  }
3711
3936
  // A minimal AuthProvider so getSession()/logout() work for OIDC sessions, mirroring email.
3937
+ // Signing surface (slice 2): a hosted login on a wallets-enabled app carries an
3938
+ // embedded Crossmint wallet (@user.address). signAndSubmitTransaction routes
3939
+ // through the Bounded signer popup; signMessage/signTransaction are structurally
3940
+ // unsupported for Crossmint smart wallets (sign+submit atomically, no raw sig).
3712
3941
  const oidcProviderSingleton = {
3713
3942
  async login() {
3714
3943
  const idToken = sessionManager().getIdToken();
@@ -3719,6 +3948,17 @@ const oidcProviderSingleton = {
3719
3948
  return idToken ? userFromIdToken(idToken) : null;
3720
3949
  },
3721
3950
  async logout() { sessionManager().clearSession(); },
3951
+ async signMessage(_message) {
3952
+ throw new Error(hasEmbeddedWallet() ? EMBEDDED_WALLET_UNSUPPORTED : OIDC_NO_WALLET);
3953
+ },
3954
+ async signTransaction(_t) {
3955
+ throw new Error(hasEmbeddedWallet() ? EMBEDDED_WALLET_UNSUPPORTED : OIDC_NO_WALLET);
3956
+ },
3957
+ async signAndSubmitTransaction(transaction, _feePayer) {
3958
+ if (!hasEmbeddedWallet())
3959
+ throw new Error(OIDC_NO_WALLET);
3960
+ return embeddedSignAndSubmit(transaction);
3961
+ },
3722
3962
  };
3723
3963
  function userFromIdToken(idToken) {
3724
3964
  const c = decodeJwt(idToken);
@@ -4759,8 +4999,6 @@ async function getIdToken() {
4759
4999
  return core.getIdToken(false);
4760
5000
  }
4761
5001
 
4762
- var bufferExports = requireBuffer();
4763
-
4764
5002
  /**
4765
5003
  * Extracts a human-readable error message from Solana transaction logs.
4766
5004
  */