Skip to content

draft: operation-binding companion extension and TS prototype#1932

Draft
ayushozha wants to merge 6 commits intox402-foundation:mainfrom
ayushozha:codex/spec-operation-binding
Draft

draft: operation-binding companion extension and TS prototype#1932
ayushozha wants to merge 6 commits intox402-foundation:mainfrom
ayushozha:codex/spec-operation-binding

Conversation

@ayushozha
Copy link
Copy Markdown
Contributor

@ayushozha ayushozha commented Apr 4, 2026

Summary

This draft PR still starts with the first narrow slice from #1921: a standalone operation-binding companion extension. It now also includes a TypeScript prototype in @x402/extensions so reviewers can react to both the proposed spec surface and a concrete SDK shape.

The goal is still to keep this first step narrow:

  • deterministic operationDigest
  • an operation-bound receipt model that composes with existing receipt / settlement work
  • a companion extension rather than changing offer-and-receipt

What This PR Includes

Spec draft

TypeScript prototype

  • new @x402/extensions/operation-binding entrypoint
  • deterministic logical-input and digest utilities
  • JWS and EIP-712 operation-receipt signing helpers
  • receipt verification helpers
  • resource-server declaration helpers for surfacing operation-binding requirements on 402 Payment Required
  • targeted tests covering canonicalization stability, digest changes, JWS/EIP-712 signing and verification, and digest mismatch detection

What This PR Does Not Yet Do

  • automatic end-to-end core settlement integration in the resource server / facilitator pipeline
  • Sign-In-With-X persistence or proof storage
  • OpenAPI wrapper generation
  • facilitator starter-kit runtime work
  • non-JSON or streaming request-body binding

For the prototype, I kept the code in @x402/extensions rather than wiring it directly into core server settlement hooks, because the current hook surface does not yet expose enough validated operation context to do that cleanly.

Validation

Ran locally:

  • pnpm --dir typescript --filter @x402/extensions test -- operation-binding.test.ts
  • pnpm --dir typescript --filter @x402/extensions build

Review Intent

This PR is meant to answer two questions:

is a separate operation-binding companion extension the right first step?
and does this TypeScript prototype look like the right shape for the first implementation slice?

If this is directionally right, I can follow up by either:

  • tightening the spec based on review, or
  • wiring the next minimal core integration path once the binding model is accepted.

Refs #1921

@github-actions github-actions bot added typescript sdk Changes to core v2 packages labels Apr 4, 2026
@ayushozha ayushozha changed the title spec: draft operation-binding companion extension draft: operation-binding companion extension and TS prototype Apr 4, 2026
@ayushozha
Copy link
Copy Markdown
Contributor Author

This draft now includes a concrete TypeScript prototype in @x402/extensions/operation-binding, not just the spec file.

Included in the prototype:

  • deterministic logical-input + operationDigest utilities
  • JWS / EIP-712 receipt creation and verification helpers
  • resource-server declaration helpers for 402 Payment Required
  • targeted tests

Validation run locally:

  • pnpm --dir typescript --filter @x402/extensions test -- operation-binding.test.ts
  • pnpm --dir typescript --filter @x402/extensions build

If this is the right first-step shape, I can keep iterating from here rather than expanding the scope further.

@Bortlesboat
Copy link
Copy Markdown
Contributor

Direction looks right to me — keeping it as a companion extension so receipt format churn doesn't cascade is the sane call.

One question on scope: the v1 receipt is resource-server-signed, which works for any scheme. But some schemes can carry the binding natively — e.g. BOLT11 description_hash can commit directly to the operationDigest, which would give you a cryptographic tie between settlement and operation without needing a separate server signature. Is that a case you'd expect to handle as a future receipt.format value ("lightning-desc-hash" or similar), or is it explicitly out of scope because it muddies the boundary with scheme-level semantics?

Not asking you to solve it here, just trying to understand whether the v1 shape would let that plug in later or whether it'd need its own extension.

@Bortlesboat
Copy link
Copy Markdown
Contributor

Ran the TS prototype on this branch against rfc8785 (pypi) on a handful of vectors. 6 of 8 match, 2 diverge, and both come back to one bug.

The canonicalize the prototype pulls from offer-receipt/signing.ts escapes every C0 control char as \uXXXX. RFC 8785 §3.2.2.2 mandates short forms for \b \f \n \r \t, so any bound string containing those hashes differently between SDKs.

