@bounded-sh/server 0.0.40 → 0.0.44

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @bounded-sh/server
2
2
 
3
- Server SDK for Tarobase API - Node.js/Backend implementation. This package provides functionality for server-side applications to interact with the Tarobase API using Solana keypairs for authentication.
3
+ Server SDK for the Bounded API. It provides explicit, isolated Solana-keypair or EVM-private-key clients for backend applications; no signer is loaded from ambient process state.
4
4
 
5
5
  ## Installation
6
6
 
@@ -16,7 +16,7 @@ yarn add @bounded-sh/server
16
16
 
17
17
  ## Features
18
18
 
19
- - Server-side authentication using Solana keypairs
19
+ - Server-side authentication using Solana keypairs or EVM private keys (SIWS/SIWE)
20
20
  - In-memory session management (no cookies or local storage required)
21
21
  - Support for all Tarobase data operations (get, getMany, set, query)
22
22
  - Real-time subscriptions
@@ -86,6 +86,27 @@ const signature = await wallet.signMessage("Hello, world!");
86
86
  // const signature = await wallet.signAndSubmitTransaction(transaction);
87
87
  ```
88
88
 
89
+ ### EVM Keypair Client
90
+
91
+ EVM clients require a trusted RPC and deployed `BoundedRuntime` address. A write response can select only an explicitly configured chain/runtime pair, and the SDK verifies the returned `setDocuments` calldata against the requested app, paths, actions, and values before signing.
92
+
93
+ ```typescript
94
+ import { init, createWalletClient } from '@bounded-sh/server';
95
+
96
+ await init({ appId: 'your-app-id', chain: 'base' });
97
+
98
+ const wallet = await createWalletClient({
99
+ evmPrivateKey: process.env.EVM_PRIVATE_KEY!,
100
+ chainId: 8453,
101
+ rpcUrl: process.env.BASE_RPC_URL!,
102
+ runtimeAddress: process.env.BOUNDED_RUNTIME_BASE!,
103
+ });
104
+
105
+ await wallet.set('todos/123', { text: 'Buy milk', completed: false });
106
+ ```
107
+
108
+ For deliberate multi-chain use, provide `chains: { [chainId]: { rpcUrl, runtimeAddress } }`. There is no implicit RPC, runtime address, or chain switch.
109
+
89
110
  ### Subscriptions
90
111
 
91
112
  ```typescript
@@ -117,7 +138,16 @@ function getWebhookKeysUrl(): string | undefined;
117
138
  ### Authentication
118
139
 
119
140
  ```typescript
120
- function createWalletClient(options: { keypair: string }): Promise<WalletClient>;
141
+ function createWalletClient(options:
142
+ | { keypair: string }
143
+ | {
144
+ evmPrivateKey: string;
145
+ chainId: number;
146
+ rpcUrl?: string;
147
+ runtimeAddress?: string;
148
+ chains?: Record<string, { rpcUrl: string; runtimeAddress: string; name?: string }>;
149
+ }
150
+ ): Promise<WalletClient>;
121
151
  function getAuthProvider(): Promise<AuthProvider>; // always rejects; no process-global provider
122
152
  function getIdToken(): Promise<string | null>; // always rejects; no process-global session
123
153
  ```
@@ -212,7 +242,7 @@ Please see the main repository for contribution guidelines.
212
242
 
213
243
  This server-side SDK differs from the web SDK in several important ways:
214
244
 
215
- 1. **Authentication Method**: Uses Solana keypairs instead of browser wallets
245
+ 1. **Authentication Method**: Uses explicit Solana or EVM keys instead of browser wallets
216
246
  2. **Storage Strategy**: Uses in-memory storage instead of localStorage/sessionStorage
217
247
  3. **Environment**: Optimized for Node.js/backend environments rather than browsers
218
248
  4. **Session Management**: Does not rely on browser-specific APIs
