Skip to content

operation-binding companion extension and TS prototype#1932

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

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.

@nutstrut
Copy link
Copy Markdown

nutstrut commented Apr 5, 2026

The operation-binding extension fills the gap between payment and execution cleanly. The operationDigest approach — RFC 8785 canonicalization, SHA-256 digest, deterministic binding — is exactly the right primitive for that layer.

One composition point worth flagging for the spec: the operation-binding receipt answers "was this exact validated operation paid for?" but doesn't address what comes after execution — whether the delivery matched the specification.

SAR (Settlement Attestation Receipt, #1195) is designed to fill that slot. It operates post-execution and produces a signed delivery proof using the same verification profile: RFC 8785 canonicalization, SHA-256 digest derivation, Ed25519 signatures, kid-indexed key discovery. The signed core is intentionally narrow so it composes with operation-binding without coupling envelope semantics.

The full stack would then be:

operationDigest (payment bound to exact operation) → execution → SAR (delivery bound to spec) → reputation signal

On the RFC 8785 escaping issue Bortlesboat identified — SAR's canonicalization implementation uses correct short-form escapes for \b \f \n \r \t per §3.2.2.2. The sar-sdk fixture suite includes vectors covering the \n and \t cases (V4 and V5 in Bortlesboat's table). Happy to share those vectors if useful for cross-validation.

For the shared canonicalization test vector appendix Bortlesboat proposed — aligning SAR fixtures with the operation-binding vector set would give independent implementers a single target to validate both layers against.

@ayushozha
Copy link
Copy Markdown
Contributor Author

@phdargen looping you in for a maintainer read here.

@Bortlesboat's feedback makes me think the companion-extension / first-slice approach is directionally right. Since then this branch has also incorporated the RFC 8785 escaping fix, pinned digest regression vectors, and a small HTTP-flow integration test around operation-binding.

Is this aligned with where you want operation-binding to head? If yes, would you want this merged as the initial prototype and iterate follow-up spec/runtime work in separate PRs, or would you prefer it narrowed or split further before merge?

@ayushozha
Copy link
Copy Markdown
Contributor Author

@Bortlesboat since you’ve already been the main technical reviewer on this thread: with the RFC 8785 escape fix, pinned digest vectors, and the small HTTP-flow integration coverage now on the branch, does this look like the right initial prototype to merge and iterate on in follow-up PRs, or would you prefer it narrowed or split further first?

@ayushozha ayushozha marked this pull request as ready for review April 6, 2026 03:40
@Bortlesboat
Copy link
Copy Markdown
Contributor

Bortlesboat commented Apr 6, 2026

Escape fix looks correct, vectors match. This is the right shape to merge and iterate.

Only thing I'd track for a follow-up is a Test Vectors appendix in the spec doc itself the TS test suite has them pinned, but SDK authors in other languages will want a bit-for-bit target without having to dig through the TypeScript.

Merge it.

@ayushozha ayushozha changed the title draft: operation-binding companion extension and TS prototype operation-binding companion extension and TS prototype Apr 6, 2026
@ayushozha
Copy link
Copy Markdown
Contributor Author

ayushozha commented Apr 6, 2026

Thanks, that’s very helpful, @Bortlesboat. I’ll keep the Test Vectors appendix as follow-up work so this PR stays narrow. At this point the branch looks stable and the remaining GitHub blocker appears to be a formal approving review from someone with merge rights.

@ayushozha
Copy link
Copy Markdown
Contributor Author

@phdargen @CarsonRoscoe could you take a look at this when you have a moment?

Given @Bortlesboat's review that this is the right first shape to merge and iterate on, I’d mainly like to confirm whether this is aligned with the direction you want operation-binding to head. If yes, I’m happy to keep the Test Vectors appendix and any broader follow-up work in separate PRs.

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.

3 participants