@bounded-sh/core 0.0.72 → 0.0.73

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