@@ -0,0 +1,33 @@
1
+ import type { AuthProvider, EVMTransaction, SolTransaction, TransactionResult, User, SetOptions } from '@bounded-sh/core';
2
+ import type { PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js';
3
+ export interface EvmKeypairChainConfig {
4
+ chainId: number;
5
+ rpcUrl: string;
6
+ runtimeAddress: string;
7
+ name?: string;
8
+ }
9
+ export declare class EvmKeypairProvider implements AuthProvider {
10
+ readonly address: string;
11
+ readonly defaultChainId: number;
12
+ private readonly account;
13
+ private readonly chains;
14
+ constructor(privateKey: string, defaultChainId: number, chainConfigs: EvmKeypairChainConfig[]);
15
+ private user;
16
+ createBoundedSession(): Promise<{
17
+ address: string;
18
+ accessToken: string;
19
+ idToken: string;
20
+ refreshToken: string;
21
+ }>;
22
+ login(): Promise<User>;
23
+ restoreSession(): Promise<User>;
24
+ logout(): Promise<void>;
25
+ signMessage(message: string): Promise<string>;
26
+ signTransaction(_transaction: Transaction | VersionedTransaction): Promise<Transaction | VersionedTransaction>;
27
+ signAndSubmitTransaction(_transaction: Transaction | VersionedTransaction, _feePayer?: PublicKey): Promise<string>;
28
+ runTransaction(evm?: EVMTransaction, sol?: SolTransaction, options?: SetOptions): Promise<TransactionResult>;
29
+ getNativeMethods(): Promise<any>;
30
+ transactionPolicy(): {
31
+ runtimeAddresses: Record<string, string>;
32
+ };
33
+ }
package/dist/index.d.ts CHANGED
@@ -8,5 +8,7 @@ export type { AuthProvider, ClientConfig, User, GetOptions, SearchOptions, SetOp
8
8
  export { getIdToken } from './utils';
9
9
  export { createWalletClient, WalletClient } from './wallet-client';
10
10
  export type { CreateWalletClientOptions } from './wallet-client';
11
+ export { EvmKeypairProvider } from './auth/providers/evm-keypair-provider';
12
+ export type { EvmKeypairChainConfig } from './auth/providers/evm-keypair-provider';
11
13
  export { verifyWebhook, clearWebhookKeyCache, clearWebhookReplayCache, WebhookVerificationError, DEFAULT_WEBHOOK_KEYS_URL, InMemoryReplayStore, } from './webhooks';
12
14
  export type { WebhookPayload, WebhookPublicKey, WebhookOperation, VerifyWebhookOptions, WebhookHeaders, WebhookReplayStore, } from './webhooks';
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ var bs58 = require('bs58');
6
6
  var buffer = require('buffer');
7
7
  var anchor = require('@coral-xyz/anchor');
8
8
  var nacl = require('tweetnacl');
9
+ var viem = require('viem');
10
+ var accounts = require('viem/accounts');
9
11
 
10
12
  function _interopNamespaceDefault(e) {
11
13
  var n = Object.create(null);
@@ -677,6 +679,144 @@ class OffchainAuthProvider {
677
679
  }
678
680
  }
679
681
 
682
+ const NO_SOLANA = 'An EVM keypair client cannot sign Solana transactions.';
683
+ function normalizeChainConfig(input) {
684
+ var _a;
685
+ if (!Number.isSafeInteger(input.chainId) || input.chainId <= 0)
686
+ throw new Error('EVM chainId must be a positive safe integer');
687
+ let rpc;
688
+ try {
689
+ rpc = new URL(input.rpcUrl);
690
+ }
691
+ catch (_b) {
692
+ throw new Error(`EVM RPC URL is invalid for chain ${input.chainId}`);
693
+ }
694
+ if (rpc.protocol !== 'https:' && !(rpc.protocol === 'http:' && (rpc.hostname === 'localhost' || rpc.hostname === '127.0.0.1' || rpc.hostname === '[::1]'))) {
695
+ throw new Error(`EVM RPC URL must use HTTPS for chain ${input.chainId}`);
696
+ }
697
+ const runtimeAddress = core.normalizeEvmAddress(input.runtimeAddress);
698
+ if (!runtimeAddress)
699
+ throw new Error(`BoundedRuntime address is invalid for chain ${input.chainId}`);
700
+ return { chainId: input.chainId, rpcUrl: rpc.toString(), runtimeAddress, name: ((_a = input.name) === null || _a === void 0 ? void 0 : _a.trim()) || `EVM ${input.chainId}` };
701
+ }
702
+ function asViemChain(config) {
703
+ return viem.defineChain({
704
+ id: config.chainId,
705
+ name: config.name,
706
+ nativeCurrency: { name: 'Native token', symbol: 'NATIVE', decimals: 18 },
707
+ rpcUrls: { default: { http: [config.rpcUrl] } },
708
+ });
709
+ }
710
+ class EvmKeypairProvider {
711
+ constructor(privateKey, defaultChainId, chainConfigs) {
712
+ this.chains = new Map();
713
+ if (!/^0x[0-9a-fA-F]{64}$/.test(privateKey))
714
+ throw new Error('Invalid EVM private key: expected 0x followed by 64 hex characters.');
715
+ this.account = accounts.privateKeyToAccount(privateKey);
716
+ const address = core.normalizeEvmAddress(this.account.address);
717
+ if (!address)
718
+ throw new Error('Could not derive a valid EVM address from the private key');
719
+ this.address = address;
720
+ for (const raw of chainConfigs) {
721
+ const chain = normalizeChainConfig(raw);
722
+ if (this.chains.has(chain.chainId))
723
+ throw new Error(`Duplicate EVM chain configuration: ${chain.chainId}`);
724
+ this.chains.set(chain.chainId, chain);
725
+ }
726
+ if (!this.chains.has(defaultChainId))
727
+ throw new Error(`Default EVM chain ${defaultChainId} is not configured`);
728
+ this.defaultChainId = defaultChainId;
729
+ }
730
+ user() {
731
+ return { id: this.address, address: null, evmAddress: this.address, provider: this };
732
+ }
733
+ async createBoundedSession() {
734
+ var _a, _b, _c;
735
+ const nonce = await core.genAuthNonce();
736
+ const { appId } = await core.getConfig();
737
+ const message = core.genSiweMessage(this.address, nonce, appId, 'server', 'https://server/login', { chainId: this.defaultChainId });
738
+ const signature = await this.account.signMessage({ message });
739
+ const session = await core.createSessionWithSignature(this.address, message, signature, 'siwe');
740
+ return {
741
+ address: this.address,
742
+ accessToken: String((_a = session.accessToken) !== null && _a !== void 0 ? _a : ''),
743
+ idToken: String((_b = session.idToken) !== null && _b !== void 0 ? _b : ''),
744
+ refreshToken: String((_c = session.refreshToken) !== null && _c !== void 0 ? _c : ''),
745
+ };
746
+ }
747
+ async login() { return this.user(); }
748
+ async restoreSession() { return this.user(); }
749
+ async logout() { }
750
+ async signMessage(message) {
751
+ return this.account.signMessage({ message });
752
+ }
753
+ async signTransaction(_transaction) {
754
+ throw new Error(NO_SOLANA);
755
+ }
756
+ async signAndSubmitTransaction(_transaction, _feePayer) {
757
+ throw new Error(NO_SOLANA);
758
+ }
759
+ async runTransaction(evm, sol, options) {
760
+ var _a;
761
+ if (evm && sol)
762
+ throw new Error('A transaction cannot target EVM and Solana at the same time.');
763
+ if (!evm || !evm.chainId || !evm.to || !evm.data)
764
+ throw new Error('A complete EVM transaction is required');
765
+ if ((options === null || options === void 0 ? void 0 : options.shouldSubmitTx) === false)
766
+ throw new Error('EVM keypair writes submit atomically; shouldSubmitTx:false is unsupported.');
767
+ const config = this.chains.get(evm.chainId);
768
+ if (!config)
769
+ throw new Error(`EVM chain ${evm.chainId} is not configured for this wallet client`);
770
+ if (core.normalizeEvmAddress(evm.to) !== config.runtimeAddress)
771
+ throw new Error('EVM transaction target is not the configured BoundedRuntime');
772
+ if (evm.nonce !== undefined)
773
+ throw new Error('Server-supplied EVM nonces are not accepted');
774
+ try {
775
+ if (BigInt((_a = evm.value) !== null && _a !== void 0 ? _a : '0') !== BigInt(0))
776
+ throw new Error('BoundedRuntime.setDocuments cannot transfer native value');
777
+ }
778
+ catch (error) {
779
+ if (error instanceof Error && error.message.includes('cannot transfer'))
780
+ throw error;
781
+ throw new Error('Invalid EVM transaction value');
782
+ }
783
+ // The shared decoder accepts only canonical ordinary/attested document
784
+ // writes and rejects selector-prefix smuggling, aliases and trailing bytes.
785
+ core.assertCanonicalBoundedRuntimeWriteCalldata(evm.data);
786
+ // A server key can safely switch between explicitly configured chains: each
787
+ // tx selects its own immutable viem client/RPC/runtime tuple. No untrusted
788
+ // server response may introduce a new chain or RPC endpoint.
789
+ const chain = asViemChain(config);
790
+ const transport = viem.http(config.rpcUrl);
791
+ const wallet = viem.createWalletClient({ account: this.account, chain, transport });
792
+ const publicClient = viem.createPublicClient({ chain, transport });
793
+ const hash = await wallet.sendTransaction({
794
+ account: this.account,
795
+ chain,
796
+ to: config.runtimeAddress,
797
+ data: evm.data,
798
+ value: BigInt(0),
799
+ });
800
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
801
+ if (receipt.status !== 'success')
802
+ throw new Error(`EVM transaction ${hash} reverted on-chain`);
803
+ if (receipt.blockNumber > BigInt(Number.MAX_SAFE_INTEGER))
804
+ throw new Error('EVM receipt block number exceeds the SDK safe-integer range');
805
+ return {
806
+ transactionSignature: hash,
807
+ blockNumber: Number(receipt.blockNumber),
808
+ gasUsed: receipt.gasUsed.toString(),
809
+ data: receipt,
810
+ };
811
+ }
812
+ async getNativeMethods() { return this.account; }
813
+ transactionPolicy() {
814
+ return {
815
+ runtimeAddresses: Object.fromEntries([...this.chains].map(([id, config]) => [String(id), config.runtimeAddress])),
816
+ };
817
+ }
818
+ }
819
+
680
820
  var __rest = (undefined && undefined.__rest) || function (s, e) {
681
821
  var t = {};
682
822
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
@@ -728,10 +868,11 @@ function jwtExpiringSoon(token) {
728
868
  /* WalletClient */
729
869
  /* ------------------------------------------------------------------ */
730
870
  class WalletClient {
731
- constructor(provider, sessionManager, address) {
871
+ constructor(provider, sessionManager, address, evmTransactionPolicy) {
732
872
  this.provider = provider;
733
873
  this.sessionManager = sessionManager;
734
874
  this.address = address;
875
+ this.evmTransactionPolicy = evmTransactionPolicy;
735
876
  this.live = {
736
877
  intent: (roomPath, intent, opts = {}) => core.live.intent(roomPath, intent, this.mergeLiveIntentOptions(opts)),
737
878
  status: (roomPath, opts = {}) => core.live.status(roomPath, Object.assign(Object.assign({}, opts), { headers: this.sanitizeHeaders(opts.headers) })),
@@ -759,9 +900,7 @@ class WalletClient {
759
900
  }
760
901
  /* ---- Auth override helpers ---- */
761
902
  buildOverrides() {
762
- return {
763
- authProvider: this.provider,
764
- _getAuthHeaders: async () => {
903
+ return Object.assign({ authProvider: this.provider, _getAuthHeaders: async () => {
765
904
  let s = await this.sessionManager.getSession();
766
905
  // Keypair-backed session: if the cached token is expired/near-expiry,
767
906
  // re-sign for a fresh one (we always hold the key). Keeps long-lived WS
@@ -772,12 +911,9 @@ class WalletClient {
772
911
  s = await this.sessionManager.getSession();
773
912
  }
774
913
  return { Authorization: `Bearer ${s.idToken}` };
775
- },
776
- _clearAuth: async () => {
914
+ }, _clearAuth: async () => {
777
915
  this.sessionManager.clearSession();
778
- },
779
- _walletAddress: this.address,
780
- };
916
+ }, _walletAddress: this.address }, (this.evmTransactionPolicy ? { _evmTransactionPolicy: this.evmTransactionPolicy } : {}));
781
917
  }
782
918
  /** Strip Authorization from caller headers to prevent overriding wallet token */
783
919
  sanitizeHeaders(headers) {
@@ -789,7 +925,7 @@ class WalletClient {
789
925
  mergeSetOverrides(opts) {
790
926
  var _a;
791
927
  const ovr = this.buildOverrides();
792
- return Object.assign(Object.assign({}, opts), { _overrides: Object.assign(Object.assign({}, opts === null || opts === void 0 ? void 0 : opts._overrides), { headers: this.sanitizeHeaders((_a = opts === null || opts === void 0 ? void 0 : opts._overrides) === null || _a === void 0 ? void 0 : _a.headers), authProvider: ovr.authProvider, _getAuthHeaders: ovr._getAuthHeaders, _clearAuth: ovr._clearAuth }) });
928
+ return Object.assign(Object.assign({}, opts), { _overrides: Object.assign(Object.assign({}, opts === null || opts === void 0 ? void 0 : opts._overrides), { headers: this.sanitizeHeaders((_a = opts === null || opts === void 0 ? void 0 : opts._overrides) === null || _a === void 0 ? void 0 : _a.headers), authProvider: ovr.authProvider, _getAuthHeaders: ovr._getAuthHeaders, _clearAuth: ovr._clearAuth, _evmTransactionPolicy: ovr._evmTransactionPolicy }) });
793
929
  }
794
930
  mergeReadOverrides(existing) {
795
931
  const ovr = this.buildOverrides();
@@ -883,19 +1019,53 @@ class WalletClient {
883
1019
  }
884
1020
  }
885
1021
  /**
886
- * Create a self-contained wallet client bound to a specific keypair.
1022
+ * Create a self-contained wallet client bound to one Solana or EVM keypair.
887
1023
  * Each client has its own auth session — no global state is mutated.
888
1024
  *
889
1025
  * @example
890
1026
  * ```ts
891
1027
  * const vault = await createWalletClient({ keypair: process.env.VAULT_KEY });
892
1028
  * await vault.set('markets/123', data);
1029
+ *
1030
+ * const evmVault = await createWalletClient({
1031
+ * evmPrivateKey: process.env.EVM_PRIVATE_KEY,
1032
+ * chainId: 8453,
1033
+ * rpcUrl: process.env.BASE_RPC_URL,
1034
+ * runtimeAddress: process.env.BOUNDED_RUNTIME,
1035
+ * });
893
1036
  * ```
894
1037
  */
895
1038
  async function createWalletClient(opts) {
896
1039
  var _a;
897
- const kp = parseKeypair(opts.keypair);
898
1040
  const config = await core.getConfig();
1041
+ if ('evmPrivateKey' in opts && typeof opts.evmPrivateKey === 'string') {
1042
+ const chainConfigs = [];
1043
+ if (opts.chains) {
1044
+ for (const [rawId, chain] of Object.entries(opts.chains)) {
1045
+ const chainId = Number(rawId);
1046
+ if (!Number.isSafeInteger(chainId) || chainId <= 0)
1047
+ throw new Error(`Invalid EVM chain id in chains map: ${rawId}`);
1048
+ chainConfigs.push({ chainId, rpcUrl: chain.rpcUrl, runtimeAddress: chain.runtimeAddress, name: chain.name });
1049
+ }
1050
+ }
1051
+ if (opts.rpcUrl !== undefined || opts.runtimeAddress !== undefined) {
1052
+ if (!opts.rpcUrl || !opts.runtimeAddress)
1053
+ throw new Error('Single-chain EVM clients require both rpcUrl and runtimeAddress');
1054
+ if (chainConfigs.some((chain) => chain.chainId === opts.chainId))
1055
+ throw new Error(`EVM chain ${opts.chainId} is configured twice`);
1056
+ chainConfigs.push({ chainId: opts.chainId, rpcUrl: opts.rpcUrl, runtimeAddress: opts.runtimeAddress });
1057
+ }
1058
+ if (chainConfigs.length === 0)
1059
+ throw new Error('EVM wallet clients require an explicit RPC URL and BoundedRuntime address');
1060
+ const provider = new EvmKeypairProvider(opts.evmPrivateKey, opts.chainId, chainConfigs);
1061
+ const sessionManager = core.ServerSessionManager.forSessionFactory(() => provider.createBoundedSession());
1062
+ await sessionManager.getSession();
1063
+ return new WalletClient(provider, sessionManager, provider.address, provider.transactionPolicy());
1064
+ }
1065
+ if (!('keypair' in opts) || typeof opts.keypair !== 'string') {
1066
+ throw new Error('createWalletClient requires exactly one of keypair or evmPrivateKey');
1067
+ }
1068
+ const kp = parseKeypair(opts.keypair);
899
1069
  let provider = new SolanaKeypairProvider((_a = config.rpcUrl) !== null && _a !== void 0 ? _a : null, kp);
900
1070
  if (config.chain === 'offchain') {
901
1071
  provider = new OffchainAuthProvider(provider);
@@ -1193,6 +1363,7 @@ Object.defineProperty(exports, "getWebhookKeysUrl", {
1193
1363
  get: function () { return core.getWebhookKeysUrl; }
1194
1364
  });
1195
1365
  exports.DEFAULT_WEBHOOK_KEYS_URL = DEFAULT_WEBHOOK_KEYS_URL;
1366
+ exports.EvmKeypairProvider = EvmKeypairProvider;
1196
1367
  exports.InMemoryReplayStore = InMemoryReplayStore;
1197
1368
  exports.WalletClient = WalletClient;
1198
1369
  exports.WebhookVerificationError = WebhookVerificationError;