Concrete: body {"query":"line1\nline2","limit":1} under the /search binding from Example B.

  • TS prototype emits "query":"line1\u000aline2"b5f30ef114cc2426389ccd9b0780503425bf4c961b630b78e3286531a8717819
  • RFC 8785 emits "query":"line1\nline2"5caac7f75db73ddc5c662458aec0f25a58f50358c68c12a17cb7925f43291223

Same story with \t. Since the spec already requires "RFC 8785 exactly, not a JCS-like variant," this reads as a prototype bug rather than a spec ambiguity. Fix is five lines in serializeString (emit short forms for those five chars) or swap to a maintained JCS library.

Test vectors

While both impls were running, pinned 8 vectors against the two example bindings (rfc8785-jcs + sha-256). V1-V3 and V6-V8 agree between the two implementations; V4 and V5 diverge per the bug above.

WEATHER = { resourceUrl: "https://api.example.com/weather/SF", method: "GET",
            pathTemplate: "/weather/:city", operationId: "weather.getCurrent",
            policyVersion: "2026-04-04",
            bindPathParams: true, bindQuery: true, bindBody: false }

SEARCH  = { resourceUrl: "https://api.example.com/search", method: "POST",
            pathTemplate: "/search", operationId: "search.run",
            policyVersion: "2026-04-04",
            bindPathParams: false, bindQuery: false, bindBody: true }
# binding components operationDigest (RFC 8785)
V1 WEATHER pathParams={city:"SF"}, query={units:"metric"} fc54afe61f8ecb553a313c317bc31785def72ac348213718235077586956fd45
V2 WEATHER pathParams={city:"SF"}, query={units:"metric",lang:"en"} 263839b2849b379b304775feb8e88724ac5b0d7f3fb418a71b197ec4a4e35618
V3 SEARCH body={query:"x402",limit:10} 44bb1eb7c73f954faf13b4996eaeb0f1c2b98a9cc5c4e741c5e25d3d21dc0a11
V4 SEARCH body={query:"line1\nline2",limit:1} 5caac7f75db73ddc5c662458aec0f25a58f50358c68c12a17cb7925f43291223
V5 SEARCH body={q:"hello\tworld",emoji:"café 🐢"} ac24f9d26314787e218aa0ae946e94a2be76e9cc63325afcca5e3c8b00a04333
V6 SEARCH with bindBody=false body={should:"be ignored"} → logical body is null 85d5bd39278b4dda2e79ed21bf0dd21e58399fc17c019283ac939e5cc5bd096b
V7 SEARCH body={max_safe:9007199254740991} (2^53-1) a1b5ce6dfecedc5adc4793aa97acf61f85107c530bf850fb3331a76f4c071177
V8 SEARCH body={s:"a\u007fb"} (DEL passes through per JCS) a73790954115b12634364b06ab3aeba5d0eaecffe0dced0daf15e9fc3f05bc9e

Would be worth pinning a subset of these in the spec as a Test Vectors appendix — independent SDK authors get a bit-for-bit target to verify against, which is the cheapest insurance against drift.

One more number-related thing

rfc8785 (Python) refuses to serialize integers outside ±(2^53−1), because RFC 8785 defers number formatting to ECMAScript Number.prototype.toString which can't represent larger ints without precision loss. Spec currently says body MAY be any JSON value. Probably worth a normative note that bound numbers MUST be within the JS safe-integer range (or represented as strings) — otherwise strict-JCS SDKs in Python/Rust/Go will throw where JS silently drops precision on 2^53 and up.

Can send a small PR against this branch with the escape fix plus regression tests for \n and \t if that's useful.

@ayushozha
Copy link
Copy Markdown
Contributor Author

Addressed the RFC 8785 escaping bug in the shared canonicalizer.

What changed:

  • serializeString in offer-receipt/signing.ts now emits the required short escapes for \b, \f, \n, \r, and \t
  • added canonicalization regression coverage in offer-receipt.test.ts
  • added exact operation-binding digest vectors for the newline and tab cases, plus a bindBody=false vector, in operation-binding.test.ts

Ran locally:

  • pnpm --dir typescript --filter @x402/extensions test -- offer-receipt.test.ts operation-binding.test.ts
  • pnpm --dir typescript --filter @x402/extensions build

This is on the branch now in 34bab63f.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sdk Changes to core v2 packages specs Spec changes or additions typescript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants