Skip to content

fix: separate resource binding from participant authorization - #762

Merged
prasad-stripe merged 1 commit into
prasad-stripe/polymorphic-bindingfrom
fix/binding-identity-lift
Aug 24, 2026
Merged

prasad-stripe merged 1 commit into
prasad-stripe/polymorphic-bindingfrom
fix/binding-identity-lift

Conversation

@igrigorik

Copy link
Copy Markdown
Contributor

A token is bound to a resource and issued to a participant. Those need
different checks — the resource is a replay guard compared for exact
equality, the participant is an authorization question.

#746 moves identity out of the binding object but rule 1 still
compares it. That can't work, because identity depends on who is
calling: a business omits it when detokenizing directly, its PSP
includes it when acting on the business's behalf (platform-tokenizer
L288 and L487). Same token, two legal shapes, so equality over it
passes at most one of them.

  • Rule 1 compares type and id only, and ignores unrecognized
    members rather than rejecting or comparing them. binding is an
    open object now and nothing else defines this.
  • Rule 2: a tokenizer must not depend on binding.id resolving,
    but may check locally if it owns the resource.
  • Rule 4 (new): record the participant at mint, verify it at burn.
  • /detokenize told callers to omit identity "when it was omitted
    at tokenization"; the prose says omit it when you are the target.
    Those disagree — fixed to match the prose.
  • minLength: 1 on binding.id.

   A token is bound to a resource and issued to a participant. Those need
   different checks — the resource is a replay guard compared for exact
   equality, the participant is an authorization question.

   #746 moves `identity` out of the `binding` object but rule 1 still
   compares it. That can't work, because `identity` depends on who is
   calling: a business omits it when detokenizing directly, its PSP
   includes it when acting on the business's behalf (platform-tokenizer
   L288 and L487). Same token, two legal shapes, so equality over it
   passes at most one of them.

   - Rule 1 compares `type` and `id` only, and ignores unrecognized
     members rather than rejecting or comparing them. `binding` is an
     open object now and nothing else defines this.
   - Rule 2: a tokenizer must not *depend* on `binding.id` resolving,
     but may check locally if it owns the resource.
   - Rule 4 (new): record the participant at mint, verify it at burn.
   - `/detokenize` told callers to omit `identity` "when it was omitted
     at tokenization"; the prose says omit it when you are the target.
     Those disagree — fixed to match the prose.
   - `minLength: 1` on `binding.id`.
@igrigorik igrigorik self-assigned this Aug 21, 2026
@igrigorik igrigorik changed the title fix: binding identity lift fix: separate resource binding from participant authorization Aug 21, 2026
@damaz91 damaz91 added the status:needs-triage Signal that the PR is ready for human triage label Aug 21, 2026
prasad-stripe added a commit that referenced this pull request Aug 24, 2026
Follow-ups to #762 across the payment specification.

The payment guide still described binding as an association to a
checkout, in the Key Definitions entry, the instrument acquisition input
table, the handler authoring guidance, and the security best practice.
Binding is to a capability resource identified by `type` and `id`. The
definition also folded business identity into binding, which is now the
separate participant axis.

The platform tokenizer example needed the same split. Binding
verification now names the requesting participant rather than "caller
identity", and the requirement labelled "Identity binding" becomes
"Issued to participant". The row below it, "Resource-bound", is then the
only one using bound, which is correct.

Two tokenizer rules are adjusted from #762. Rule 1 no longer declares
that members other than `type` and `id` fall outside the replay guard: a
Tokenizer MUST NOT reject unrecognized members and MUST ignore them when
comparing, but MAY compare members defined by an extension it
implements, so an extension that scopes a binding can still mean
something. Rule 4 now states that authority for one participant to act
for another is handler-defined and outside this specification, which the
MUST otherwise leaves ungrounded.

Markdown tables reflowed to the file convention where new text changed
column widths.

Rule adjustments in response to review by @igrigorik.
@prasad-stripe
prasad-stripe merged commit 6f4e1eb into prasad-stripe/polymorphic-binding Aug 24, 2026
7 of 8 checks passed
@prasad-stripe
prasad-stripe deleted the fix/binding-identity-lift branch August 24, 2026 16:12
prasad-stripe added a commit that referenced this pull request Aug 24, 2026
Follow-ups to #762 across the payment specification.

The payment guide still described binding as an association to a
checkout, in the Key Definitions entry, the instrument acquisition input
table, the handler authoring guidance, and the security best practice.
Binding is to a capability resource identified by `type` and `id`. The
definition also folded business identity into binding, which is now the
separate participant axis.

The platform tokenizer example needed the same split. Binding
verification now names the requesting participant rather than "caller
identity", and the requirement labelled "Identity binding" becomes
"Issued to participant". The row below it, "Resource-bound", is then the
only one using bound, which is correct.

Two tokenizer rules are adjusted from #762. Rule 1 no longer declares
that members other than `type` and `id` fall outside the replay guard: a
Tokenizer MUST NOT reject unrecognized members and MUST ignore them when
comparing, but MAY compare members defined by an extension it
implements, so an extension that scopes a binding can still mean
something. Rule 4 now states that authority for one participant to act
for another is handler-defined and outside this specification, which the
MUST otherwise leaves ungrounded.

Markdown tables reflowed to the file convention where new text changed
column widths.

Rule adjustments in response to review by @igrigorik.
prasad-stripe added a commit that referenced this pull request Aug 24, 2026
* refactor!: make token binding vertical-agnostic

PR #741 moves the payment constructs to `common/` but keeps
`binding.json` in `shopping/types` because it references checkout
sessions. That leaves the tokenization handler contract, which lives in
`source/handlers/` and is horizontal by construction, requiring a
`checkout_id` on a required field of both `/tokenize` and `/detokenize`.
A non-shopping capability cannot call either endpoint without
fabricating a checkout identifier. The same file already refs the
credential from `common/`, so the asymmetry is only in binding.

Binding becomes a flat `{type, id}` pair in `common/types/binding.json`:

- `type` is the capability that owns the bound resource, refs
  `reverse_domain_name.json`, e.g. `dev.ucp.shopping.checkout`.
- `id` is the opaque identifier of that resource within the capability.

Resource scope and participant scope are now separate. `identity` moves
out of `binding` to a sibling field on the `/tokenize` and `/detokenize`
request bodies: `binding` says which resource the token is for,
`identity` says which participant it is for. `payment_identity.json`
itself is unchanged; only its position in the request moves.

The tokenization handler refs both schemas, exactly as it already refs
the base credential. One binding shape covers every capability, so no
per-vertical binding schema is needed and SDKs keep a single model.

Tokenizers need no new logic. Binding was never semantically validated
(a tokenizer cannot confirm a checkout exists), so it is an opaque
equality token. The guide now states three normative rules that make
this explicit: verification is exact equality over the binding object
and the identity presented with it, `binding.id` is opaque and MUST NOT
be parsed or resolved, and a tokenizer MUST NOT reject a request solely
because it does not recognize `binding.type`. Those rules are what close
the cross-type confusion this generalization would otherwise introduce.

BREAKING CHANGE: `https://ucp.dev/schemas/shopping/types/binding.json`
moves to `https://ucp.dev/schemas/common/types/binding.json`,
`checkout_id` is replaced by `type` plus `id`, and `identity` moves from
inside `binding` to a sibling request field. Checkout bindings become
`{"type": "dev.ucp.shopping.checkout", "id": "<checkout_id>"}`. Requesting
this rides #741's release so implementers absorb both moves at once.

Binding shape credit to @gsmith85, who proposed the flat form in review.
Identity separation credit to @raginpirate.

* fix: separate resource binding from participant authorization

   A token is bound to a resource and issued to a participant. Those need
   different checks — the resource is a replay guard compared for exact
   equality, the participant is an authorization question.

   #746 moves `identity` out of the `binding` object but rule 1 still
   compares it. That can't work, because `identity` depends on who is
   calling: a business omits it when detokenizing directly, its PSP
   includes it when acting on the business's behalf (platform-tokenizer
   L288 and L487). Same token, two legal shapes, so equality over it
   passes at most one of them.

   - Rule 1 compares `type` and `id` only, and ignores unrecognized
     members rather than rejecting or comparing them. `binding` is an
     open object now and nothing else defines this.
   - Rule 2: a tokenizer must not *depend* on `binding.id` resolving,
     but may check locally if it owns the resource.
   - Rule 4 (new): record the participant at mint, verify it at burn.
   - `/detokenize` told callers to omit `identity` "when it was omitted
     at tokenization"; the prose says omit it when you are the target.
     Those disagree — fixed to match the prose.
   - `minLength: 1` on `binding.id`.

* docs: align binding vocabulary and tighten tokenizer rules

Follow-ups to #762 across the payment specification.

The payment guide still described binding as an association to a
checkout, in the Key Definitions entry, the instrument acquisition input
table, the handler authoring guidance, and the security best practice.
Binding is to a capability resource identified by `type` and `id`. The
definition also folded business identity into binding, which is now the
separate participant axis.

The platform tokenizer example needed the same split. Binding
verification now names the requesting participant rather than "caller
identity", and the requirement labelled "Identity binding" becomes
"Issued to participant". The row below it, "Resource-bound", is then the
only one using bound, which is correct.

Two tokenizer rules are adjusted from #762. Rule 1 no longer declares
that members other than `type` and `id` fall outside the replay guard: a
Tokenizer MUST NOT reject unrecognized members and MUST ignore them when
comparing, but MAY compare members defined by an extension it
implements, so an extension that scopes a binding can still mean
something. Rule 4 now states that authority for one participant to act
for another is handler-defined and outside this specification, which the
MUST otherwise leaves ungrounded.

Markdown tables reflowed to the file convention where new text changed
column widths.

Rule adjustments in response to review by @igrigorik.

* Update docs/specification/payment/tokenization.md

Co-authored-by: Ilya Grigorik <ilya@grigorik.com>

* docs: use generic binding vocabulary in encrypted credential example

The encrypted credential handler embeds binding inside its own encrypted
payload rather than calling /tokenize or /detokenize, but the payload is
still carrying the shopping-bound checkout_id. Align it with the binding
placement guidance in template.md, which now says the binding object is
what belongs inside the credential payload.

Reported by @amithanda.

---------

Co-authored-by: Ilya Grigorik <ilya@grigorik.com>
@waynemak waynemak added the area:payments Issues and pull requests related to the Payments vertical label Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:payments Issues and pull requests related to the Payments vertical status:needs-triage Signal that the PR is ready for human triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants