@bounded-sh/core 0.0.72 → 0.0.74

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.
@@ -1,4 +1,4 @@
1
- import { g as getConfig, b as bufferExports } from './index-P4eUuhOB.mjs';
1
+ import { g as getConfig, b as bufferExports } from './index-DXV0ETfH.mjs';
2
2
  import 'axios';
3
3
  import 'tweetnacl';
4
4
  import 'reconnecting-websocket';
@@ -149,33 +149,39 @@ class BorshCursor {
149
149
  return value >>> 0;
150
150
  }
151
151
  skip(length, field) { this.requireBytes(length, field); this.offset += length; }
152
- readString(field) {
153
- const length = this.readU32(`${field} length`);
152
+ readBytes(length, field) {
154
153
  this.requireBytes(length, field);
155
154
  const raw = this.data.slice(this.offset, this.offset + length);
156
155
  this.offset += length;
157
- return bufferExports.Buffer.from(raw).toString('utf8');
156
+ return raw;
157
+ }
158
+ readString(field) {
159
+ const length = this.readU32(`${field} length`);
160
+ return bufferExports.Buffer.from(this.readBytes(length, field)).toString('utf8');
161
+ }
162
+ readLengthPrefixedBytes(field) {
163
+ return this.readBytes(this.readU32(`${field} length`), field);
158
164
  }
159
- skipBytes(field) { this.skip(this.readU32(`${field} length`), field); }
160
165
  isAtEnd() { return this.offset === this.data.length; }
161
166
  }
