feat(cart): Embedded protocol transport binding for cart capability + reauth mechanism in ECP#244
Conversation
lemonmade
left a comment
There was a problem hiding this comment.
Left a few small nits and questions, but I am really liking how this has shaped up, especially the symmetry with Embedded Checkout 👍 This PR does still have one Cart-only message, ect.line_items.change_request; do we think this same feature makes sense in embedded checkout?
| - **Result Payload:** | ||
| - `authorization` (string, **REQUIRED**): The requested authorization data, | ||
| can be in the form of an OAuth token, JWT, API keys, etc. | ||
| - `checkout` (object, **REQUIRED**): An optional checkout holding the last known state to the host. |
There was a problem hiding this comment.
Why is this needed? The embedded checkout should know the last checkout it sent before this message.
| "id": "auth_1", | ||
| "method": "ec.auth", | ||
| "params": { | ||
| "type": "oauth" |
There was a problem hiding this comment.
What are the allowed types here?
| - **Direction:** host → Embedded Checkout | ||
| - **Type:** Response | ||
| - **Result Payload:** | ||
| - `authorization` (string, **REQUIRED**): The requested authorization data, |
There was a problem hiding this comment.
IMO, authorization reads a little weirdly for a message called ec.auth. What about using token here, or credential, which mirrors the name for a similar concept in payment instruments?
| ``` | ||
|
|
||
| If the ingestion of the authorization is not successful, Embedded Checkout **MAY** | ||
| re-initiate this request with the host again. |
There was a problem hiding this comment.
Can we should how a Host would indicate failure, and what an Embedded Checkout should do with failures that come up?
| in the cart interface. These are informational only. The cart has | ||
| already applied the changes and rendered the updated UI. | ||
|
|
||
| #### `ect.line_items.change` |
There was a problem hiding this comment.
I think we are missing documentation for ect.line_items.change_request.
igrigorik
left a comment
There was a problem hiding this comment.
Overall 👍 on the feature. A couple authoring and technical flags to jam through...
Why are adding ec.auth / ect.auth?
I understand the reauth scenario (token expiry mid-session) but this feels unlikely and marginal. Why was this not an issue for checkout and problem for cart? I would separate this discussion and functionality out from this PR.
Do we really need config.capabilities?
UCP already has a capability negotiation model: discovery (ucp.capabilities) → per-session intersection (response envelope) → per-message handshake. This PR adds a fourth layer — a capability registry nested inside the embedded transport config.
I think the per-response pattern is sufficient here. When a host creates a cart, it gets back ucp.services[embedded] in the response: it knows ECaP is available for this cart. When you're ready to transition to checkout, checkout response will tell you if embedded checkout is available — all of this works already. The response context disambiguates which capability the embedded transport applies to.
I don't think we need another discovery mechanism here. More on this below...
Complete semantics are underspecified
ec.complete in checkout means "order placed" — it's terminal. ect.complete in cart means "buyer wants to proceed to checkout", which is a transition signal. Same name, different semantics.
Conceptually what we're modelling is closer to ec.payment.credential_request: buyer taps "pay now," business requests what it needs from the host, then proceeds. I think complete is workable but something to think about.
Should we model transition explicitly? As a napkin sketch...
{
"method": "ec.cart.complete",
"params": {
"cart": { "id": "cart_123", ... },
"transition": { // or navigation, ..., whichever
"checkout": {
"url": "https://business.com/checkout/from-cart/cart_123",
"delegate": ["payment.credential", "fulfillment.address_change"]
}
}
}
}If transition.checkout is present: business is advertising that it offers embedded checkout at this URL with these delegations. In this contract, host is always in control. It can use the offered transition URL, append ec_* params, navigate current or new frame/webview, or ignore the offer and run create checkout via API call + use native flow. Business offers, host chooses.
This also allows host to control whether it wants to reuse current frame/webview vs. spin up new one -- for example, if it wants to control transition between states. It also eliminates the need for config.capabilities: the transition advertisement IS the per-session embedded checkout signal, delivered at exactly the moment it matters.
There is now a lot of duplicated boilerplate across EC/ECT
Let's think how we can extract and refactor duplicated spec content and schema to ensure we have consistency across these protocols.
To that end, why are redefining same and common mechanisms (e.g. auth parameters) with different names? As an example, ec_auth should be consistent and reusable primitive across all flows; ec_auth is the "embeded commerce auth token". URL params configure the embedded session; they don't care whether you're embedding a cart or a checkout. We shouldn't need to redefine these and create opportunities to deviate across capabilities. This simplifies the implementation dramatically as well. Same argument applies to delegate, color_scheme, etc.
Let's extract the common set of in a shared spec. Then checkout, cart, and whatever other embedded modes we invent, can build on top with just the unique bits they need.
ECT... 😅
Bikeshed: why "T", it's never explained?
In retrospect, maybe we should have namespaced (ec.{capability}.{action}), and should we consider that here and now? Today we have checkout and adding cart, next we'll invent orders (eo?). Instead of proliferating opaque prefixes: ec.cart.{...}, ...?
Embedded Checkout Protocol (ECP) is a checkout-specific implementation of UCP's Embedded Protocol (EP) transport binding that enables a host to embed a business's checkout interface
This is a mess. Here's how I'd clean it up...
Embedded Commerce Protocol (ECP) is a transport binding that enables a host to embed a business capability (e.g. cart, checkout), receive events as buyer interacts, and request and process delegated buyer actions.
In other words, "ECP" is the embedded protocol of UCP. It supports multiple capabilities, and provides shared primitives (auth, delegate, etc), on which capabilities scaffold their specialized request/response semantics.
I recognize that I'm pointing to a larger refactor, but now is the right time to do it as we go from n=1 to multiple embedded capabilities. Happy to help with the refactor.
| ```json | ||
| { | ||
| "jsonrpc": "2.0", | ||
| "method": "ec.messages.change", |
There was a problem hiding this comment.
Should be ect.messages.change?
| in the cart interface. These are informational only. The cart has | ||
| already applied the changes and rendered the updated UI. | ||
|
|
||
| #### `ect.line_items.change` |
There was a problem hiding this comment.
Echoing @lemonmade: ect.line_items.change_request is defined in embedded.openrpc.json but is not documented. Also: ect.context.change is defined in the OpenRPC but missing from the spec.
| - **Result Payload:** | ||
| - `authorization` (string, **REQUIRED**): The requested authorization data, | ||
| can be in the form of an OAuth token, JWT, API keys, etc. | ||
| - `cart` (object, **REQUIRED**): An optional cart holding the last known state to the host. |
There was a problem hiding this comment.
Contradiction: cart is declared (object, **REQUIRED**) in the payload description, but the text says "An optional cart holding the last known state."
| } | ||
| ``` | ||
|
|
||
| ### Loading an Embedded Checkout URL |
There was a problem hiding this comment.
Should be "Loading an Embedded Cart URL"?
| completed cart. | ||
|
|
||
| - **Direction:** Embedded Cart → host | ||
| - **Type:** Request |
There was a problem hiding this comment.
ect.complete is documented as a Request (with id: "transition_1"), but in embedded.openrpc.json it has no result schema — which is the notification pattern.
For comparison, ec.complete in ECP is a notification. If ECaP's ect.complete should be a request (which makes sense for the cart→checkout transition handshake), the OpenRPC needs a result schema defining what the host returns (e.g., acknowledgment, checkout initiation data).
|
|
||
| ### Handshake Messages | ||
|
|
||
| #### `ect.ready` |
There was a problem hiding this comment.
Ditto, this is same handshake as ec.ready: can we extract this and just note the name difference?
|
|
||
| ### Authentication | ||
|
|
||
| #### `ect.auth` |
There was a problem hiding this comment.
- Reauth: Certain authentication methods (i.e. OAuth token) have strict expiration timestamps.
If a session lasted longer than the allowed duration, business can request for a refreshed
authorization to be provided by the host before the session continues
True but practically, is this.. a problem we need to solve? What is our expected TTL on these tokens?
| } | ||
| ``` | ||
|
|
||
| ### Security for Web-Based Hosts |
There was a problem hiding this comment.
Ditto, let's not replicate this. Extract to shared resource.
| - **Result Payload:** | ||
| - `authorization` (string, **REQUIRED**): The requested authorization data, | ||
| can be in the form of an OAuth token, JWT, API keys, etc. | ||
| - `checkout` (object, **REQUIRED**): An optional checkout holding the last known state to the host. |
| "type": "string", | ||
| "description": "Requested authorization. Some common examples include API key and OAuth token." | ||
| }, | ||
| "cart": { |
| } | ||
| }, | ||
|
|
||
| { |
There was a problem hiding this comment.
Idea: Consolidate to a single ect.change method.
Right now, the host has to run a JSON diff to figure out what triggered the event for several of these methods (e.g. ect.line_items.change). If the contract requires an array of mutated JSON paths alongside the full cart snapshot, we keep the idempotency of the snapshot pattern but eliminate the diffing overhead for the host.
Further, we can continue to be specific about which properties MUST provide change notification now, while providing a flexible API that will avoid needing to add many change* notification RPCs going forward.
{
"method": "ect.change",
"params": {
"changed_paths": [
"$.line_items[1].quantity",
"$.totals"
],
"cart": { ... full snapshot ... }
}
}
|
|
||
| All ECaP parameters are passed via URL query string, not HTTP headers, to ensure | ||
| maximum compatibility across different embedding environments. Parameters use | ||
| the `ect_` prefix to avoid namespace pollution and clearly distinguish ECaP |
There was a problem hiding this comment.
Per this observation from @igrigorik: #244 (review) :
In retrospect, maybe we should have namespaced (ec.{capability}.{action}), and should we consider that here and now? Today we have checkout and adding cart, next we'll invent orders (eo?). Instead of proliferating opaque prefixes: ec.cart.{...}, ...?
Even with the inconsistency it will introduce, I would take this opportunity to use more descriptive naming for the embedded APIs, rather than forward propagating usage of abbreviations.
- Anecdotally, it is noticeably harder to parse this naming scheme in the documentation.
- Abbreviated names will reduce readability in implementation code.
- Specifically,
ectis so visually similar toecthat it is begging for transposition typos 🙂.
| "name": "type", | ||
| "description": "The type of authorization business is requesting from the host.", | ||
| "type": "string", | ||
| "enum": ["api_key", "oauth"] |
There was a problem hiding this comment.
An example of this: 040684c
This is a reminder to add a blurb about avoiding enums, for forward compatibility in schema-authoring.md, I can create a PR.
| { | ||
| "name": "ect.ready", | ||
| "summary": "Handshake from business to host", | ||
| "description": "Initiates the Embedded Cart Protocol. Business declares which delegation it supports, host responds with optional channel upgrade and initial cart state.", |
There was a problem hiding this comment.
The current initialization flow requires two round trips for initialization. Is there a reason we can't support proactive auth and reduce it to one?
If the business includes a requested_auth type the Host can optionally return the token in the readyResult. This reduces UI flicker and bridge latency, especially on mobile where native-to-web serialization is expensive.
If we want to maintain symmetry this could be added to ec.ready as well?
Reopening the PR to keep commit history clean (original PR) and hopefully third-time is the charm. This will also cross-reference/carry-over comments from the old PR.
Summary
Cart capability (
dev.ucp.shopping.cart) is being introduced in #73. This adds an additional transport binding (EP) on the capability (building directly upon the referenced PR), on top of existing REST & MCP bindings. It follows the main UCP principle that "Embedded bindings should retain symmetry regardless of the capability it’s implemented for" (and hence why certain special mechanisms like auth are also being introduce to ECP to maintain symmetry).Motivation
Businesses need a way to embed their cart building UI into eligible platforms, especially when complex experiences (i.e. item recommendations & upsells) are involved during cart building. Embedded Cart Protocol (ECaP) addresses this need.
Goals
Non-Goals
Detailed Design
Key methods supported by ECaP:
ect.readyect.authect.start,ect.completeect.line_items.change,ect.buyer.change,ect.messages.changeRisks and Mitigations
servicesadvertisement. Also structurally it's also very similar to ECP so businesses already supporting ECP should be familiar with the design already.ect.auth) to exchange required authorization data instead of adding them as query parameters incontinue_urlto avoid hijacking attacks & support reauth scenarios.Graduation Criteria
Working Draft → Candidate:
Candidate → Stable:
Implementation History
TBD