@twzrd1111/clawrouter-mpp 0.4.0 → 0.5.0

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
@@ -3,6 +3,28 @@
3
3
  MPP (Machine Payments Protocol) payment rails for [ClawRouter](https://github.com/BlockRunAI/ClawRouter):
4
4
  pay an MPP seller over **Tempo** or **Solana** instead of x402, under ClawRouter's own spend limits.
5
5
 
6
+ ## Upgrading to 0.5.0 from 0.4.0
7
+
8
+ This release adds opt-in Solana Echo purchase binding (#21). Omit `echo` to preserve existing
9
+ behavior. `echo: {}` offers binding and pays unbound when a seller omits or changes the reflected
10
+ ID; `echo: { requireBinding: true }` refuses those challenges before signing.
11
+
12
+ - Each call sends fresh 16-byte buyer entropy as `X-Buyer-Nonce: <32 hex>`. A cooperating seller
13
+ reflects it as `request.externalId: "twzrd:p:<nonce>"`, which the existing SDK writes as a memo.
14
+ - With `receiptVerification` enabled, a finalized payment passing all chain checks can set
15
+ `receipt_reference_purchase_bound: true` only when exactly one SPL Memo (v1 and v2 counted
16
+ together) matches the retained buyer expectation byte for byte. Malformed UTF-8, a leading BOM,
17
+ or multiple memos cannot silently match.
18
+ - Identical complete challenges share one credential and one spend record; binding remains per
19
+ call. The local nonce is not part of the shared-signing cache key.
20
+ - Fulfillment schema stays `twzrd.mpp.fulfillment.v3`; core peer floor stays `>=0.12.309`.
21
+ `response_bytes` and `body_sha256` are existing fields, first shipped in 0.3.0.
22
+
23
+ Seller reflection is a separate integration. This release does not deploy a seller, verify Tempo
24
+ receipts, prove model execution, or supply a durable replay index. The existing in-memory indexes
25
+ forget on restart; cross-worker uniqueness requires caller-supplied durable, atomic storage.
26
+ See the [seller wire contract](https://github.com/twzrd-sol/ClawRouter-1/blob/899295ef627032260dbff363bbb012d446c9ecc6/docs/echo-protocol.md).
27
+
6
28
  ## Upgrading to 0.4.0 from 0.3.0
7
29
 
8
30
  This release includes the standalone Solana receipt verifier (#16), fulfillment wiring (#18),
@@ -126,8 +148,10 @@ the verifier and writes its result into the row:
126
148
  - `receipt_reference_chain_verified` (`true`, `false`, or `null`), `receipt_reference_finalized_slot`,
127
149
  `receipt_reference_mismatch` (why `false`), and `receipt_reference_unverified_reason` (why `null`;
128
150
  `verifier_error` when the verifier threw).
129
- - `receipt_reference_purchase_bound` and its reason. It stays `null/no_buyer_nonce` until a seller
130
- echoes a buyer nonce; `chain_verified: true` does not mean the payment belongs to this purchase alone.
151
+ - `receipt_reference_purchase_bound` and its reason. With opt-in `echo: { requireBinding: true }`
152
+ and a cooperating seller, one exactly matching buyer memo can establish binding after chain
153
+ verification. Otherwise it remains unbound; see `docs/echo-protocol.md`. Chain verification alone
154
+ does not mean the payment belongs to this purchase alone.
131
155
  - `receipt_reference_local_acceptance` (`accepted`, `refused`, or `null`) and
132
156
  `receipt_reference_refusal_reason` (`reference_reused`, `stale_by_local_clock`): this sink's
133
157
  policy, which never changes a chain field.
package/dist/index.d.ts CHANGED
@@ -3,6 +3,18 @@ import { PayFetch, SpendControl, SpendRecord } from '@twzrd1111/clawrouter';
3
3
  export { PayFetch } from '@twzrd1111/clawrouter';
4
4
  import { KeyPairSigner } from '@solana/kit';
5
5
 
6
+ declare const MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
7
+ declare const MEMO_V1_PROGRAM_ID = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo";
8
+ type BindingInstruction = {
9
+ program: string;
10
+ data: Uint8Array;
11
+ };
12
+ type PurchaseBoundOutcome = {
13
+ purchase_bound: boolean | null;
14
+ purchase_bound_reason: "no_buyer_nonce" | "nonce_mismatch" | "chain_unverified" | null;
15
+ };
16
+ /** An expected buyer nonce binds only through one exactly matching, valid UTF-8 SPL Memo. */
17
+ declare function evaluatePurchaseBinding(instructions: readonly BindingInstruction[], expectedExternalId: string | null | undefined): PurchaseBoundOutcome;
6
18
  /** A blockhash stays valid for about 150 slots, roughly 60-90 s. */
7
19
  declare const LANDING_WINDOW_SECONDS = 90;
8
20
  declare const CLUSTER_IDS: {
@@ -112,6 +124,8 @@ type ReceiptChainVerifyInput = {
112
124
  signedAtUnix: number | null;
113
125
  /** Buyer-generated id of the logical purchase (not the seller's challenge id). */
114
126
  purchaseId: string;
127
+ /** Buyer-generated Echo expectation, retained only after exact pre-sign reflection. */
128
+ expectedExternalId?: string | null;
115
129
  };
116
130
  /** A built verifier. `cluster` lets a rail refuse a verifier configured for another cluster. */
117
131
  type ReceiptChainVerify = ((input: ReceiptChainVerifyInput) => Promise<ReceiptChainVerification>) & {
@@ -272,6 +286,15 @@ declare function createMppFetch(account: Account, base: typeof fetch | undefined
272
286
 
273
287
  declare const DEFAULT_SOLANA_USD_MINTS: ReadonlySet<string>;
274
288
  type SolanaNetwork = "mainnet" | "devnet" | "localnet";
289
+ type EchoPolicy = {
290
+ requireBinding?: boolean;
291
+ };
292
+ type EchoBindingContext = {
293
+ buyerNonce: string;
294
+ expectedExternalId: string;
295
+ };
296
+ declare function createEchoContext(): EchoBindingContext;
297
+ declare function assertEchoPreSign(challengeExternalId: unknown, context: EchoBindingContext, policy?: EchoPolicy): "bound" | "unbound";
275
298
  type SolanaMppFetchOptions = {
276
299
  signer: KeyPairSigner;
277
300
  rpcUrl: string;
@@ -287,6 +310,8 @@ type SolanaMppFetchOptions = {
287
310
  * omitted (the row's receipt_reference_* verification fields stay null). Never delays the response.
288
311
  */
289
312
  receiptVerification?: SolanaReceiptVerificationOptions;
313
+ /** Opt in to buyer nonce reflection. Omit to preserve existing seller externalId behavior. */
314
+ echo?: EchoPolicy;
290
315
  };
291
316
  type SolanaReceiptVerificationOptions = {
292
317
  /** From createReceiptChainVerifier, built for the same cluster as `network`. */
@@ -389,5 +414,5 @@ declare function fulfillPaidProof<T>(opts: {
389
414
  upstream: () => Promise<T>;
390
415
  }): Promise<T>;
391
416
 
392
- export { CreditLedger, DEFAULT_RPC_TIMEOUT_MS, DEFAULT_SOLANA_USD_MINTS, DEFAULT_TEMPO_USD_ASSETS, DEFAULT_TRUSTED_USD_MINTS, InMemoryReceiptReferenceIndex, LANDING_WINDOW_SECONDS, MPP_FULFILLMENT_MAX_BODY_BYTES, MPP_FULFILLMENT_RECORD_SCHEMA, MppFlushTimeoutError, MppGateError, MppPaidNoCompletionError, RECEIPT_REFERENCE_NOT_EVALUATED, ReplayRejectedError, SpentProofLedger, createMppFetch, createReceiptChainVerifier, createSolanaMppFetch, creditPaidNoCompletion, fulfillPaidProof, quoteFromChallenge, readCredits, readPaidCallEvidence };
393
- export type { ChainMismatch, ChainUnverifiedReason, CreditEntry, GetFinalizedTransaction, MppBodyDigest, MppFetchOptions, MppFlushOptions, MppFulfillmentRecord, MppFulfillmentSink, MppGateChallenge, MppGateDecision, MppGateEvaluator, MppIntegration, MppPayFetch, MppPaymentFacts, MppReceiptReferenceVerification, ReceiptChainVerification, ReceiptChainVerifierConfig, ReceiptChainVerify, ReceiptChainVerifyInput, ReceiptReferenceIndex, RpcTransaction, SolanaCluster, SolanaMppFetchOptions, SolanaReceiptVerificationOptions, TrustedMint };
417
+ export { CreditLedger, DEFAULT_RPC_TIMEOUT_MS, DEFAULT_SOLANA_USD_MINTS, DEFAULT_TEMPO_USD_ASSETS, DEFAULT_TRUSTED_USD_MINTS, InMemoryReceiptReferenceIndex, LANDING_WINDOW_SECONDS, MEMO_PROGRAM_ID, MEMO_V1_PROGRAM_ID, MPP_FULFILLMENT_MAX_BODY_BYTES, MPP_FULFILLMENT_RECORD_SCHEMA, MppFlushTimeoutError, MppGateError, MppPaidNoCompletionError, RECEIPT_REFERENCE_NOT_EVALUATED, ReplayRejectedError, SpentProofLedger, assertEchoPreSign, createEchoContext, createMppFetch, createReceiptChainVerifier, createSolanaMppFetch, creditPaidNoCompletion, evaluatePurchaseBinding, fulfillPaidProof, quoteFromChallenge, readCredits, readPaidCallEvidence };
418
+ export type { ChainMismatch, ChainUnverifiedReason, CreditEntry, EchoBindingContext, EchoPolicy, GetFinalizedTransaction, MppBodyDigest, MppFetchOptions, MppFlushOptions, MppFulfillmentRecord, MppFulfillmentSink, MppGateChallenge, MppGateDecision, MppGateEvaluator, MppIntegration, MppPayFetch, MppPaymentFacts, MppReceiptReferenceVerification, PurchaseBoundOutcome, ReceiptChainVerification, ReceiptChainVerifierConfig, ReceiptChainVerify, ReceiptChainVerifyInput, ReceiptReferenceIndex, RpcTransaction, SolanaCluster, SolanaMppFetchOptions, SolanaReceiptVerificationOptions, TrustedMint };
package/dist/index.js CHANGED
@@ -912,7 +912,7 @@ function createMppFetch(account, base = fetch, opts) {
912
912
  }
913
913
 
914
914
  // src/mpp-solana-fetch.ts
915
- import { randomUUID } from "crypto";
915
+ import { randomBytes, randomUUID } from "crypto";
916
916
  import {
917
917
  assertSpendPolicyAllows as assertSpendPolicyAllows2,
918
918
  SpendPolicyError as SpendPolicyError3
@@ -922,6 +922,23 @@ var DEFAULT_SOLANA_USD_MINTS = /* @__PURE__ */ new Set([
922
922
  "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
923
923
  "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
924
924
  ]);
925
+ function createEchoContext() {
926
+ const buyerNonce = randomBytes(16).toString("hex");
927
+ return { buyerNonce, expectedExternalId: `twzrd:p:${buyerNonce}` };
928
+ }
929
+ function assertEchoPreSign(challengeExternalId, context, policy = {}) {
930
+ if (challengeExternalId === void 0 || challengeExternalId === null) {
931
+ if (policy.requireBinding)
932
+ throw new SpendPolicyError3("REJECTED_UNBOUND_CHALLENGE: Seller failed to echo buyer nonce");
933
+ return "unbound";
934
+ }
935
+ if (challengeExternalId !== context.expectedExternalId) {
936
+ if (policy.requireBinding)
937
+ throw new SpendPolicyError3("SECURITY_VIOLATION: Challenge externalId mismatch");
938
+ return "unbound";
939
+ }
940
+ return "bound";
941
+ }
925
942
  var CAIP2 = {
926
943
  mainnet: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
927
944
  devnet: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
@@ -997,6 +1014,8 @@ async function createSolanaMppFetch(opts) {
997
1014
  opts.spend,
998
1015
  async () => {
999
1016
  const request = new Request(input, cleanInit);
1017
+ const echoContext = opts.echo === void 0 ? void 0 : createEchoContext();
1018
+ if (echoContext) request.headers.set("X-Buyer-Nonce", echoContext.buyerNonce);
1000
1019
  const requestForRecord = request.clone();
1001
1020
  const probe = await base(request.clone());
1002
1021
  if (probe.status !== 402) return probe;
@@ -1024,6 +1043,7 @@ async function createSolanaMppFetch(opts) {
1024
1043
  );
1025
1044
  const payment = paymentFactsFromMppChallenge(challenge);
1026
1045
  markPaymentChallenge(payment);
1046
+ const expectedExternalId = echoContext && assertEchoPreSign(challenge.request.externalId, echoContext, opts.echo) === "bound" ? echoContext.expectedExternalId : null;
1027
1047
  const key = JSON.stringify([
1028
1048
  challenge.id,
1029
1049
  challenge.realm,
@@ -1112,7 +1132,8 @@ async function createSolanaMppFetch(opts) {
1112
1132
  signedTransaction,
1113
1133
  signedAtUnix,
1114
1134
  purchaseId: randomUUID(),
1115
- hasSplits: Array.isArray(splits) && splits.length > 0
1135
+ hasSplits: Array.isArray(splits) && splits.length > 0,
1136
+ expectedExternalId
1116
1137
  };
1117
1138
  } catch (err) {
1118
1139
  if (signed) {
@@ -1159,7 +1180,9 @@ async function createSolanaMppFetch(opts) {
1159
1180
  payer: opts.signer.address,
1160
1181
  signedTransaction: result.signedTransaction,
1161
1182
  signedAtUnix: result.signedAtUnix,
1162
- purchaseId: result.purchaseId
1183
+ purchaseId: result.purchaseId,
1184
+ // Binding belongs to this call, not merely a cached credential.
1185
+ expectedExternalId: result.expectedExternalId === echoContext?.expectedExternalId ? result.expectedExternalId : null
1163
1186
  }),
1164
1187
  verification.waitMs,
1165
1188
  verification.pollMs
@@ -1248,6 +1271,23 @@ async function fulfillPaidProof(opts) {
1248
1271
 
1249
1272
  // src/receipt-chain-verify.ts
1250
1273
  var SOLANA_KIT_RANGE = "~6.10.0";
1274
+ var MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
1275
+ var MEMO_V1_PROGRAM_ID = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo";
1276
+ var MEMO_PROGRAMS = /* @__PURE__ */ new Set([MEMO_PROGRAM_ID, MEMO_V1_PROGRAM_ID]);
1277
+ function evaluatePurchaseBinding(instructions, expectedExternalId) {
1278
+ const unbound = { purchase_bound: null, purchase_bound_reason: "no_buyer_nonce" };
1279
+ const mismatch = { purchase_bound: false, purchase_bound_reason: "nonce_mismatch" };
1280
+ if (!expectedExternalId) return unbound;
1281
+ const memos = instructions.filter((ix) => MEMO_PROGRAMS.has(ix.program));
1282
+ if (memos.length === 0) return unbound;
1283
+ if (memos.length !== 1) return mismatch;
1284
+ try {
1285
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(memos[0].data);
1286
+ } catch {
1287
+ return mismatch;
1288
+ }
1289
+ return bytesEqual(memos[0].data, new TextEncoder().encode(expectedExternalId)) ? { purchase_bound: true, purchase_bound_reason: null } : mismatch;
1290
+ }
1251
1291
  var TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
1252
1292
  var TOKEN_2022_PROGRAM = "TokenzQdBNbLqP7VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
1253
1293
  var ASSOCIATED_TOKEN_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
@@ -1342,12 +1382,10 @@ function createReceiptChainVerifier(config) {
1342
1382
  async function verify(input) {
1343
1383
  const chain = await chainFacts(input);
1344
1384
  const verified = chain.receipt_reference_chain_verified;
1345
- const purchase = verified === true ? {
1346
- receipt_reference_purchase_bound: null,
1347
- receipt_reference_purchase_bound_reason: "no_buyer_nonce"
1348
- } : {
1349
- receipt_reference_purchase_bound: null,
1350
- receipt_reference_purchase_bound_reason: "chain_unverified"
1385
+ const binding = verified === true ? evaluatePurchaseBinding(chain.instructions ?? [], input.expectedExternalId) : { purchase_bound: null, purchase_bound_reason: "chain_unverified" };
1386
+ const purchase = {
1387
+ receipt_reference_purchase_bound: binding.purchase_bound,
1388
+ receipt_reference_purchase_bound_reason: binding.purchase_bound_reason
1351
1389
  };
1352
1390
  let refusal = null;
1353
1391
  if (chain.blockTime !== null && input.signedAtUnix !== null && Number.isFinite(input.signedAtUnix) && input.signedAtUnix - chain.blockTime > maxAge) {
@@ -1501,7 +1539,13 @@ function createReceiptChainVerifier(config) {
1501
1539
  ]
1502
1540
  });
1503
1541
  if (source !== expectedSource) return at(false, "payer_mismatch");
1504
- return at(true, null);
1542
+ return {
1543
+ ...at(true, null),
1544
+ instructions: message.instructions.map((ix) => ({
1545
+ program: keys[ix.programAddressIndex],
1546
+ data: new Uint8Array(ix.data ?? [])
1547
+ }))
1548
+ };
1505
1549
  }
1506
1550
  return Object.freeze(Object.assign(verify, { cluster }));
1507
1551
  }
@@ -1522,6 +1566,8 @@ export {
1522
1566
  DEFAULT_TRUSTED_USD_MINTS,
1523
1567
  InMemoryReceiptReferenceIndex,
1524
1568
  LANDING_WINDOW_SECONDS,
1569
+ MEMO_PROGRAM_ID,
1570
+ MEMO_V1_PROGRAM_ID,
1525
1571
  MPP_FULFILLMENT_MAX_BODY_BYTES,
1526
1572
  MPP_FULFILLMENT_RECORD_SCHEMA,
1527
1573
  MppFlushTimeoutError,
@@ -1530,10 +1576,13 @@ export {
1530
1576
  RECEIPT_REFERENCE_NOT_EVALUATED,
1531
1577
  ReplayRejectedError,
1532
1578
  SpentProofLedger,
1579
+ assertEchoPreSign,
1580
+ createEchoContext,
1533
1581
  createMppFetch,
1534
1582
  createReceiptChainVerifier,
1535
1583
  createSolanaMppFetch,
1536
1584
  creditPaidNoCompletion,
1585
+ evaluatePurchaseBinding,
1537
1586
  fulfillPaidProof,
1538
1587
  quoteFromChallenge,
1539
1588
  readCredits,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twzrd1111/clawrouter-mpp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "MPP (Machine Payments Protocol) payment rails for ClawRouter: Tempo and Solana charge, with spend limits and paid-failure handling",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",