162
- function skipBoundedFieldValue(cursor) {
167
+ function readBoundedFieldValue(cursor) {
163
168
  const option = cursor.readU8('operation value option');
164
169
  if (option === 0)
165
- return;
170
+ return null;
166
171
  if (option !== 1)
167
172
  throw new Error('Server transaction has malformed Bounded field value option');
168
173
  const variant = cursor.readU8('operation value variant');
169
- if (variant === 0 || variant === 1)
170
- cursor.skip(8, 'operation numeric value');
171
- else if (variant === 2)
172
- cursor.skip(1, 'operation bool value');
173
- else if (variant === 3)
174
- cursor.readString('operation string value');
175
- else if (variant === 4)
176
- cursor.skip(32, 'operation address value');
177
- else
178
- throw new Error(`Server transaction has unsupported Bounded field value variant: ${variant}`);
174
+ if (variant === 0)
175
+ return { variant: 'u64', value: readU64LE(cursor.readBytes(8, 'operation numeric value'), 0) };
176
+ if (variant === 1)
177
+ return { variant: 'i64', value: readI64LE(cursor.readBytes(8, 'operation numeric value'), 0) };
178
+ if (variant === 2)
179
+ return { variant: 'bool', value: cursor.readU8('operation bool value') !== 0 };
180
+ if (variant === 3)
181
+ return { variant: 'string', value: cursor.readString('operation string value') };
182
+ if (variant === 4)
183
+ return { variant: 'address', value: cursor.readBytes(32, 'operation address value') };
184
+ throw new Error(`Server transaction has unsupported Bounded field value variant: ${variant}`);
179
185
  }
180
186
  function normalizeOnchainPath(path) {
181
187
  let normalized = path.startsWith('/') ? path.slice(1) : path;
@@ -194,43 +200,323 @@ function parseBoundedSetDocumentsInstruction(data, label) {
194
200
  }
195
201
  const cursor = new BorshCursor(data, 8, label);
196
202
  const appId = cursor.readString('appId');
197
- const documentPaths = [];
203
+ const documents = [];
198
204
  const documentCount = cursor.readU32('documents length');
199
205
  for (let i = 0; i < documentCount; i++) {
200
- documentPaths.push(normalizeOnchainPath(cursor.readString('document path')));
206
+ const path = normalizeOnchainPath(cursor.readString('document path'));
207
+ const operations = [];
201
208
  const operationCount = cursor.readU32('operations length');
202
209
  for (let j = 0; j < operationCount; j++) {
203
- cursor.readString('operation key');
204
- skipBoundedFieldValue(cursor);
205
- cursor.skip(1, 'operation kind');
210
+ const key = cursor.readString('operation key');
211
+ const value = readBoundedFieldValue(cursor);
212
+ const kind = cursor.readU8('operation kind');
213
+ operations.push({ key, value, kind });
206
214
  }
215
+ documents.push({ path, operations });
207
216
  }
208
217
  const deletePaths = [];
209
218
  const deleteCount = cursor.readU32('delete paths length');
210
219
  for (let i = 0; i < deleteCount; i++)
211
220
  deletePaths.push(normalizeOnchainPath(cursor.readString('delete path')));
221
+ const txData = [];
212
222
  const txDataCount = cursor.readU32('txData length');
213
223
  for (let i = 0; i < txDataCount; i++) {
214
- cursor.readString('txData plugin function key');
215
- cursor.skipBytes('txData bytes');
216
- cursor.skipBytes('txData raIndices');
224
+ txData.push({
225
+ pluginFunctionKey: cursor.readString('txData plugin function key'),
226
+ payload: cursor.readLengthPrefixedBytes('txData bytes'),
227
+ raIndices: cursor.readLengthPrefixedBytes('txData raIndices'),
228
+ });
217
229
  }
218
- const simulate = cursor.readU8('simulate');
219
- if (simulate !== 0 && simulate !== 1)
230
+ const simulateByte = cursor.readU8('simulate');
231
+ if (simulateByte !== 0 && simulateByte !== 1)
220
232
  throw new Error(`${label} has malformed Bounded simulate flag`);
221
233
  if (!cursor.isAtEnd())
222
234
  throw new Error(`${label} has trailing Bounded instruction data`);
223
- return { appId, documentPaths, deletePaths };
235
+ return { appId, documents, deletePaths, txData, simulate: simulateByte === 1 };
236
+ }
237
+ // ---------------------------------------------------------------------------
238
+ // OA-0001: signing-intent binding
239
+ //
240
+ // The wallet must sign ONLY the state transition the caller actually requested.
241
+ // The expected intent below is derived from the CLIENT'S OWN write request - the
242
+ // one ground truth a tampered builder response cannot change - and the parsed
243
+ // instruction must match it field for field before any signer is invoked.
244
+ // ---------------------------------------------------------------------------
245
+ /** The program's field-operation kinds (programs/tarobase; mirrored by the worker's solana-tx-builder). */
246
+ const FIELD_OPERATION_WRITE = 0;
247
+ const FIELD_OPERATION_DELETE = 1;
248
+ const FIELD_OPERATION_TIMESTAMP = 2;
249
+ const FIELD_OPERATION_INCREMENT = 3;
250
+ // Keys the worker drops from a client write by NAME before encoding
251
+ // (realtime-worker types.ts: SYSTEM_FIELDS without the `_`-prefixed entries the
252
+ // client already strips, plus the prototype-pollution DANGEROUS_KEYS). The
253
+ // expected model must skip exactly the keys the honest builder skips, or a
254
+ // legitimate write would be rejected.
255
+ const SERVER_DROPPED_FIELD_KEYS = new Set([
256
+ 'pathId',
257
+ 'relativePath',
258
+ 'absolutePath',
259
+ '__proto__',
260
+ 'constructor',
261
+ 'prototype',
262
+ ]);
263
+ // The worker resolves {operation:'time',value:'now'} into this sentinel and the
264
+ // onchain encoder maps the sentinel to a TIMESTAMP op - including when a caller
265
+ // writes the literal string, so the expected model must mirror that quirk.
266
+ const TIME_NOW_SENTINEL = '__TIME_NOW__';
267
+ /**
268
+ * Build the expected write intent from the exact request the client POSTed
269
+ * (`[{ destinationPath, document }]`, a `null` document being a delete).
270
+ *
271
+ * Each document is JSON-round-tripped first: the server parsed the SERIALIZED
272
+ * body, so the intent must be derived from the same bytes - this drops
273
+ * `undefined` values, turns `NaN`/`Infinity` into `null`, and normalizes key
274
+ * order exactly as the server's JSON.parse did.
275
+ *
276
+ * Operation order mirrors the worker's transform (normalizeWriteBody ->
277
+ * resolveOperationFields -> documentToOperations): plain fields in request key
278
+ * order, then increment fields appended in the order they appeared. The
279
+ * comparison is therefore ORDERED, not a multiset - the honest builder is
280
+ * deterministic given the request, and ordering additionally catches
281
+ * duplicate-path and reordering tampering that the retired set comparison hid.
282
+ */
283
+ function buildExpectedWriteIntent(documents) {
284
+ const intentDocuments = [];
285
+ const deletePaths = [];
286
+ for (const entry of documents) {
287
+ if (entry.document == null) {
288
+ deletePaths.push(entry.destinationPath);
289
+ continue;
290
+ }
291
+ if (typeof entry.document !== 'object') {
292
+ // The worker 400s a scalar document, so no honest 202 can exist for this
293
+ // request. Throw here rather than let a dishonest 202 be compared against
294
+ // an intent this entry never had.
295
+ throw new Error(`Cannot derive the expected write intent for ${entry.destinationPath}: the request document is not an object`);
296
+ }
297
+ const wireDocument = JSON.parse(JSON.stringify(entry.document));
298
+ const operations = [];
299
+ const increments = [];
300
+ for (const [key, value] of Object.entries(wireDocument)) {
301
+ if (key.startsWith('_') || SERVER_DROPPED_FIELD_KEYS.has(key))
302
+ continue;
303
+ if (value === null) {
304
+ operations.push({ key, kind: 'delete' });
305
+ continue;
306
+ }
307
+ if (typeof value === 'object' && !Array.isArray(value) &&
308
+ typeof value.operation === 'string' && value.operation.length > 0) {
309
+ const operation = value.operation;
310
+ if (operation === 'increment' && typeof value.value === 'number') {
311
+ increments.push({ key, delta: value.value });
312
+ continue;
313
+ }
314
+ if (operation === 'time' && value.value === 'now') {
315
+ operations.push({ key, kind: 'timestamp' });
316
+ continue;
317
+ }
318
+ // Any other {operation,...} shape is rejected by the worker with a 400,
319
+ // so it has no honest onchain encoding. Falling through to a plain write
320
+ // of the raw object keeps the comparison fail-closed: a 202 can only
321
+ // arrive from a dishonest builder, and no instruction can match this.
322
+ }
323
+ if (value === TIME_NOW_SENTINEL) {
324
+ operations.push({ key, kind: 'timestamp' });
325
+ continue;
326
+ }
327
+ operations.push({ key, kind: 'write', value });
328
+ }
329
+ for (const { key, delta } of increments) {
330
+ operations.push({ key, kind: 'increment', delta });
331
+ }
332
+ intentDocuments.push({ path: entry.destinationPath, operations });
333
+ }
334
+ return { documents: intentDocuments, deletePaths };
335
+ }
336
+ /**
337
+ * Minimal base58 decoder, used ONLY to recognize a declared-Address field: a
338
+ * request string that decodes to exactly the 32 bytes the instruction carries
339
+ * is a faithful AddressVal encoding of that string. Returns null on any
340
+ * non-base58 character.
341
+ */
342
+ function decodeBase58(text) {
343
+ if (text.length === 0)
344
+ return null;
345
+ let value = BigInt(0);
346
+ for (const ch of text) {
347
+ const digit = BASE58_ALPHABET.indexOf(ch);
348
+ if (digit < 0)
349
+ return null;
350
+ value = value * BigInt(58) + BigInt(digit);
351
+ }
352
+ // Leading '1' characters are leading zero bytes, not part of the number.
353
+ let leadingZeros = 0;
354
+ while (leadingZeros < text.length && text[leadingZeros] === BASE58_ALPHABET[0])
355
+ leadingZeros++;
356
+ const body = [];
357
+ if (value > BigInt(0)) {
358
+ let hex = value.toString(16);
359
+ if (hex.length % 2 === 1)
360
+ hex = `0${hex}`;
361
+ for (let i = 0; i < hex.length; i += 2)
362
+ body.push(parseInt(hex.slice(i, i + 2), 16));
363
+ }
364
+ const out = new Uint8Array(leadingZeros + body.length);
365
+ out.set(body, leadingZeros);
366
+ return out;
224
367
  }
225
- function assertSamePathSet(label, expectedPaths, actualPaths) {
226
- const expected = new Set(expectedPaths.map(normalizeOnchainPath).filter(Boolean));
227
- const actual = new Set(actualPaths.map(normalizeOnchainPath).filter(Boolean));
228
- const missing = [...expected].filter(path => !actual.has(path));
229
- const extra = [...actual].filter(path => !expected.has(path));
230
- if (missing.length || extra.length) {
231
- const details = [missing.length ? `missing paths: ${missing.join(', ')}` : '', extra.length ? `unexpected paths: ${extra.join(', ')}` : '']
232
- .filter(Boolean).join('; ');
233
- throw new Error(`${label} Bounded instruction paths do not match requested write paths (${details})`);
368
+ /**
369
+ * Whether `parsed` is an encoding the HONEST builder could have produced for
370
+ * the request field value. The onchain FieldValue variant is chosen by the
371
+ * collection's DECLARED schema (worker-side `jsValueToFieldValue`), which the
372
+ * client does not hold, so every variant any declared type could yield for this
373
+ * JSON value is accepted - and nothing else:
374
+ * - boolean: BoolVal only.
375
+ * - integer number/bigint: U64Val (inference/UInt; UInt takes abs for
376
+ * negatives) or I64Val (Int), of the exact integer.
377
+ * - fractional number: StringVal of its decimal text (inference/Float), or a
378
+ * truncated U64/I64 (declared UInt/Int truncates toward zero).
379
+ * - string: StringVal verbatim, AddressVal when it is the base58 of the exact
380
+ * 32 bytes (declared Address), or U64/I64 of its integer text (declared
381
+ * UInt/Int accept numeric strings, truncating any fraction).
382
+ * - array/object: StringVal of its JSON serialization (identical on both sides
383
+ * of the wire for the round-tripped value).
384
+ */
385
+ function isFaithfulFieldValueEncoding(requestValue, parsed) {
386
+ if (parsed === null)
387
+ return false;
388
+ if (typeof requestValue === 'boolean') {
389
+ return parsed.variant === 'bool' && parsed.value === requestValue;
390
+ }
391
+ if (typeof requestValue === 'number') {
392
+ if (!Number.isFinite(requestValue))
393
+ return false;
394
+ if (Number.isInteger(requestValue)) {
395
+ const exact = BigInt(requestValue);
396
+ if (parsed.variant === 'i64' && parsed.value === exact)
397
+ return true;
398
+ // The declared-UInt encoder stores abs(value), so a negative request
399
+ // integer can honestly arrive as its positive magnitude.
400
+ return parsed.variant === 'u64' && parsed.value === (exact < BigInt(0) ? -exact : exact);
401
+ }
402
+ if (parsed.variant === 'string' && parsed.value === String(requestValue))
403
+ return true;
404
+ const truncated = BigInt(Math.trunc(requestValue));
405
+ if (parsed.variant === 'i64' && parsed.value === truncated)
406
+ return true;
407
+ return parsed.variant === 'u64' && parsed.value === (truncated < BigInt(0) ? -truncated : truncated);
408
+ }
409
+ if (typeof requestValue === 'string') {
410
+ if (parsed.variant === 'string' && parsed.value === requestValue)
411
+ return true;
412
+ if (parsed.variant === 'address') {
413
+ const decoded = decodeBase58(requestValue);
414
+ const expectedBytes = parsed.value;
415
+ if (decoded !== null && decoded.length === 32 && decoded.every((byte, i) => byte === expectedBytes[i]))
416
+ return true;
417
+ }
418
+ if (parsed.variant === 'u64' || parsed.variant === 'i64') {
419
+ const trimmed = requestValue.trim();
420
+ const intPart = trimmed.includes('.') ? trimmed.split('.')[0] : trimmed;
421
+ if (/^-?\d+$/.test(intPart)) {
422
+ const numeric = BigInt(intPart);
423
+ if (parsed.variant === 'i64' && parsed.value === numeric)
424
+ return true;
425
+ if (parsed.variant === 'u64' && parsed.value === (numeric < BigInt(0) ? -numeric : numeric))
426
+ return true;
427
+ }
428
+ }
429
+ return false;
430
+ }
431
+ if (requestValue !== null && typeof requestValue === 'object') {
432
+ return parsed.variant === 'string' && parsed.value === JSON.stringify(requestValue);
433
+ }
434
+ return false;
435
+ }
436
+ const EXPECTED_KIND_TO_WIRE = {
437
+ write: FIELD_OPERATION_WRITE,
438
+ delete: FIELD_OPERATION_DELETE,
439
+ timestamp: FIELD_OPERATION_TIMESTAMP,
440
+ increment: FIELD_OPERATION_INCREMENT,
441
+ };
442
+ function describeParsedValue(value) {
443
+ if (value === null)
444
+ return 'no value';
445
+ if (value.variant === 'address')
446
+ return `address 0x${bufferExports.Buffer.from(value.value).toString('hex')}`;
447
+ if (value.variant === 'u64' || value.variant === 'i64')
448
+ return `${value.variant} ${value.value.toString()}`;
449
+ return `${value.variant} ${JSON.stringify(value.value)}`;
450
+ }
451
+ /**
452
+ * OA-0001: the one Bounded set-documents instruction in a server-built
453
+ * transaction must encode EXACTLY the requested write - app id, ordered
454
+ * documents, per-field operation key/kind/value, ordered deletes, and a clear
455
+ * simulate flag - or the transaction is rejected BEFORE the wallet sees it.
456
+ *
457
+ * The comparison is ordered because the honest builder (realtime-worker's
458
+ * solana-tx-builder) preserves the request's document/delete/operation order
459
+ * deterministically; nothing legitimate reorders, and an ordered check catches
460
+ * duplicate-path and reorder tampering the retired path-SET comparison hid.
461
+ *
462
+ * Deliberately NOT bound here: the plugin `txData` entries and the remaining
463
+ * accounts their ra_indices reference. Those are derived by server-side policy
464
+ * hooks (a Jupiter quote, an ATA layout), not by the caller's request, so the
465
+ * client holds no request-derived expectation to compare against; that surface
466
+ * stays bound by the onchain policy, the typed setup-instruction guards, and
467
+ * the attestation co-signer the program demands for any non-empty txData.
468
+ */
469
+ function assertMatchesExpectedIntent(label, expectedAppId, expected, parsed) {
470
+ var _a;
471
+ if (parsed.appId !== expectedAppId)
472
+ throw new Error(`${label} Bounded instruction appId does not match configured appId`);
473
+ if (parsed.simulate) {
474
+ throw new Error(`${label} Bounded instruction has the simulate flag set; a client-signed write must be a real execution`);
475
+ }
476
+ if (parsed.deletePaths.length !== expected.deletePaths.length ||
477
+ parsed.deletePaths.some((path, i) => path !== normalizeOnchainPath(expected.deletePaths[i]))) {
478
+ throw new Error(`${label} Bounded instruction delete paths do not match the requested deletes`);
479
+ }
480
+ if (parsed.documents.length !== expected.documents.length) {
481
+ throw new Error(`${label} Bounded instruction document count does not match the requested write ` +
482
+ `(expected ${expected.documents.length}, got ${parsed.documents.length})`);
483
+ }
484
+ for (let i = 0; i < expected.documents.length; i++) {
485
+ const expectedDoc = expected.documents[i];
486
+ const actualDoc = parsed.documents[i];
487
+ const expectedPath = normalizeOnchainPath(expectedDoc.path);
488
+ if (actualDoc.path !== expectedPath) {
489
+ throw new Error(`${label} Bounded instruction document path "${actualDoc.path}" does not match requested path "${expectedPath}"`);
490
+ }
491
+ if (actualDoc.operations.length !== expectedDoc.operations.length) {
492
+ throw new Error(`${label} Bounded instruction operations for path "${expectedPath}" do not match the requested write ` +
493
+ `(expected ${expectedDoc.operations.length} operations, got ${actualDoc.operations.length})`);
494
+ }
495
+ for (let j = 0; j < expectedDoc.operations.length; j++) {
496
+ const expectedOp = expectedDoc.operations[j];
497
+ const actualOp = actualDoc.operations[j];
498
+ if (actualOp.key !== expectedOp.key || actualOp.kind !== EXPECTED_KIND_TO_WIRE[expectedOp.kind]) {
499
+ throw new Error(`${label} Bounded instruction operation ${j} on path "${expectedPath}" does not match the requested write ` +
500
+ `(expected key "${expectedOp.key}" kind ${expectedOp.kind}, got key "${actualOp.key}" kind ${actualOp.kind})`);
501
+ }
502
+ if (expectedOp.kind === 'delete' || expectedOp.kind === 'timestamp') {
503
+ if (actualOp.value !== null) {
504
+ throw new Error(`${label} Bounded instruction operation "${expectedOp.key}" on path "${expectedPath}" carries an unexpected value`);
505
+ }
506
+ continue;
507
+ }
508
+ if (expectedOp.kind === 'increment') {
509
+ const expectedDelta = BigInt(Math.trunc(expectedOp.delta));
510
+ if (((_a = actualOp.value) === null || _a === void 0 ? void 0 : _a.variant) !== 'i64' || actualOp.value.value !== expectedDelta) {
511
+ throw new Error(`${label} Bounded instruction increment "${expectedOp.key}" on path "${expectedPath}" does not match the requested delta`);
512
+ }
513
+ continue;
514
+ }
515
+ if (!isFaithfulFieldValueEncoding(expectedOp.value, actualOp.value)) {
516
+ throw new Error(`${label} Bounded instruction operation "${expectedOp.key}" on path "${expectedPath}" encodes a value that does not match the requested field ` +
517
+ `(got ${describeParsedValue(actualOp.value)})`);
518
+ }
519
+ }
234
520
  }
235
521
  }
236
522
  /** Read a little-endian u64 as a BigInt (lamport amounts can exceed 2^53). */
@@ -240,6 +526,11 @@ function readU64LE(bytes, offset) {
240
526
  value |= BigInt(bytes[offset + i]) << BigInt(8 * i);
241
527
  return value;
242
528
  }
529
+ /** Read a little-endian i64 as a BigInt (two's complement). */
530
+ function readI64LE(bytes, offset) {
531
+ const unsigned = readU64LE(bytes, offset);
532
+ return unsigned >= (BigInt(1) << BigInt(63)) ? unsigned - (BigInt(1) << BigInt(64)) : unsigned;
533
+ }
243
534
  /**
244
535
  * Resolve an instruction's referenced account keys against the transaction's
245
536
  * STATIC key list. A setup instruction that reaches into an address-lookup
@@ -318,14 +609,14 @@ function deserializeAndValidateServerTransaction(serializedTransaction, web3, op
318
609
  var _a;
319
610
  const { VersionedTransaction: VersionedTransactionClass, PublicKey: PublicKeyCtor } = web3;
320
611
  const transaction = VersionedTransactionClass.deserialize(bufferExports.Buffer.from(serializedTransaction, 'base64'));
321
- const { label, expectedAppId, expectedWritePaths } = options;
612
+ const { label, expectedAppId, expectedIntent } = options;
322
613
  const accountKeys = transaction.message.staticAccountKeys;
323
614
  if (accountKeys.length === 0)
324
615
  throw new Error(`${label} has no static account keys`);
325
616
  const feePayer = accountKeys[0];
326
617
  const isAccountSigner = (index) => transaction.message.isAccountSigner(index);
327
618
  let boundedInstructionCount = 0;
328
- const actualWritePaths = [];
619
+ let parsedBoundedInstruction = null;
329
620
  for (const ix of transaction.message.compiledInstructions) {
330
621
  if (ix.programIdIndex >= accountKeys.length)
331
622
  throw new Error(`${label} has program ID in lookup table (not allowed)`);
@@ -345,31 +636,38 @@ function deserializeAndValidateServerTransaction(serializedTransaction, web3, op
345
636
  }
346
637
  else if (programId === BOUNDED_PROGRAM_MAINNET || programId === BOUNDED_PROGRAM_DEVNET) {
347
638
  boundedInstructionCount++;
348
- const parsed = parseBoundedSetDocumentsInstruction(data, label);
349
- if (parsed.appId !== expectedAppId)
350
- throw new Error(`${label} Bounded instruction appId does not match configured appId`);
351
- actualWritePaths.push(...parsed.documentPaths, ...parsed.deletePaths);
639
+ parsedBoundedInstruction = parseBoundedSetDocumentsInstruction(data, label);
352
640
  }
353
641
  }
354
642
  if (boundedInstructionCount !== 1)
355
643
  throw new Error(`${label} must contain exactly one Bounded set-documents instruction`);
356
- assertSamePathSet(label, expectedWritePaths, actualWritePaths);
644
+ // OA-0001: bind the FULL operation semantics of the one Bounded instruction to
645
+ // the caller's own request before any signer runs - not just the app/path
646
+ // envelope, which a tampered builder response can keep while changing amounts.
647
+ assertMatchesExpectedIntent(label, expectedAppId, expectedIntent, parsedBoundedInstruction);
357
648
  return transaction;
358
649
  }
359
- async function handlePreBuiltTransaction(tx, authProvider, options, expectedWritePaths) {
650
+ async function handlePreBuiltTransaction(tx, authProvider, options, expectedIntent) {
360
651
  var _a, _b, _c, _d, _e, _f, _g, _h;
361
652
  const { Connection, VersionedTransaction, PublicKey } = await import('@solana/web3.js');
362
653
  // Honor a wallet client's immutable config snapshot for the expected app-id
363
654
  // binding and RPC endpoint (CTA-05); otherwise read the global init() config.
364
655
  const config = (_b = (_a = options === null || options === void 0 ? void 0 : options._overrides) === null || _a === void 0 ? void 0 : _a._config) !== null && _b !== void 0 ? _b : await getConfig();
365
656
  const transaction = deserializeAndValidateServerTransaction(tx.serializedTransaction, { VersionedTransaction, PublicKey }, {
366
- label: 'Pre-built transaction', expectedAppId: config.appId, expectedWritePaths,
657
+ label: 'Pre-built transaction', expectedAppId: config.appId, expectedIntent,
367
658
  });
368
659
  const shouldSubmit = (options === null || options === void 0 ? void 0 : options.shouldSubmitTx) !== false;
369
660
  const rpcUrl = (_c = config.rpcUrl) === null || _c === void 0 ? void 0 : _c.trim();
370
661
  const injectedRpc = (_d = options === null || options === void 0 ? void 0 : options._overrides) === null || _d === void 0 ? void 0 : _d._solanaRpc;
371
662
  if (shouldSubmit && !rpcUrl && !injectedRpc) {
372
- throw new Error(`Pre-built Solana transaction submission requires init({ rpcUrl }) for ${tx.network}`);
663
+ // Checked BEFORE the wallet signs, so a misconfigured app fails its first
664
+ // launch attempt without spending the user's signature. Deliberately no
665
+ // bundled default endpoint (2026-06-28 hardening): the app must supply the
666
+ // RPC it submits through.
667
+ throw new Error(`Pre-built Solana transaction submission requires init({ rpcUrl }) for ${tx.network}. ` +
668
+ `Pass a TOP-LEVEL rpcUrl (with chain) to init(), e.g. ` +
669
+ `init({ appId, chain: '${tx.network}', rpcUrl: '<your ${tx.network} RPC endpoint>' }); ` +
670
+ `a nested walletLogin.rpcUrl only configures wallet login and does not enable submission.`);
373
671
  }
374
672
  const signedTx = await authProvider.signTransaction(transaction);
375
673
  const rawTx = signedTx.serialize();
@@ -450,5 +748,5 @@ async function handlePreBuiltTransaction(tx, authProvider, options, expectedWrit
450
748
  };
451
749
  }
452
750
 
453
- export { deriveTransactionSignature, handlePreBuiltTransaction, isDefinitiveSolanaSendFailure };
454
- //# sourceMappingURL=solana-operations-DEWIxJS8.mjs.map
751
+ export { buildExpectedWriteIntent, deriveTransactionSignature, handlePreBuiltTransaction, isDefinitiveSolanaSendFailure };
752
+ //# sourceMappingURL=solana-operations-DiM6_8z7.mjs.map