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