@bounded-sh/server 0.0.66 → 0.0.68

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.js CHANGED
@@ -53,6 +53,20 @@ async function getIdToken() {
53
53
  throw new Error('getIdToken is not available as a tarobase-server top-level auth operation. Use createWalletClient({ keypair }) and call the returned client methods.');
54
54
  }
55
55
 
56
+ /**
57
+ * DS3-0414: does the transaction already carry a REAL signer signature? A Solana
58
+ * signature is computed over the compiled message, which INCLUDES recentBlockhash, so
59
+ * the blockhash must not be rewritten once any signer (sponsor / multisig / co-signer)
60
+ * has signed - a rewrite silently invalidates their signature. Legacy signatures are
61
+ * `{ signature: Buffer | null }` entries; versioned signatures are 64-byte arrays that
62
+ * stay all-zero placeholders until a signer fills them. Mirrors turnkey-signer-bridge.ts.
63
+ */
64
+ function transactionHasSignature(transaction) {
65
+ if ('serializeMessage' in transaction) {
66
+ return transaction.signatures.some((s) => s.signature != null);
67
+ }
68
+ return transaction.signatures.some((sig) => sig.some((b) => b !== 0));
69
+ }
56
70
  class SolanaKeypairProvider {
57
71
  constructor(rpcUrl, serverKeypair) {
58
72
  this.rpcUrl = rpcUrl;
@@ -109,25 +123,34 @@ class SolanaKeypairProvider {
109
123
  // 1. Get RPC URL and create connection
110
124
  const rpcUrl = this.getRpcUrl();
111
125
  const connection = new web3_js.Connection(rpcUrl, 'confirmed');
112
- // 2. Get fresh blockhash and set it on the transaction before signing
113
- const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
114
- // Use duck typing instead of instanceof to handle multiple @solana/web3.js versions
115
- const isLegacyTransaction = 'recentBlockhash' in transaction && !('message' in transaction && 'staticAccountKeys' in transaction.message);
116
- if (isLegacyTransaction) {
117
- const legacyTx = transaction;
118
- legacyTx.recentBlockhash = blockhash;
119
- legacyTx.lastValidBlockHeight = lastValidBlockHeight;
120
- // Set feePayer if not already set
121
- if (!legacyTx.feePayer) {
122
- legacyTx.feePayer = feePayer !== null && feePayer !== void 0 ? feePayer : this.keypair.publicKey;
126
+ // 2. Get fresh blockhash and set it on the transaction before signing.
127
+ // DS3-0414: a Solana signature covers the compiled message (recentBlockhash included),
128
+ // so refreshing the blockhash after a sponsor / co-signer has already signed silently
129
+ // invalidates their signature - and this is the sponsored/fee-payer submit path, where a
130
+ // co-signed transaction is exactly what arrives. signTransaction only ADDS this keypair's
131
+ // signature (partialSign), it never re-signs the others. So only refresh the blockhash
132
+ // while the transaction is still unsigned; once a real signature is present, sign and
133
+ // submit the caller's message verbatim.
134
+ if (!transactionHasSignature(transaction)) {
135
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
136
+ // Use duck typing instead of instanceof to handle multiple @solana/web3.js versions
137
+ const isLegacyTransaction = 'recentBlockhash' in transaction && !('message' in transaction && 'staticAccountKeys' in transaction.message);
138
+ if (isLegacyTransaction) {
139
+ const legacyTx = transaction;
140
+ legacyTx.recentBlockhash = blockhash;
141
+ legacyTx.lastValidBlockHeight = lastValidBlockHeight;
142
+ // Set feePayer if not already set
143
+ if (!legacyTx.feePayer) {
144
+ legacyTx.feePayer = feePayer !== null && feePayer !== void 0 ? feePayer : this.keypair.publicKey;
145
+ }
146
+ }
147
+ else {
148
+ // VersionedTransaction
149
+ const versionedTx = transaction;
150
+ versionedTx.message.recentBlockhash = blockhash;
151
+ // Note: VersionedTransaction feePayer is set in the message at creation time
152
+ // and cannot be modified after creation
123
153
  }
124
- }
125
- else {
126
- // VersionedTransaction
127
- const versionedTx = transaction;
128
- versionedTx.message.recentBlockhash = blockhash;
129
- // Note: VersionedTransaction feePayer is set in the message at creation time
130
- // and cannot be modified after creation
131
154
  }
132
155
  // 3. Sign the transaction
133
156
  const signedTx = await this.signTransaction(transaction);
@@ -333,6 +356,8 @@ class EvmKeypairProvider {
333
356
  this.defaultChainId = defaultChainId;
334
357
  }
335
358
  user() {
359
+ // @user.address (the Solana wallet) is null for an EVM keypair login: this
360
+ // principal has only an EVM address. User.address is typed `string | null`.
336
361
  return { id: this.address, address: null, evmAddress: this.address, provider: this };
337
362
  }
338
363
  async createBoundedSession(sessionConfig) {