diff --git a/conformance/corpus/cases.json b/conformance/corpus/cases.json index bf081676..62fc6af7 100644 --- a/conformance/corpus/cases.json +++ b/conformance/corpus/cases.json @@ -151,6 +151,1143 @@ "signature": "x" } }, + { + "id": "AgentRequestAcceptance/payload/missing", + "message": "AgentRequestAcceptance", + "valid": false, + "rules": [ + "required" + ], + "json": { + "signature": "x" + } + }, + { + "id": "AgentRequestAcceptance/signature/too_short", + "message": "AgentRequestAcceptance", + "valid": false, + "rules": [ + "string.min_len" + ], + "json": { + "payload": { + "idempotency_key": "idem-tx", + "items": [ + { + "exchange": "exchange.example", + "offer_sig": "offer-signature" + } + ], + "requester_id": "agent-seed" + } + } + }, + { + "id": "AgentRequestAcceptance/valid", + "message": "AgentRequestAcceptance", + "valid": true, + "json": { + "payload": { + "idempotency_key": "idem-tx", + "items": [ + { + "exchange": "exchange.example", + "offer_sig": "offer-signature" + } + ], + "requester_id": "agent-seed" + }, + "signature": "x" + } + }, + { + "id": "AgentRequestAcceptanceItem/exchange/too_short", + "message": "AgentRequestAcceptanceItem", + "valid": false, + "rules": [ + "string.min_len" + ], + "json": { + "offer_sig": "x" + } + }, + { + "id": "AgentRequestAcceptanceItem/offer_sig/too_short", + "message": "AgentRequestAcceptanceItem", + "valid": false, + "rules": [ + "string.min_len" + ], + "json": { + "exchange": "x" + } + }, + { + "id": "AgentRequestAcceptanceItem/valid", + "message": "AgentRequestAcceptanceItem", + "valid": true, + "json": { + "exchange": "x", + "offer_sig": "x" + } + }, + { + "id": "AgentRequestAcceptancePayload/items/too_many", + "message": "AgentRequestAcceptancePayload", + "valid": false, + "rules": [ + "repeated.max_items" + ], + "json": { + "idempotency_key": "idem-tx", + "items": [ + { + "exchange": "exchange.example", + "offer_sig": "offer-signature" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + }, + { + "exchange": "x", + "offer_sig": "x" + } + ], + "requester_id": "agent-seed" + } + }, + { + "id": "AgentRequestAcceptancePayload/valid", + "message": "AgentRequestAcceptancePayload", + "valid": true, + "json": { + "idempotency_key": "idem-tx", + "items": [ + { + "exchange": "exchange.example", + "offer_sig": "offer-signature" + } + ], + "requester_id": "agent-seed" + } + }, { "id": "AuthorizedExchange/domain/killer#0", "message": "AuthorizedExchange", diff --git a/conformance/corpusgen/main.go b/conformance/corpusgen/main.go index 9d09e59e..dbdbad25 100644 --- a/conformance/corpusgen/main.go +++ b/conformance/corpusgen/main.go @@ -88,6 +88,13 @@ func seeds() map[string]proto.Message { // items field is now repeated.min_items=1 (single-offer mode removed). "Offer": offer(), "TransactionRequest": &rampv1.TransactionRequest{IdempotencyKey: "idem-tx", Items: []*rampv1.TransactionItem{{Offer: offer()}}}, + "AgentRequestAcceptancePayload": &rampv1.AgentRequestAcceptancePayload{ + Items: []*rampv1.AgentRequestAcceptanceItem{{ + OfferSig: "offer-signature", + Exchange: "exchange.example", + }}, + RequesterId: "agent-seed", IdempotencyKey: "idem-tx", + }, // ramp.admin.v1 payloads embedded (required) in the setter request/response // envelopes. RequiredFields MUST be exactly ["x"]: the repeated.unique // duplicate_item edge appends the auto-filled good item (stringSamples[0]=="x") diff --git a/docs/design-history.md b/docs/design-history.md index de5d062a..13f2b75d 100644 --- a/docs/design-history.md +++ b/docs/design-history.md @@ -40,9 +40,12 @@ agent's identity (`agent_identity_hash`) and needs no return-path relay. ## Canonical signing: JCS over proto-JSON, not deterministic protobuf -The two signed RAMP payloads that cover a protobuf message — `Offer.signature` and -the agent's `AgentAcceptance.signature` — originally covered deterministic protobuf -*binary*: marshal the message with deterministic field order, sign those bytes. We +Three signed RAMP payloads cover a protobuf message: `Offer.signature`, the +agent's `AgentAcceptance.signature`, and the agent's +`AgentRequestAcceptance.signature` — the request-set proof, added later and born +directly onto the settled form. The first two originally covered deterministic +protobuf *binary*: marshal the message with deterministic field order, sign +those bytes. We reversed that and moved both onto RFC 8785 JCS over canonical proto-JSON, `JCS(protojson(msg with the signature fields cleared))`, under one pinned proto-JSON option set (snake_case field names, enums as name strings, unpopulated @@ -78,15 +81,18 @@ One clause of that pinned option set carries more weight than it looks. with an empty value — so the canonical bytes for an empty string field are never the bytes for a populated one. Go inherits that from `protojson` for free. A port that assembles the JSON object by hand does not, and has to enumerate the omission for -every field or it signs bytes Go never produces. The acceptance payload is the one -place the Python and TS ports hand-build the object — Go renders the -`AgentAcceptancePayload` message through the same protojson canonicalizer as the -offer — and it is exactly where the divergence +every field or it signs bytes Go never produces. The acceptance payloads are the +places the Python and TS ports hand-build the object — both +`AgentAcceptancePayload` and `AgentRequestAcceptancePayload`, the latter down to +each entry of its nested `items` list — while Go renders both messages through +the same protojson canonicalizer as the offer. The original acceptance payload +is exactly where the divergence appeared: Python and TS dropped an empty `requester_domain` but emitted an empty `requester_id`, which is wire-valid because `Requester.id` carries no `min_len`. Such a mismatch fails closed, but it falsifies the byte-equivalence the canonical-bytes accessors promise, so the shared corpus now carries a vector that holds every port to -the omission. +the omission, and the request-acceptance vectors pin the same invariant for the +second hand-built payload. The two reversals left a consequence worth naming, because it is the reason the canonical bytes are a first-class SDK export rather than an internal detail. A @@ -95,8 +101,10 @@ re-derives the bytes at verification time has silently pinned an *already-signed payload to whatever canonicalization the SDK implements *later* — the failure would surface as "the signature does not verify", indistinguishable from "it was never signed". So all three SDKs expose the exact signed bytes as a public accessor -(`CanonicalOfferBytes` / `CanonicalAcceptanceBytes` in Go, `canonical_offer_payload` -/ `jcs_acceptance_payload` in Python, `canonicalOfferPayload` / `acceptancePayload` +(`CanonicalOfferBytes` / `CanonicalAcceptanceBytes` / +`CanonicalRequestAcceptanceBytes` in Go, `canonical_offer_payload` / +`jcs_acceptance_payload` / `jcs_request_acceptance_payload` in Python, +`canonicalOfferPayload` / `acceptancePayload` / `requestAcceptancePayload` in TS). A party keeping evidence stores those bytes and re-verifies against them verbatim, rather than trusting a future canonicalizer to reproduce the past. @@ -433,6 +441,18 @@ top-level-versus-items mismatch to police. That last exemption is why string: an empty value is unroutable, and the swap-protection its signature is supposed to provide is vacuous when the signed bytes carry no recipient at all. +`AgentRequestAcceptanceItem.exchange` does not reopen that exemption. It is a +signed **projection index**, not an audience field: the binding audience +statement for a transaction stays `Offer.exchange` inside each Exchange-signed +offer, and the item's copy exists so a recipient of a projected subrequest can +derive which signed references must appear in its own projection without +holding the offers addressed to other Exchanges. The +top-level-versus-items-mismatch objection that killed a top-level audience +field does not apply, because the projection check itself polices the copy: +every forwarded offer must name the verifying Exchange, and every reference +whose `exchange` names it must be present, in order, so a disagreement between +the two spellings of the recipient is a refusal, not a latent inconsistency. + ## The audience match is exact; the endpoint rule is not Two host comparisons sit a few sections apart in this document and answer diff --git a/docs/sdk-parity-matrix.md b/docs/sdk-parity-matrix.md index 4a6decd1..9d1002f2 100644 --- a/docs/sdk-parity-matrix.md +++ b/docs/sdk-parity-matrix.md @@ -12,7 +12,7 @@ Go is the oracle (`sdk/go/{helpers,resolvers,core,connect,connectserver}`); Python and TS mirror it. This document is **generated** from the same two artifacts CI already enforces against the code, so it cannot drift from the real surface — a mismatch fails the API-surface gate or the corpus-completeness gate before it can reach this file. -**At a glance:** 130 symbols at cross-language parity · 16 documented divergences · 179 Go-idiomatic exclusions · 34 conformance corpora, each tri-replayed. +**At a glance:** 133 symbols at cross-language parity · 16 documented divergences · 183 Go-idiomatic exclusions · 35 conformance corpora, each tri-replayed. Layering (L1 pure trust core vs L2 I/O resolvers), the SSRF transport-wiring invariant, and naming conventions are recorded in [`design-history.md`](./design-history.md). @@ -32,6 +32,7 @@ Legend: a name = the public face in that language · `—` = intentionally none | `BareDomainPattern` | `BARE_DOMAIN_PATTERN` | `bareDomainPattern` | | `CanonicalAcceptanceBytes` | `jcs_acceptance_payload` | `acceptancePayload` | | `CanonicalOfferBytes` | `canonical_offer_payload` | `canonicalOfferPayload` | +| `CanonicalRequestAcceptanceBytes` | `jcs_request_acceptance_payload` | `requestAcceptancePayload` | | `CanonicalRestrictionToken` | `canonical_restriction_token` | `canonicalRestrictionToken` | | `CanonicalizeMoney` | `canonicalize_money` | `canonicalizeMoney` | | `CatalogRejectionDetail` | `catalog_rejection_detail` | `catalogRejectionDetail` | @@ -93,6 +94,7 @@ Legend: a name = the public face in that language · `—` = intentionally none | `SignOffer` | `sign_offer_jcs` | `signOffer` | | `SignOfferAcceptance` | `sign_offer_acceptance_jcs` | `signOfferAcceptance` | | `SignRequest` | `sign_request` | `signRequest` | +| `SignRequestAcceptance` | `sign_request_acceptance_jcs` | `signRequestAcceptance` | | `SignURLEd25519` | `sign_ed25519_signed_url` | `signEd25519SignedUrl` | | `SignatureAgentHeader` | `SignatureAgentHeader` | `SignatureAgentHeader` | | `StaticKeyResolver` | `StaticKeyResolver` | `StaticKeyResolver` | @@ -106,6 +108,7 @@ Legend: a name = the public face in that language · `—` = intentionally none | `VerifyMultisigRequest` | `verify_multisig_request_server` | `verifyMultisigRequestServer` | | `VerifyOfferAcceptance` | `verify_offer_acceptance_jcs` | `verifyOfferAcceptance` | | `VerifyRequest` | `verify_request` | `verifyRequestServer` | +| `VerifyRequestAcceptance` | `verify_request_acceptance_jcs` | `verifyRequestAcceptance` | | `VerifyURLEd25519` | `verify_ed25519_signed_url` | `verifyEd25519SignedUrl` | ### resolvers — L2 I/O (key/endpoint resolution, active-key, SSRF-guarded fetch) @@ -319,6 +322,7 @@ Go constructs (functional-option builders, `errors.Is` sentinels, value types, c | `helpers.ErrOfferExpired` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | | `helpers.ErrOfferSignatureInvalid` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | | `helpers.ErrProofOfPossessionMismatch` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | +| `helpers.ErrRequestAcceptanceSignatureInvalid` | Go errors.Is sentinel; py/ts return false for a request-acceptance mismatch rather than exporting a sentinel. | | `helpers.ErrSignatureLifetimeTooLong` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | | `helpers.ErrSignatureVerify` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | | `helpers.ErrTooManyHops` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | @@ -343,6 +347,7 @@ Go constructs (functional-option builders, `errors.Is` sentinels, value types, c | `helpers.RegistrationDataTooManyMembers` | Member of the mapped helpers.RegistrationDataVerdict vocabulary. Python and TypeScript spell it as a literal; the shared registration-schema corpus pins the token. | | `helpers.RegistrationDataUncanonicalizable` | Member of the mapped helpers.RegistrationDataVerdict vocabulary. Python and TypeScript spell it as a literal; the shared registration-schema corpus pins the token. | | `helpers.RegistrationSchemaCompileTimeout` | Go-only wall-clock backstop on compilation, not part of the accepted/refused contract and deliberately absent from the ports: Go's runtime preempts, while a CPU-bound spin holds CPython's interpreter and blocks Node's event loop, so a timer there cannot interrupt the work it names. What bounds all three identically is static — the size, depth and evaluation caps and the pattern alphabet — and no admitted schema should ever reach this timeout. | +| `helpers.RequestAcceptancePayload` | Go typed-protobuf builder for the request-acceptance payload; Python and TypeScript build their language-native payload objects inside the mapped canonicalizer/client. | | `helpers.RetrievalAuthFailureReasonFromToken` | Go lookup from the delivery edge's refusal token to the typed enum; py/ts branch on the token string directly. | | `helpers.SchemaAccepted` | Member of the mapped helpers.SchemaVerdict vocabulary. Python and TypeScript spell it as a literal; the shared registration-schema corpus pins the token. | | `helpers.SchemaCompileTimeout` | Member of the mapped helpers.SchemaVerdict vocabulary. Python and TypeScript spell it as a literal; the shared registration-schema corpus pins the token. | @@ -361,6 +366,7 @@ Go constructs (functional-option builders, `errors.Is` sentinels, value types, c | `helpers.SharedValidator` | Go protovalidate validator singleton; TS/Python ship no protovalidate face. | | `helpers.SignOfferAcceptanceWith` | Go Signer-custody variant of SignOfferAcceptance so the SDK never holds the key; py/ts pass key material directly to their single acceptance signer. | | `helpers.SignOptions` | Go options struct for SignRequest; py/ts pass options via kwargs/options objects. | +| `helpers.SignRequestAcceptanceWith` | Go Signer-custody variant of SignRequestAcceptance; Python custody is a SigningTransport method and TypeScript passes its CryptoKey directly. | | `helpers.SignatureAgentFromContext` | Go context.Context accessor; py/ts thread signature-agent state explicitly. | | `helpers.SignedURL` | Go signed-URL result value type; py/ts return language-native result objects. | | `helpers.Signer` | Go Signer interface; py/ts inject a sign function (TS Ed25519SignFn; Python a signing callable) rather than a named Signer type — divergent handle shape, not a missing operation. | @@ -372,6 +378,7 @@ Go constructs (functional-option builders, `errors.Is` sentinels, value types, c | `helpers.VerifyOffer` | Go low-level offer-signature verify; py/ts route offer verification through the Verifier face (core.Verifier). | | `helpers.VerifyOptions` | Go options struct for verify; py/ts pass options via kwargs/options objects. | | `helpers.VerifyPresentedOffer` | Go low-level presented-offer freshness verify; py/ts route offer verification through the Verifier face. | +| `helpers.VerifyRequestAcceptanceProjection` | Go Exchange-server projection gate; Python and TypeScript currently ship agent clients, while canonical sign/verify remains at parity and shared vectors pin the payload. | | `helpers.VerifyRequestResolved` | Go resolver-injected VerifyRequest overload; py/ts expose a single verify entry point. | | `helpers.WithSignatureAgent` | Go functional-option builder; py/ts pass options via kwargs/options objects. | | `resolvers.ActiveKeyScanOptions` | Go scan-options struct; py/ts pass scan options inline. | @@ -408,6 +415,7 @@ Go emits each `*-vectors.json` oracle; Python and TS replay it. The completeness | `helpers/testdata/offer-verify-vectors.json` | ✅ | ✅ | ✅ | | `helpers/testdata/pop-vectors.json` | ✅ | ✅ | ✅ | | `helpers/testdata/registration-schema-vectors.json` | ✅ | ✅ | ✅ | +| `helpers/testdata/request-acceptance-vectors.json` | ✅ | ✅ | ✅ | | `helpers/testdata/scopes-vectors.json` | ✅ | ✅ | ✅ | | `helpers/testdata/sign-request-vectors.json` | ✅ | ✅ | ✅ | | `helpers/testdata/signedurl-vectors.json` | ✅ | ✅ | ✅ | diff --git a/gen/descriptor.binpb b/gen/descriptor.binpb index 04133ad4..f14f4a8a 100644 Binary files a/gen/descriptor.binpb and b/gen/descriptor.binpb differ diff --git a/gen/go/ramp/v1/ramp.pb.go b/gen/go/ramp/v1/ramp.pb.go index 3d35adad..2d195386 100644 --- a/gen/go/ramp/v1/ramp.pb.go +++ b/gen/go/ramp/v1/ramp.pb.go @@ -4564,6 +4564,240 @@ func (x *AgentAcceptance) GetSignatureAlgorithm() string { return "" } +// AgentRequestAcceptance — the agent's topology-independent authorization of +// one complete ordered execute set. A Broker forwards this envelope unchanged +// when it projects a mixed-Exchange request into per-Exchange subrequests. +// Each receiving Exchange verifies the signature, then requires its subrequest +// to equal the complete in-order projection of payload.items whose exchange +// names that Exchange. This is what makes removal, append, and reorder visible +// before request-level idempotency state is claimed. +// +// A projected subrequest is a NEW HTTP request its sender authors. The party +// that projects — a Broker, or the agent itself when it splits its own +// mixed-Exchange set — computes that subrequest's Content-Digest and signs it +// with its own RFC 9421 request signature. The agent's original RFC 9421 +// signature covered the body the agent sent and does not travel with a +// projected body; that is expected, not a gap, and it is why this proof +// exists: like AgentAcceptance, it is a detached body signature that stays +// valid however the request travels. The hop-signature stack applies only to +// requests forwarded byte-for-byte. If a delegation rides the request, the +// holder-binding rule is unchanged — the wire signer must be the delegation's +// terminal holder — so a Broker may project a delegated request only when the +// agent has delegated to the Broker's key. +// +// The verification key is the agent key published for the requester the signed +// payload names. payload.requester_domain must equal the request's +// requester.domain, and the Exchange accepts the signature only if it verifies +// against an Ed25519 key currently valid in that domain's WBA directory +// ({requester_domain}/.well-known/http-message-signatures-directory), fetched +// under the same SSRF discipline as every directory fetch. The envelope +// carries no keyid, so the verifier tries the currently-valid Ed25519 keys of +// that directory; rotation overlap keeps that set small. When the requester +// itself signed the arriving request, the request-signing key the Exchange +// already resolved is that key, and no second fetch is needed. A signature +// that verifies against a key the requester's domain publishes is what turns +// the claimed requester identity into an authenticated one. +type AgentRequestAcceptance struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The signed payload is carried because a projected subrequest does not carry + // offers addressed to other Exchanges and therefore cannot reconstruct the + // original complete set by itself. + Payload *AgentRequestAcceptancePayload `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + // Hex-encoded detached Ed25519 signature over the canonical payload bytes. + Signature string `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` + // Signature algorithm; "EdDSA" for Ed25519. + SignatureAlgorithm string `protobuf:"bytes,3,opt,name=signature_algorithm,json=signatureAlgorithm,proto3" json:"signature_algorithm,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentRequestAcceptance) Reset() { + *x = AgentRequestAcceptance{} + mi := &file_ramp_v1_ramp_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentRequestAcceptance) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentRequestAcceptance) ProtoMessage() {} + +func (x *AgentRequestAcceptance) ProtoReflect() protoreflect.Message { + mi := &file_ramp_v1_ramp_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentRequestAcceptance.ProtoReflect.Descriptor instead. +func (*AgentRequestAcceptance) Descriptor() ([]byte, []int) { + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{19} +} + +func (x *AgentRequestAcceptance) GetPayload() *AgentRequestAcceptancePayload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *AgentRequestAcceptance) GetSignature() string { + if x != nil { + return x.Signature + } + return "" +} + +func (x *AgentRequestAcceptance) GetSignatureAlgorithm() string { + if x != nil { + return x.SignatureAlgorithm + } + return "" +} + +// AgentRequestAcceptanceItem is the minimum reference needed to authorize an +// offer's membership, order, and fan-out destination without repeating the +// full Offer in every projected subrequest. Offer.signature transitively binds +// the full offer, including its exchange field; the explicit exchange lets a +// recipient derive which signed references must appear in its projection. +type AgentRequestAcceptanceItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + OfferSig string `protobuf:"bytes,1,opt,name=offer_sig,json=offerSig,proto3" json:"offer_sig,omitempty"` + Exchange string `protobuf:"bytes,2,opt,name=exchange,proto3" json:"exchange,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentRequestAcceptanceItem) Reset() { + *x = AgentRequestAcceptanceItem{} + mi := &file_ramp_v1_ramp_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentRequestAcceptanceItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentRequestAcceptanceItem) ProtoMessage() {} + +func (x *AgentRequestAcceptanceItem) ProtoReflect() protoreflect.Message { + mi := &file_ramp_v1_ramp_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentRequestAcceptanceItem.ProtoReflect.Descriptor instead. +func (*AgentRequestAcceptanceItem) Descriptor() ([]byte, []int) { + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{20} +} + +func (x *AgentRequestAcceptanceItem) GetOfferSig() string { + if x != nil { + return x.OfferSig + } + return "" +} + +func (x *AgentRequestAcceptanceItem) GetExchange() string { + if x != nil { + return x.Exchange + } + return "" +} + +// AgentRequestAcceptancePayload fixes the field set signed by an +// AgentRequestAcceptance. Its canonical bytes are +// JCS(protojson(AgentRequestAcceptancePayload)) using the canonical-signing +// rules defined on Offer.signature. +type AgentRequestAcceptancePayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Complete original request order, before Broker fan-out. Capped at 256 — + // the same ceiling a discovery query's uris list carries, so one request + // can reference at most one offer per queried URI at the query cap. The Go + // verification helper enforces the same bound itself before doing any + // canonicalization work, because a verifier may run with wire validation + // off and the canonical rendering of an unbounded list is the expensive + // step an unauthenticated caller could otherwise buy for free. + Items []*AgentRequestAcceptanceItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + RequesterId string `protobuf:"bytes,2,opt,name=requester_id,json=requesterId,proto3" json:"requester_id,omitempty"` + RequesterDomain string `protobuf:"bytes,3,opt,name=requester_domain,json=requesterDomain,proto3" json:"requester_domain,omitempty"` + IdempotencyKey string `protobuf:"bytes,4,opt,name=idempotency_key,json=idempotencyKey,proto3" json:"idempotency_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentRequestAcceptancePayload) Reset() { + *x = AgentRequestAcceptancePayload{} + mi := &file_ramp_v1_ramp_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentRequestAcceptancePayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentRequestAcceptancePayload) ProtoMessage() {} + +func (x *AgentRequestAcceptancePayload) ProtoReflect() protoreflect.Message { + mi := &file_ramp_v1_ramp_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentRequestAcceptancePayload.ProtoReflect.Descriptor instead. +func (*AgentRequestAcceptancePayload) Descriptor() ([]byte, []int) { + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{21} +} + +func (x *AgentRequestAcceptancePayload) GetItems() []*AgentRequestAcceptanceItem { + if x != nil { + return x.Items + } + return nil +} + +func (x *AgentRequestAcceptancePayload) GetRequesterId() string { + if x != nil { + return x.RequesterId + } + return "" +} + +func (x *AgentRequestAcceptancePayload) GetRequesterDomain() string { + if x != nil { + return x.RequesterDomain + } + return "" +} + +func (x *AgentRequestAcceptancePayload) GetIdempotencyKey() string { + if x != nil { + return x.IdempotencyKey + } + return "" +} + // AgentAcceptancePayload — the canonical signing structure for AgentAcceptance. // It is NEVER sent on the wire; it exists solely so the signer (SDK) and the // verifier (Exchange) derive BYTE-IDENTICAL signed bytes from the same proto @@ -4601,7 +4835,7 @@ type AgentAcceptancePayload struct { func (x *AgentAcceptancePayload) Reset() { *x = AgentAcceptancePayload{} - mi := &file_ramp_v1_ramp_proto_msgTypes[19] + mi := &file_ramp_v1_ramp_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4613,7 +4847,7 @@ func (x *AgentAcceptancePayload) String() string { func (*AgentAcceptancePayload) ProtoMessage() {} func (x *AgentAcceptancePayload) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[19] + mi := &file_ramp_v1_ramp_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4626,7 +4860,7 @@ func (x *AgentAcceptancePayload) ProtoReflect() protoreflect.Message { // Deprecated: Use AgentAcceptancePayload.ProtoReflect.Descriptor instead. func (*AgentAcceptancePayload) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{19} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{22} } func (x *AgentAcceptancePayload) GetOfferSig() string { @@ -4684,6 +4918,11 @@ type TransactionRequest struct { // presented bytes against its own key — stateless, self-contained bearer // tokens, with no reconstruct-from-catalog. Items []*TransactionItem `protobuf:"bytes,7,rep,name=items,proto3" json:"items,omitempty"` + // Optional for wire compatibility. When present, an Exchange verifies this + // before creating or serving request-level idempotency state. A Broker MUST + // forward it unchanged on every projected subrequest. Older clients that omit + // it retain per-item execution semantics but receive no request-level claim. + AgentRequestAcceptance *AgentRequestAcceptance `protobuf:"bytes,8,opt,name=agent_request_acceptance,json=agentRequestAcceptance,proto3,oneof" json:"agent_request_acceptance,omitempty"` // Extension point Ext *structpb.Struct `protobuf:"bytes,15,opt,name=ext,proto3" json:"ext,omitempty"` // Critical extension keys (COSE crit pattern, RFC 9052). @@ -4697,7 +4936,7 @@ type TransactionRequest struct { func (x *TransactionRequest) Reset() { *x = TransactionRequest{} - mi := &file_ramp_v1_ramp_proto_msgTypes[20] + mi := &file_ramp_v1_ramp_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4709,7 +4948,7 @@ func (x *TransactionRequest) String() string { func (*TransactionRequest) ProtoMessage() {} func (x *TransactionRequest) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[20] + mi := &file_ramp_v1_ramp_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4722,7 +4961,7 @@ func (x *TransactionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TransactionRequest.ProtoReflect.Descriptor instead. func (*TransactionRequest) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{20} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{23} } func (x *TransactionRequest) GetVer() string { @@ -4753,6 +4992,13 @@ func (x *TransactionRequest) GetItems() []*TransactionItem { return nil } +func (x *TransactionRequest) GetAgentRequestAcceptance() *AgentRequestAcceptance { + if x != nil { + return x.AgentRequestAcceptance + } + return nil +} + func (x *TransactionRequest) GetExt() *structpb.Struct { if x != nil { return x.Ext @@ -4787,7 +5033,7 @@ type TransactionItem struct { func (x *TransactionItem) Reset() { *x = TransactionItem{} - mi := &file_ramp_v1_ramp_proto_msgTypes[21] + mi := &file_ramp_v1_ramp_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4799,7 +5045,7 @@ func (x *TransactionItem) String() string { func (*TransactionItem) ProtoMessage() {} func (x *TransactionItem) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[21] + mi := &file_ramp_v1_ramp_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4812,7 +5058,7 @@ func (x *TransactionItem) ProtoReflect() protoreflect.Message { // Deprecated: Use TransactionItem.ProtoReflect.Descriptor instead. func (*TransactionItem) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{21} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{24} } func (x *TransactionItem) GetOffer() *Offer { @@ -4866,7 +5112,7 @@ type TransactionResponse struct { func (x *TransactionResponse) Reset() { *x = TransactionResponse{} - mi := &file_ramp_v1_ramp_proto_msgTypes[22] + mi := &file_ramp_v1_ramp_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4878,7 +5124,7 @@ func (x *TransactionResponse) String() string { func (*TransactionResponse) ProtoMessage() {} func (x *TransactionResponse) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[22] + mi := &file_ramp_v1_ramp_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4891,7 +5137,7 @@ func (x *TransactionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TransactionResponse.ProtoReflect.Descriptor instead. func (*TransactionResponse) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{22} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{25} } func (x *TransactionResponse) GetVer() string { @@ -4985,7 +5231,7 @@ type TransactionResultItem struct { func (x *TransactionResultItem) Reset() { *x = TransactionResultItem{} - mi := &file_ramp_v1_ramp_proto_msgTypes[23] + mi := &file_ramp_v1_ramp_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4997,7 +5243,7 @@ func (x *TransactionResultItem) String() string { func (*TransactionResultItem) ProtoMessage() {} func (x *TransactionResultItem) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[23] + mi := &file_ramp_v1_ramp_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5010,7 +5256,7 @@ func (x *TransactionResultItem) ProtoReflect() protoreflect.Message { // Deprecated: Use TransactionResultItem.ProtoReflect.Descriptor instead. func (*TransactionResultItem) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{23} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{26} } func (x *TransactionResultItem) GetOfferId() string { @@ -5117,7 +5363,7 @@ type Cost struct { func (x *Cost) Reset() { *x = Cost{} - mi := &file_ramp_v1_ramp_proto_msgTypes[24] + mi := &file_ramp_v1_ramp_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5129,7 +5375,7 @@ func (x *Cost) String() string { func (*Cost) ProtoMessage() {} func (x *Cost) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[24] + mi := &file_ramp_v1_ramp_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5142,7 +5388,7 @@ func (x *Cost) ProtoReflect() protoreflect.Message { // Deprecated: Use Cost.ProtoReflect.Descriptor instead. func (*Cost) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{24} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{27} } func (x *Cost) GetAmount() string { @@ -5202,7 +5448,7 @@ type PushResourcesRequest struct { func (x *PushResourcesRequest) Reset() { *x = PushResourcesRequest{} - mi := &file_ramp_v1_ramp_proto_msgTypes[25] + mi := &file_ramp_v1_ramp_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5214,7 +5460,7 @@ func (x *PushResourcesRequest) String() string { func (*PushResourcesRequest) ProtoMessage() {} func (x *PushResourcesRequest) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[25] + mi := &file_ramp_v1_ramp_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5227,7 +5473,7 @@ func (x *PushResourcesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PushResourcesRequest.ProtoReflect.Descriptor instead. func (*PushResourcesRequest) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{25} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{28} } func (x *PushResourcesRequest) GetVer() string { @@ -5372,7 +5618,7 @@ type ResourceEntry struct { func (x *ResourceEntry) Reset() { *x = ResourceEntry{} - mi := &file_ramp_v1_ramp_proto_msgTypes[26] + mi := &file_ramp_v1_ramp_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5384,7 +5630,7 @@ func (x *ResourceEntry) String() string { func (*ResourceEntry) ProtoMessage() {} func (x *ResourceEntry) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[26] + mi := &file_ramp_v1_ramp_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5397,7 +5643,7 @@ func (x *ResourceEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceEntry.ProtoReflect.Descriptor instead. func (*ResourceEntry) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{26} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{29} } func (x *ResourceEntry) GetDomain() string { @@ -5550,7 +5796,7 @@ type PushResourcesResponse struct { func (x *PushResourcesResponse) Reset() { *x = PushResourcesResponse{} - mi := &file_ramp_v1_ramp_proto_msgTypes[27] + mi := &file_ramp_v1_ramp_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5562,7 +5808,7 @@ func (x *PushResourcesResponse) String() string { func (*PushResourcesResponse) ProtoMessage() {} func (x *PushResourcesResponse) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[27] + mi := &file_ramp_v1_ramp_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5575,7 +5821,7 @@ func (x *PushResourcesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PushResourcesResponse.ProtoReflect.Descriptor instead. func (*PushResourcesResponse) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{27} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{30} } func (x *PushResourcesResponse) GetVer() string { @@ -5642,7 +5888,7 @@ type RemoveResourcesRequest struct { func (x *RemoveResourcesRequest) Reset() { *x = RemoveResourcesRequest{} - mi := &file_ramp_v1_ramp_proto_msgTypes[28] + mi := &file_ramp_v1_ramp_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5654,7 +5900,7 @@ func (x *RemoveResourcesRequest) String() string { func (*RemoveResourcesRequest) ProtoMessage() {} func (x *RemoveResourcesRequest) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[28] + mi := &file_ramp_v1_ramp_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5667,7 +5913,7 @@ func (x *RemoveResourcesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveResourcesRequest.ProtoReflect.Descriptor instead. func (*RemoveResourcesRequest) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{28} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{31} } func (x *RemoveResourcesRequest) GetVer() string { @@ -5711,7 +5957,7 @@ type RemoveResourcesResponse struct { func (x *RemoveResourcesResponse) Reset() { *x = RemoveResourcesResponse{} - mi := &file_ramp_v1_ramp_proto_msgTypes[29] + mi := &file_ramp_v1_ramp_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5723,7 +5969,7 @@ func (x *RemoveResourcesResponse) String() string { func (*RemoveResourcesResponse) ProtoMessage() {} func (x *RemoveResourcesResponse) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[29] + mi := &file_ramp_v1_ramp_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5736,7 +5982,7 @@ func (x *RemoveResourcesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveResourcesResponse.ProtoReflect.Descriptor instead. func (*RemoveResourcesResponse) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{29} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{32} } func (x *RemoveResourcesResponse) GetVer() string { @@ -5771,7 +6017,7 @@ type RefreshCatalogRequest struct { func (x *RefreshCatalogRequest) Reset() { *x = RefreshCatalogRequest{} - mi := &file_ramp_v1_ramp_proto_msgTypes[30] + mi := &file_ramp_v1_ramp_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5783,7 +6029,7 @@ func (x *RefreshCatalogRequest) String() string { func (*RefreshCatalogRequest) ProtoMessage() {} func (x *RefreshCatalogRequest) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[30] + mi := &file_ramp_v1_ramp_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5796,7 +6042,7 @@ func (x *RefreshCatalogRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RefreshCatalogRequest.ProtoReflect.Descriptor instead. func (*RefreshCatalogRequest) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{30} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{33} } func (x *RefreshCatalogRequest) GetVer() string { @@ -5833,7 +6079,7 @@ type RefreshCatalogResponse struct { func (x *RefreshCatalogResponse) Reset() { *x = RefreshCatalogResponse{} - mi := &file_ramp_v1_ramp_proto_msgTypes[31] + mi := &file_ramp_v1_ramp_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5845,7 +6091,7 @@ func (x *RefreshCatalogResponse) String() string { func (*RefreshCatalogResponse) ProtoMessage() {} func (x *RefreshCatalogResponse) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[31] + mi := &file_ramp_v1_ramp_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5858,7 +6104,7 @@ func (x *RefreshCatalogResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RefreshCatalogResponse.ProtoReflect.Descriptor instead. func (*RefreshCatalogResponse) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{31} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{34} } func (x *RefreshCatalogResponse) GetVer() string { @@ -5900,7 +6146,7 @@ type ReportingObligation struct { func (x *ReportingObligation) Reset() { *x = ReportingObligation{} - mi := &file_ramp_v1_ramp_proto_msgTypes[32] + mi := &file_ramp_v1_ramp_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5912,7 +6158,7 @@ func (x *ReportingObligation) String() string { func (*ReportingObligation) ProtoMessage() {} func (x *ReportingObligation) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[32] + mi := &file_ramp_v1_ramp_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5925,7 +6171,7 @@ func (x *ReportingObligation) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportingObligation.ProtoReflect.Descriptor instead. func (*ReportingObligation) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{32} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{35} } func (x *ReportingObligation) GetRequired() bool { @@ -6016,7 +6262,7 @@ type UsageReport struct { func (x *UsageReport) Reset() { *x = UsageReport{} - mi := &file_ramp_v1_ramp_proto_msgTypes[33] + mi := &file_ramp_v1_ramp_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6028,7 +6274,7 @@ func (x *UsageReport) String() string { func (*UsageReport) ProtoMessage() {} func (x *UsageReport) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[33] + mi := &file_ramp_v1_ramp_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6041,7 +6287,7 @@ func (x *UsageReport) ProtoReflect() protoreflect.Message { // Deprecated: Use UsageReport.ProtoReflect.Descriptor instead. func (*UsageReport) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{33} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{36} } func (x *UsageReport) GetVer() string { @@ -6129,7 +6375,7 @@ type AttributionDetail struct { func (x *AttributionDetail) Reset() { *x = AttributionDetail{} - mi := &file_ramp_v1_ramp_proto_msgTypes[34] + mi := &file_ramp_v1_ramp_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6141,7 +6387,7 @@ func (x *AttributionDetail) String() string { func (*AttributionDetail) ProtoMessage() {} func (x *AttributionDetail) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[34] + mi := &file_ramp_v1_ramp_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6154,7 +6400,7 @@ func (x *AttributionDetail) ProtoReflect() protoreflect.Message { // Deprecated: Use AttributionDetail.ProtoReflect.Descriptor instead. func (*AttributionDetail) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{34} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{37} } func (x *AttributionDetail) GetDisplayedUrl() string { @@ -6208,7 +6454,7 @@ type Usage struct { func (x *Usage) Reset() { *x = Usage{} - mi := &file_ramp_v1_ramp_proto_msgTypes[35] + mi := &file_ramp_v1_ramp_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6220,7 +6466,7 @@ func (x *Usage) String() string { func (*Usage) ProtoMessage() {} func (x *Usage) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[35] + mi := &file_ramp_v1_ramp_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6233,7 +6479,7 @@ func (x *Usage) ProtoReflect() protoreflect.Message { // Deprecated: Use Usage.ProtoReflect.Descriptor instead. func (*Usage) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{35} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{38} } func (x *Usage) GetFunction() []string { @@ -6300,7 +6546,7 @@ type UsageAsset struct { func (x *UsageAsset) Reset() { *x = UsageAsset{} - mi := &file_ramp_v1_ramp_proto_msgTypes[36] + mi := &file_ramp_v1_ramp_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6312,7 +6558,7 @@ func (x *UsageAsset) String() string { func (*UsageAsset) ProtoMessage() {} func (x *UsageAsset) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[36] + mi := &file_ramp_v1_ramp_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6325,7 +6571,7 @@ func (x *UsageAsset) ProtoReflect() protoreflect.Message { // Deprecated: Use UsageAsset.ProtoReflect.Descriptor instead. func (*UsageAsset) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{36} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{39} } func (x *UsageAsset) GetUri() string { @@ -6376,7 +6622,7 @@ type UsageReportResponse struct { func (x *UsageReportResponse) Reset() { *x = UsageReportResponse{} - mi := &file_ramp_v1_ramp_proto_msgTypes[37] + mi := &file_ramp_v1_ramp_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6388,7 +6634,7 @@ func (x *UsageReportResponse) String() string { func (*UsageReportResponse) ProtoMessage() {} func (x *UsageReportResponse) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[37] + mi := &file_ramp_v1_ramp_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6401,7 +6647,7 @@ func (x *UsageReportResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UsageReportResponse.ProtoReflect.Descriptor instead. func (*UsageReportResponse) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{37} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{40} } func (x *UsageReportResponse) GetVer() string { @@ -6487,7 +6733,7 @@ type DiscoveryRequest struct { func (x *DiscoveryRequest) Reset() { *x = DiscoveryRequest{} - mi := &file_ramp_v1_ramp_proto_msgTypes[38] + mi := &file_ramp_v1_ramp_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6499,7 +6745,7 @@ func (x *DiscoveryRequest) String() string { func (*DiscoveryRequest) ProtoMessage() {} func (x *DiscoveryRequest) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[38] + mi := &file_ramp_v1_ramp_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6512,7 +6758,7 @@ func (x *DiscoveryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DiscoveryRequest.ProtoReflect.Descriptor instead. func (*DiscoveryRequest) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{38} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{41} } func (x *DiscoveryRequest) GetVer() string { @@ -6640,7 +6886,7 @@ type RequestConstraints struct { func (x *RequestConstraints) Reset() { *x = RequestConstraints{} - mi := &file_ramp_v1_ramp_proto_msgTypes[39] + mi := &file_ramp_v1_ramp_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6652,7 +6898,7 @@ func (x *RequestConstraints) String() string { func (*RequestConstraints) ProtoMessage() {} func (x *RequestConstraints) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[39] + mi := &file_ramp_v1_ramp_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6665,7 +6911,7 @@ func (x *RequestConstraints) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestConstraints.ProtoReflect.Descriptor instead. func (*RequestConstraints) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{39} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{42} } func (x *RequestConstraints) GetExchanges() []string { @@ -6781,7 +7027,7 @@ type JsonWebKey struct { func (x *JsonWebKey) Reset() { *x = JsonWebKey{} - mi := &file_ramp_v1_ramp_proto_msgTypes[40] + mi := &file_ramp_v1_ramp_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6793,7 +7039,7 @@ func (x *JsonWebKey) String() string { func (*JsonWebKey) ProtoMessage() {} func (x *JsonWebKey) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[40] + mi := &file_ramp_v1_ramp_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6806,7 +7052,7 @@ func (x *JsonWebKey) ProtoReflect() protoreflect.Message { // Deprecated: Use JsonWebKey.ProtoReflect.Descriptor instead. func (*JsonWebKey) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{40} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{43} } func (x *JsonWebKey) GetKty() string { @@ -7057,7 +7303,7 @@ type AccountRegistration struct { func (x *AccountRegistration) Reset() { *x = AccountRegistration{} - mi := &file_ramp_v1_ramp_proto_msgTypes[41] + mi := &file_ramp_v1_ramp_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7069,7 +7315,7 @@ func (x *AccountRegistration) String() string { func (*AccountRegistration) ProtoMessage() {} func (x *AccountRegistration) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[41] + mi := &file_ramp_v1_ramp_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7082,7 +7328,7 @@ func (x *AccountRegistration) ProtoReflect() protoreflect.Message { // Deprecated: Use AccountRegistration.ProtoReflect.Descriptor instead. func (*AccountRegistration) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{41} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{44} } func (x *AccountRegistration) GetDataSchema() *structpb.Struct { @@ -7220,7 +7466,7 @@ type WellKnownManifest struct { func (x *WellKnownManifest) Reset() { *x = WellKnownManifest{} - mi := &file_ramp_v1_ramp_proto_msgTypes[42] + mi := &file_ramp_v1_ramp_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7232,7 +7478,7 @@ func (x *WellKnownManifest) String() string { func (*WellKnownManifest) ProtoMessage() {} func (x *WellKnownManifest) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[42] + mi := &file_ramp_v1_ramp_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7245,7 +7491,7 @@ func (x *WellKnownManifest) ProtoReflect() protoreflect.Message { // Deprecated: Use WellKnownManifest.ProtoReflect.Descriptor instead. func (*WellKnownManifest) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{42} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{45} } func (x *WellKnownManifest) GetVer() string { @@ -7472,7 +7718,7 @@ type WBAFile struct { func (x *WBAFile) Reset() { *x = WBAFile{} - mi := &file_ramp_v1_ramp_proto_msgTypes[43] + mi := &file_ramp_v1_ramp_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7484,7 +7730,7 @@ func (x *WBAFile) String() string { func (*WBAFile) ProtoMessage() {} func (x *WBAFile) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[43] + mi := &file_ramp_v1_ramp_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7497,7 +7743,7 @@ func (x *WBAFile) ProtoReflect() protoreflect.Message { // Deprecated: Use WBAFile.ProtoReflect.Descriptor instead. func (*WBAFile) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{43} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{46} } func (x *WBAFile) GetKeys() []*JsonWebKey { @@ -7536,7 +7782,7 @@ type KeyRevocationList struct { func (x *KeyRevocationList) Reset() { *x = KeyRevocationList{} - mi := &file_ramp_v1_ramp_proto_msgTypes[44] + mi := &file_ramp_v1_ramp_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7548,7 +7794,7 @@ func (x *KeyRevocationList) String() string { func (*KeyRevocationList) ProtoMessage() {} func (x *KeyRevocationList) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[44] + mi := &file_ramp_v1_ramp_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7561,7 +7807,7 @@ func (x *KeyRevocationList) ProtoReflect() protoreflect.Message { // Deprecated: Use KeyRevocationList.ProtoReflect.Descriptor instead. func (*KeyRevocationList) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{44} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{47} } func (x *KeyRevocationList) GetAsOf() *timestamppb.Timestamp { @@ -7594,7 +7840,7 @@ type CatalogContributor struct { func (x *CatalogContributor) Reset() { *x = CatalogContributor{} - mi := &file_ramp_v1_ramp_proto_msgTypes[45] + mi := &file_ramp_v1_ramp_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7606,7 +7852,7 @@ func (x *CatalogContributor) String() string { func (*CatalogContributor) ProtoMessage() {} func (x *CatalogContributor) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[45] + mi := &file_ramp_v1_ramp_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7619,7 +7865,7 @@ func (x *CatalogContributor) ProtoReflect() protoreflect.Message { // Deprecated: Use CatalogContributor.ProtoReflect.Descriptor instead. func (*CatalogContributor) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{45} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{48} } func (x *CatalogContributor) GetDomain() string { @@ -7659,7 +7905,7 @@ type AuthorizedExchange struct { func (x *AuthorizedExchange) Reset() { *x = AuthorizedExchange{} - mi := &file_ramp_v1_ramp_proto_msgTypes[46] + mi := &file_ramp_v1_ramp_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7671,7 +7917,7 @@ func (x *AuthorizedExchange) String() string { func (*AuthorizedExchange) ProtoMessage() {} func (x *AuthorizedExchange) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[46] + mi := &file_ramp_v1_ramp_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7684,7 +7930,7 @@ func (x *AuthorizedExchange) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthorizedExchange.ProtoReflect.Descriptor instead. func (*AuthorizedExchange) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{46} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{49} } func (x *AuthorizedExchange) GetDomain() string { @@ -7769,7 +8015,7 @@ type DiscoveryResponse struct { func (x *DiscoveryResponse) Reset() { *x = DiscoveryResponse{} - mi := &file_ramp_v1_ramp_proto_msgTypes[47] + mi := &file_ramp_v1_ramp_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7781,7 +8027,7 @@ func (x *DiscoveryResponse) String() string { func (*DiscoveryResponse) ProtoMessage() {} func (x *DiscoveryResponse) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[47] + mi := &file_ramp_v1_ramp_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7794,7 +8040,7 @@ func (x *DiscoveryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DiscoveryResponse.ProtoReflect.Descriptor instead. func (*DiscoveryResponse) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{47} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{50} } func (x *DiscoveryResponse) GetVer() string { @@ -7886,7 +8132,7 @@ type DisputeRequest struct { func (x *DisputeRequest) Reset() { *x = DisputeRequest{} - mi := &file_ramp_v1_ramp_proto_msgTypes[48] + mi := &file_ramp_v1_ramp_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7898,7 +8144,7 @@ func (x *DisputeRequest) String() string { func (*DisputeRequest) ProtoMessage() {} func (x *DisputeRequest) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[48] + mi := &file_ramp_v1_ramp_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7911,7 +8157,7 @@ func (x *DisputeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DisputeRequest.ProtoReflect.Descriptor instead. func (*DisputeRequest) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{48} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{51} } func (x *DisputeRequest) GetVer() string { @@ -8034,7 +8280,7 @@ type DisputeResponse struct { func (x *DisputeResponse) Reset() { *x = DisputeResponse{} - mi := &file_ramp_v1_ramp_proto_msgTypes[49] + mi := &file_ramp_v1_ramp_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8046,7 +8292,7 @@ func (x *DisputeResponse) String() string { func (*DisputeResponse) ProtoMessage() {} func (x *DisputeResponse) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[49] + mi := &file_ramp_v1_ramp_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8059,7 +8305,7 @@ func (x *DisputeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DisputeResponse.ProtoReflect.Descriptor instead. func (*DisputeResponse) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{49} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{52} } func (x *DisputeResponse) GetVer() string { @@ -8139,7 +8385,7 @@ type DomainVerificationRequest struct { func (x *DomainVerificationRequest) Reset() { *x = DomainVerificationRequest{} - mi := &file_ramp_v1_ramp_proto_msgTypes[50] + mi := &file_ramp_v1_ramp_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8151,7 +8397,7 @@ func (x *DomainVerificationRequest) String() string { func (*DomainVerificationRequest) ProtoMessage() {} func (x *DomainVerificationRequest) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[50] + mi := &file_ramp_v1_ramp_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8164,7 +8410,7 @@ func (x *DomainVerificationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DomainVerificationRequest.ProtoReflect.Descriptor instead. func (*DomainVerificationRequest) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{50} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{53} } func (x *DomainVerificationRequest) GetVer() string { @@ -8235,7 +8481,7 @@ type DomainVerificationChallenge struct { func (x *DomainVerificationChallenge) Reset() { *x = DomainVerificationChallenge{} - mi := &file_ramp_v1_ramp_proto_msgTypes[51] + mi := &file_ramp_v1_ramp_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8247,7 +8493,7 @@ func (x *DomainVerificationChallenge) String() string { func (*DomainVerificationChallenge) ProtoMessage() {} func (x *DomainVerificationChallenge) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[51] + mi := &file_ramp_v1_ramp_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8260,7 +8506,7 @@ func (x *DomainVerificationChallenge) ProtoReflect() protoreflect.Message { // Deprecated: Use DomainVerificationChallenge.ProtoReflect.Descriptor instead. func (*DomainVerificationChallenge) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{51} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{54} } func (x *DomainVerificationChallenge) GetVer() string { @@ -8352,7 +8598,7 @@ type DomainVerificationConfirmation struct { func (x *DomainVerificationConfirmation) Reset() { *x = DomainVerificationConfirmation{} - mi := &file_ramp_v1_ramp_proto_msgTypes[52] + mi := &file_ramp_v1_ramp_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8364,7 +8610,7 @@ func (x *DomainVerificationConfirmation) String() string { func (*DomainVerificationConfirmation) ProtoMessage() {} func (x *DomainVerificationConfirmation) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[52] + mi := &file_ramp_v1_ramp_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8377,7 +8623,7 @@ func (x *DomainVerificationConfirmation) ProtoReflect() protoreflect.Message { // Deprecated: Use DomainVerificationConfirmation.ProtoReflect.Descriptor instead. func (*DomainVerificationConfirmation) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{52} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{55} } func (x *DomainVerificationConfirmation) GetVer() string { @@ -8459,7 +8705,7 @@ type DomainVerificationResult struct { func (x *DomainVerificationResult) Reset() { *x = DomainVerificationResult{} - mi := &file_ramp_v1_ramp_proto_msgTypes[53] + mi := &file_ramp_v1_ramp_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8471,7 +8717,7 @@ func (x *DomainVerificationResult) String() string { func (*DomainVerificationResult) ProtoMessage() {} func (x *DomainVerificationResult) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[53] + mi := &file_ramp_v1_ramp_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8484,7 +8730,7 @@ func (x *DomainVerificationResult) ProtoReflect() protoreflect.Message { // Deprecated: Use DomainVerificationResult.ProtoReflect.Descriptor instead. func (*DomainVerificationResult) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{53} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{56} } func (x *DomainVerificationResult) GetVer() string { @@ -8672,7 +8918,7 @@ type RegisterRequest struct { func (x *RegisterRequest) Reset() { *x = RegisterRequest{} - mi := &file_ramp_v1_ramp_proto_msgTypes[54] + mi := &file_ramp_v1_ramp_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8684,7 +8930,7 @@ func (x *RegisterRequest) String() string { func (*RegisterRequest) ProtoMessage() {} func (x *RegisterRequest) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[54] + mi := &file_ramp_v1_ramp_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8697,7 +8943,7 @@ func (x *RegisterRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RegisterRequest.ProtoReflect.Descriptor instead. func (*RegisterRequest) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{54} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{57} } func (x *RegisterRequest) GetVer() string { @@ -8768,7 +9014,7 @@ type RegisterResponse struct { func (x *RegisterResponse) Reset() { *x = RegisterResponse{} - mi := &file_ramp_v1_ramp_proto_msgTypes[55] + mi := &file_ramp_v1_ramp_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8780,7 +9026,7 @@ func (x *RegisterResponse) String() string { func (*RegisterResponse) ProtoMessage() {} func (x *RegisterResponse) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[55] + mi := &file_ramp_v1_ramp_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8793,7 +9039,7 @@ func (x *RegisterResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RegisterResponse.ProtoReflect.Descriptor instead. func (*RegisterResponse) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{55} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{58} } func (x *RegisterResponse) GetVer() string { @@ -8855,7 +9101,7 @@ type GetAccountStatusRequest struct { func (x *GetAccountStatusRequest) Reset() { *x = GetAccountStatusRequest{} - mi := &file_ramp_v1_ramp_proto_msgTypes[56] + mi := &file_ramp_v1_ramp_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8867,7 +9113,7 @@ func (x *GetAccountStatusRequest) String() string { func (*GetAccountStatusRequest) ProtoMessage() {} func (x *GetAccountStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[56] + mi := &file_ramp_v1_ramp_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8880,7 +9126,7 @@ func (x *GetAccountStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAccountStatusRequest.ProtoReflect.Descriptor instead. func (*GetAccountStatusRequest) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{56} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{59} } func (x *GetAccountStatusRequest) GetVer() string { @@ -8959,7 +9205,7 @@ type GetAccountStatusResponse struct { func (x *GetAccountStatusResponse) Reset() { *x = GetAccountStatusResponse{} - mi := &file_ramp_v1_ramp_proto_msgTypes[57] + mi := &file_ramp_v1_ramp_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8971,7 +9217,7 @@ func (x *GetAccountStatusResponse) String() string { func (*GetAccountStatusResponse) ProtoMessage() {} func (x *GetAccountStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[57] + mi := &file_ramp_v1_ramp_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8984,7 +9230,7 @@ func (x *GetAccountStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAccountStatusResponse.ProtoReflect.Descriptor instead. func (*GetAccountStatusResponse) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{57} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{60} } func (x *GetAccountStatusResponse) GetVer() string { @@ -9067,7 +9313,7 @@ type ErrorDetail struct { func (x *ErrorDetail) Reset() { *x = ErrorDetail{} - mi := &file_ramp_v1_ramp_proto_msgTypes[58] + mi := &file_ramp_v1_ramp_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9079,7 +9325,7 @@ func (x *ErrorDetail) String() string { func (*ErrorDetail) ProtoMessage() {} func (x *ErrorDetail) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[58] + mi := &file_ramp_v1_ramp_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9092,7 +9338,7 @@ func (x *ErrorDetail) ProtoReflect() protoreflect.Message { // Deprecated: Use ErrorDetail.ProtoReflect.Descriptor instead. func (*ErrorDetail) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{58} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{61} } func (x *ErrorDetail) GetMessage() string { @@ -9275,7 +9521,7 @@ type TransactionDenial struct { func (x *TransactionDenial) Reset() { *x = TransactionDenial{} - mi := &file_ramp_v1_ramp_proto_msgTypes[59] + mi := &file_ramp_v1_ramp_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9287,7 +9533,7 @@ func (x *TransactionDenial) String() string { func (*TransactionDenial) ProtoMessage() {} func (x *TransactionDenial) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[59] + mi := &file_ramp_v1_ramp_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9300,7 +9546,7 @@ func (x *TransactionDenial) ProtoReflect() protoreflect.Message { // Deprecated: Use TransactionDenial.ProtoReflect.Descriptor instead. func (*TransactionDenial) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{59} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{62} } func (x *TransactionDenial) GetReason() DenialReason { @@ -9346,7 +9592,7 @@ type CatalogRejection struct { func (x *CatalogRejection) Reset() { *x = CatalogRejection{} - mi := &file_ramp_v1_ramp_proto_msgTypes[60] + mi := &file_ramp_v1_ramp_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9358,7 +9604,7 @@ func (x *CatalogRejection) String() string { func (*CatalogRejection) ProtoMessage() {} func (x *CatalogRejection) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[60] + mi := &file_ramp_v1_ramp_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9371,7 +9617,7 @@ func (x *CatalogRejection) ProtoReflect() protoreflect.Message { // Deprecated: Use CatalogRejection.ProtoReflect.Descriptor instead. func (*CatalogRejection) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{60} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{63} } func (x *CatalogRejection) GetReason() CatalogRejectionReason { @@ -9403,7 +9649,7 @@ type RegistrationFailure struct { func (x *RegistrationFailure) Reset() { *x = RegistrationFailure{} - mi := &file_ramp_v1_ramp_proto_msgTypes[61] + mi := &file_ramp_v1_ramp_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9415,7 +9661,7 @@ func (x *RegistrationFailure) String() string { func (*RegistrationFailure) ProtoMessage() {} func (x *RegistrationFailure) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[61] + mi := &file_ramp_v1_ramp_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9428,7 +9674,7 @@ func (x *RegistrationFailure) ProtoReflect() protoreflect.Message { // Deprecated: Use RegistrationFailure.ProtoReflect.Descriptor instead. func (*RegistrationFailure) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{61} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{64} } func (x *RegistrationFailure) GetReason() RegistrationFailureReason { @@ -9466,7 +9712,7 @@ type RegistrationFieldError struct { func (x *RegistrationFieldError) Reset() { *x = RegistrationFieldError{} - mi := &file_ramp_v1_ramp_proto_msgTypes[62] + mi := &file_ramp_v1_ramp_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9478,7 +9724,7 @@ func (x *RegistrationFieldError) String() string { func (*RegistrationFieldError) ProtoMessage() {} func (x *RegistrationFieldError) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[62] + mi := &file_ramp_v1_ramp_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9491,7 +9737,7 @@ func (x *RegistrationFieldError) ProtoReflect() protoreflect.Message { // Deprecated: Use RegistrationFieldError.ProtoReflect.Descriptor instead. func (*RegistrationFieldError) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{62} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{65} } func (x *RegistrationFieldError) GetPath() string { @@ -9519,7 +9765,7 @@ type DisputeFailure struct { func (x *DisputeFailure) Reset() { *x = DisputeFailure{} - mi := &file_ramp_v1_ramp_proto_msgTypes[63] + mi := &file_ramp_v1_ramp_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9531,7 +9777,7 @@ func (x *DisputeFailure) String() string { func (*DisputeFailure) ProtoMessage() {} func (x *DisputeFailure) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[63] + mi := &file_ramp_v1_ramp_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9544,7 +9790,7 @@ func (x *DisputeFailure) ProtoReflect() protoreflect.Message { // Deprecated: Use DisputeFailure.ProtoReflect.Descriptor instead. func (*DisputeFailure) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{63} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{66} } func (x *DisputeFailure) GetReason() DisputeFailureReason { @@ -9565,7 +9811,7 @@ type DomainVerificationFailure struct { func (x *DomainVerificationFailure) Reset() { *x = DomainVerificationFailure{} - mi := &file_ramp_v1_ramp_proto_msgTypes[64] + mi := &file_ramp_v1_ramp_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9577,7 +9823,7 @@ func (x *DomainVerificationFailure) String() string { func (*DomainVerificationFailure) ProtoMessage() {} func (x *DomainVerificationFailure) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[64] + mi := &file_ramp_v1_ramp_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9590,7 +9836,7 @@ func (x *DomainVerificationFailure) ProtoReflect() protoreflect.Message { // Deprecated: Use DomainVerificationFailure.ProtoReflect.Descriptor instead. func (*DomainVerificationFailure) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{64} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{67} } func (x *DomainVerificationFailure) GetReason() DomainVerificationFailureReason { @@ -9611,7 +9857,7 @@ type RetrievalAuthFailure struct { func (x *RetrievalAuthFailure) Reset() { *x = RetrievalAuthFailure{} - mi := &file_ramp_v1_ramp_proto_msgTypes[65] + mi := &file_ramp_v1_ramp_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9623,7 +9869,7 @@ func (x *RetrievalAuthFailure) String() string { func (*RetrievalAuthFailure) ProtoMessage() {} func (x *RetrievalAuthFailure) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[65] + mi := &file_ramp_v1_ramp_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9636,7 +9882,7 @@ func (x *RetrievalAuthFailure) ProtoReflect() protoreflect.Message { // Deprecated: Use RetrievalAuthFailure.ProtoReflect.Descriptor instead. func (*RetrievalAuthFailure) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{65} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{68} } func (x *RetrievalAuthFailure) GetReason() RetrievalAuthFailureReason { @@ -9657,7 +9903,7 @@ type UsageReportRejection struct { func (x *UsageReportRejection) Reset() { *x = UsageReportRejection{} - mi := &file_ramp_v1_ramp_proto_msgTypes[66] + mi := &file_ramp_v1_ramp_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9669,7 +9915,7 @@ func (x *UsageReportRejection) String() string { func (*UsageReportRejection) ProtoMessage() {} func (x *UsageReportRejection) ProtoReflect() protoreflect.Message { - mi := &file_ramp_v1_ramp_proto_msgTypes[66] + mi := &file_ramp_v1_ramp_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9682,7 +9928,7 @@ func (x *UsageReportRejection) ProtoReflect() protoreflect.Message { // Deprecated: Use UsageReportRejection.ProtoReflect.Descriptor instead. func (*UsageReportRejection) Descriptor() ([]byte, []int) { - return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{66} + return file_ramp_v1_ramp_proto_rawDescGZIP(), []int{69} } func (x *UsageReportRejection) GetReason() UsageReportRejectionReason { @@ -9935,20 +10181,34 @@ const file_ramp_v1_ramp_proto_rawDesc = "" + "\a_issuer\"i\n" + "\x0fAgentAcceptance\x12%\n" + "\tsignature\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\tsignature\x12/\n" + - "\x13signature_algorithm\x18\x02 \x01(\tR\x12signatureAlgorithm\"\xac\x01\n" + + "\x13signature_algorithm\x18\x02 \x01(\tR\x12signatureAlgorithm\"\xba\x01\n" + + "\x16AgentRequestAcceptance\x12H\n" + + "\apayload\x18\x01 \x01(\v2&.ramp.v1.AgentRequestAcceptancePayloadB\x06\xbaH\x03\xc8\x01\x01R\apayload\x12%\n" + + "\tsignature\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\tsignature\x12/\n" + + "\x13signature_algorithm\x18\x03 \x01(\tR\x12signatureAlgorithm\"g\n" + + "\x1aAgentRequestAcceptanceItem\x12$\n" + + "\toffer_sig\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\bofferSig\x12#\n" + + "\bexchange\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\bexchange\"\xde\x01\n" + + "\x1dAgentRequestAcceptancePayload\x12F\n" + + "\x05items\x18\x01 \x03(\v2#.ramp.v1.AgentRequestAcceptanceItemB\v\xbaH\b\x92\x01\x05\b\x01\x10\x80\x02R\x05items\x12!\n" + + "\frequester_id\x18\x02 \x01(\tR\vrequesterId\x12)\n" + + "\x10requester_domain\x18\x03 \x01(\tR\x0frequesterDomain\x12'\n" + + "\x0fidempotency_key\x18\x04 \x01(\tR\x0eidempotencyKey\"\xac\x01\n" + "\x16AgentAcceptancePayload\x12\x1b\n" + "\toffer_sig\x18\x01 \x01(\tR\bofferSig\x12!\n" + "\frequester_id\x18\x02 \x01(\tR\vrequesterId\x12)\n" + "\x10requester_domain\x18\x03 \x01(\tR\x0frequesterDomain\x12'\n" + - "\x0fidempotency_key\x18\x04 \x01(\tR\x0eidempotencyKey\"\x95\x02\n" + + "\x0fidempotency_key\x18\x04 \x01(\tR\x0eidempotencyKey\"\x92\x03\n" + "\x12TransactionRequest\x12\x10\n" + "\x03ver\x18\x01 \x01(\tR\x03ver\x123\n" + "\x0fidempotency_key\x18\x02 \x01(\tB\n" + "\xbaH\ar\x05\x10\x01\x18\xff\x01R\x0eidempotencyKey\x120\n" + "\trequester\x18\x04 \x01(\v2\x12.ramp.v1.RequesterR\trequester\x128\n" + - "\x05items\x18\a \x03(\v2\x18.ramp.v1.TransactionItemB\b\xbaH\x05\x92\x01\x02\b\x01R\x05items\x12)\n" + + "\x05items\x18\a \x03(\v2\x18.ramp.v1.TransactionItemB\b\xbaH\x05\x92\x01\x02\b\x01R\x05items\x12^\n" + + "\x18agent_request_acceptance\x18\b \x01(\v2\x1f.ramp.v1.AgentRequestAcceptanceH\x00R\x16agentRequestAcceptance\x88\x01\x01\x12)\n" + "\x03ext\x18\x0f \x01(\v2\x17.google.protobuf.StructR\x03ext\x12!\n" + - "\fext_critical\x18Z \x03(\tR\vextCritical\"\x9e\x01\n" + + "\fext_critical\x18Z \x03(\tR\vextCriticalB\x1b\n" + + "\x19_agent_request_acceptance\"\x9e\x01\n" + "\x0fTransactionItem\x12,\n" + "\x05offer\x18\x03 \x01(\v2\x0e.ramp.v1.OfferB\x06\xbaH\x03\xc8\x01\x01R\x05offer\x12H\n" + "\x10agent_acceptance\x18\x04 \x01(\v2\x18.ramp.v1.AgentAcceptanceH\x00R\x0fagentAcceptance\x88\x01\x01B\x13\n" + @@ -10629,7 +10889,7 @@ func file_ramp_v1_ramp_proto_rawDescGZIP() []byte { } var file_ramp_v1_ramp_proto_enumTypes = make([]protoimpl.EnumInfo, 28) -var file_ramp_v1_ramp_proto_msgTypes = make([]protoimpl.MessageInfo, 68) +var file_ramp_v1_ramp_proto_msgTypes = make([]protoimpl.MessageInfo, 71) var file_ramp_v1_ramp_proto_goTypes = []any{ (DiscoveryMethod)(0), // 0: ramp.v1.DiscoveryMethod (OfferAbsenceReason)(0), // 1: ramp.v1.OfferAbsenceReason @@ -10678,92 +10938,95 @@ var file_ramp_v1_ramp_proto_goTypes = []any{ (*Requester)(nil), // 44: ramp.v1.Requester (*Delegation)(nil), // 45: ramp.v1.Delegation (*AgentAcceptance)(nil), // 46: ramp.v1.AgentAcceptance - (*AgentAcceptancePayload)(nil), // 47: ramp.v1.AgentAcceptancePayload - (*TransactionRequest)(nil), // 48: ramp.v1.TransactionRequest - (*TransactionItem)(nil), // 49: ramp.v1.TransactionItem - (*TransactionResponse)(nil), // 50: ramp.v1.TransactionResponse - (*TransactionResultItem)(nil), // 51: ramp.v1.TransactionResultItem - (*Cost)(nil), // 52: ramp.v1.Cost - (*PushResourcesRequest)(nil), // 53: ramp.v1.PushResourcesRequest - (*ResourceEntry)(nil), // 54: ramp.v1.ResourceEntry - (*PushResourcesResponse)(nil), // 55: ramp.v1.PushResourcesResponse - (*RemoveResourcesRequest)(nil), // 56: ramp.v1.RemoveResourcesRequest - (*RemoveResourcesResponse)(nil), // 57: ramp.v1.RemoveResourcesResponse - (*RefreshCatalogRequest)(nil), // 58: ramp.v1.RefreshCatalogRequest - (*RefreshCatalogResponse)(nil), // 59: ramp.v1.RefreshCatalogResponse - (*ReportingObligation)(nil), // 60: ramp.v1.ReportingObligation - (*UsageReport)(nil), // 61: ramp.v1.UsageReport - (*AttributionDetail)(nil), // 62: ramp.v1.AttributionDetail - (*Usage)(nil), // 63: ramp.v1.Usage - (*UsageAsset)(nil), // 64: ramp.v1.UsageAsset - (*UsageReportResponse)(nil), // 65: ramp.v1.UsageReportResponse - (*DiscoveryRequest)(nil), // 66: ramp.v1.DiscoveryRequest - (*RequestConstraints)(nil), // 67: ramp.v1.RequestConstraints - (*JsonWebKey)(nil), // 68: ramp.v1.JsonWebKey - (*AccountRegistration)(nil), // 69: ramp.v1.AccountRegistration - (*WellKnownManifest)(nil), // 70: ramp.v1.WellKnownManifest - (*WBAFile)(nil), // 71: ramp.v1.WBAFile - (*KeyRevocationList)(nil), // 72: ramp.v1.KeyRevocationList - (*CatalogContributor)(nil), // 73: ramp.v1.CatalogContributor - (*AuthorizedExchange)(nil), // 74: ramp.v1.AuthorizedExchange - (*DiscoveryResponse)(nil), // 75: ramp.v1.DiscoveryResponse - (*DisputeRequest)(nil), // 76: ramp.v1.DisputeRequest - (*DisputeResponse)(nil), // 77: ramp.v1.DisputeResponse - (*DomainVerificationRequest)(nil), // 78: ramp.v1.DomainVerificationRequest - (*DomainVerificationChallenge)(nil), // 79: ramp.v1.DomainVerificationChallenge - (*DomainVerificationConfirmation)(nil), // 80: ramp.v1.DomainVerificationConfirmation - (*DomainVerificationResult)(nil), // 81: ramp.v1.DomainVerificationResult - (*RegisterRequest)(nil), // 82: ramp.v1.RegisterRequest - (*RegisterResponse)(nil), // 83: ramp.v1.RegisterResponse - (*GetAccountStatusRequest)(nil), // 84: ramp.v1.GetAccountStatusRequest - (*GetAccountStatusResponse)(nil), // 85: ramp.v1.GetAccountStatusResponse - (*ErrorDetail)(nil), // 86: ramp.v1.ErrorDetail - (*TransactionDenial)(nil), // 87: ramp.v1.TransactionDenial - (*CatalogRejection)(nil), // 88: ramp.v1.CatalogRejection - (*RegistrationFailure)(nil), // 89: ramp.v1.RegistrationFailure - (*RegistrationFieldError)(nil), // 90: ramp.v1.RegistrationFieldError - (*DisputeFailure)(nil), // 91: ramp.v1.DisputeFailure - (*DomainVerificationFailure)(nil), // 92: ramp.v1.DomainVerificationFailure - (*RetrievalAuthFailure)(nil), // 93: ramp.v1.RetrievalAuthFailure - (*UsageReportRejection)(nil), // 94: ramp.v1.UsageReportRejection - nil, // 95: ramp.v1.ErrorDetail.MetadataEntry - (*durationpb.Duration)(nil), // 96: google.protobuf.Duration - (*structpb.Struct)(nil), // 97: google.protobuf.Struct - (*timestamppb.Timestamp)(nil), // 98: google.protobuf.Timestamp + (*AgentRequestAcceptance)(nil), // 47: ramp.v1.AgentRequestAcceptance + (*AgentRequestAcceptanceItem)(nil), // 48: ramp.v1.AgentRequestAcceptanceItem + (*AgentRequestAcceptancePayload)(nil), // 49: ramp.v1.AgentRequestAcceptancePayload + (*AgentAcceptancePayload)(nil), // 50: ramp.v1.AgentAcceptancePayload + (*TransactionRequest)(nil), // 51: ramp.v1.TransactionRequest + (*TransactionItem)(nil), // 52: ramp.v1.TransactionItem + (*TransactionResponse)(nil), // 53: ramp.v1.TransactionResponse + (*TransactionResultItem)(nil), // 54: ramp.v1.TransactionResultItem + (*Cost)(nil), // 55: ramp.v1.Cost + (*PushResourcesRequest)(nil), // 56: ramp.v1.PushResourcesRequest + (*ResourceEntry)(nil), // 57: ramp.v1.ResourceEntry + (*PushResourcesResponse)(nil), // 58: ramp.v1.PushResourcesResponse + (*RemoveResourcesRequest)(nil), // 59: ramp.v1.RemoveResourcesRequest + (*RemoveResourcesResponse)(nil), // 60: ramp.v1.RemoveResourcesResponse + (*RefreshCatalogRequest)(nil), // 61: ramp.v1.RefreshCatalogRequest + (*RefreshCatalogResponse)(nil), // 62: ramp.v1.RefreshCatalogResponse + (*ReportingObligation)(nil), // 63: ramp.v1.ReportingObligation + (*UsageReport)(nil), // 64: ramp.v1.UsageReport + (*AttributionDetail)(nil), // 65: ramp.v1.AttributionDetail + (*Usage)(nil), // 66: ramp.v1.Usage + (*UsageAsset)(nil), // 67: ramp.v1.UsageAsset + (*UsageReportResponse)(nil), // 68: ramp.v1.UsageReportResponse + (*DiscoveryRequest)(nil), // 69: ramp.v1.DiscoveryRequest + (*RequestConstraints)(nil), // 70: ramp.v1.RequestConstraints + (*JsonWebKey)(nil), // 71: ramp.v1.JsonWebKey + (*AccountRegistration)(nil), // 72: ramp.v1.AccountRegistration + (*WellKnownManifest)(nil), // 73: ramp.v1.WellKnownManifest + (*WBAFile)(nil), // 74: ramp.v1.WBAFile + (*KeyRevocationList)(nil), // 75: ramp.v1.KeyRevocationList + (*CatalogContributor)(nil), // 76: ramp.v1.CatalogContributor + (*AuthorizedExchange)(nil), // 77: ramp.v1.AuthorizedExchange + (*DiscoveryResponse)(nil), // 78: ramp.v1.DiscoveryResponse + (*DisputeRequest)(nil), // 79: ramp.v1.DisputeRequest + (*DisputeResponse)(nil), // 80: ramp.v1.DisputeResponse + (*DomainVerificationRequest)(nil), // 81: ramp.v1.DomainVerificationRequest + (*DomainVerificationChallenge)(nil), // 82: ramp.v1.DomainVerificationChallenge + (*DomainVerificationConfirmation)(nil), // 83: ramp.v1.DomainVerificationConfirmation + (*DomainVerificationResult)(nil), // 84: ramp.v1.DomainVerificationResult + (*RegisterRequest)(nil), // 85: ramp.v1.RegisterRequest + (*RegisterResponse)(nil), // 86: ramp.v1.RegisterResponse + (*GetAccountStatusRequest)(nil), // 87: ramp.v1.GetAccountStatusRequest + (*GetAccountStatusResponse)(nil), // 88: ramp.v1.GetAccountStatusResponse + (*ErrorDetail)(nil), // 89: ramp.v1.ErrorDetail + (*TransactionDenial)(nil), // 90: ramp.v1.TransactionDenial + (*CatalogRejection)(nil), // 91: ramp.v1.CatalogRejection + (*RegistrationFailure)(nil), // 92: ramp.v1.RegistrationFailure + (*RegistrationFieldError)(nil), // 93: ramp.v1.RegistrationFieldError + (*DisputeFailure)(nil), // 94: ramp.v1.DisputeFailure + (*DomainVerificationFailure)(nil), // 95: ramp.v1.DomainVerificationFailure + (*RetrievalAuthFailure)(nil), // 96: ramp.v1.RetrievalAuthFailure + (*UsageReportRejection)(nil), // 97: ramp.v1.UsageReportRejection + nil, // 98: ramp.v1.ErrorDetail.MetadataEntry + (*durationpb.Duration)(nil), // 99: google.protobuf.Duration + (*structpb.Struct)(nil), // 100: google.protobuf.Struct + (*timestamppb.Timestamp)(nil), // 101: google.protobuf.Timestamp } var file_ramp_v1_ramp_proto_depIdxs = []int32{ 3, // 0: ramp.v1.AcceptableRestriction.axis:type_name -> ramp.v1.RestrictionKind 44, // 1: ramp.v1.ResourceQuery.requester:type_name -> ramp.v1.Requester 28, // 2: ramp.v1.ResourceQuery.acceptable_restrictions:type_name -> ramp.v1.AcceptableRestriction - 96, // 3: ramp.v1.ResourceQuery.deadline:type_name -> google.protobuf.Duration - 97, // 4: ramp.v1.ResourceQuery.ext:type_name -> google.protobuf.Struct + 99, // 3: ramp.v1.ResourceQuery.deadline:type_name -> google.protobuf.Duration + 100, // 4: ramp.v1.ResourceQuery.ext:type_name -> google.protobuf.Struct 34, // 5: ramp.v1.ResourceResponse.offers:type_name -> ramp.v1.Offer 31, // 6: ramp.v1.ResourceResponse.offer_groups:type_name -> ramp.v1.OfferGroup 32, // 7: ramp.v1.ResourceResponse.rate_limit:type_name -> ramp.v1.RateLimitInfo - 97, // 8: ramp.v1.ResourceResponse.ext:type_name -> google.protobuf.Struct + 100, // 8: ramp.v1.ResourceResponse.ext:type_name -> google.protobuf.Struct 34, // 9: ramp.v1.OfferGroup.offers:type_name -> ramp.v1.Offer 0, // 10: ramp.v1.OfferGroup.discovery_method:type_name -> ramp.v1.DiscoveryMethod 1, // 11: ramp.v1.OfferGroup.absence_reason:type_name -> ramp.v1.OfferAbsenceReason 3, // 12: ramp.v1.OfferGroup.restriction_filters:type_name -> ramp.v1.RestrictionKind - 98, // 13: ramp.v1.RateLimitInfo.reset_at:type_name -> google.protobuf.Timestamp - 96, // 14: ramp.v1.RateLimitInfo.window:type_name -> google.protobuf.Duration - 98, // 15: ramp.v1.SubscriptionQuotaInfo.resets_at:type_name -> google.protobuf.Timestamp + 101, // 13: ramp.v1.RateLimitInfo.reset_at:type_name -> google.protobuf.Timestamp + 99, // 14: ramp.v1.RateLimitInfo.window:type_name -> google.protobuf.Duration + 101, // 15: ramp.v1.SubscriptionQuotaInfo.resets_at:type_name -> google.protobuf.Timestamp 43, // 16: ramp.v1.Offer.pricing:type_name -> ramp.v1.Pricing 9, // 17: ramp.v1.Offer.delivery_method:type_name -> ramp.v1.DeliveryMethod - 60, // 18: ramp.v1.Offer.reporting:type_name -> ramp.v1.ReportingObligation - 98, // 19: ramp.v1.Offer.expires_at:type_name -> google.protobuf.Timestamp + 63, // 18: ramp.v1.Offer.reporting:type_name -> ramp.v1.ReportingObligation + 101, // 19: ramp.v1.Offer.expires_at:type_name -> google.protobuf.Timestamp 35, // 20: ramp.v1.Offer.identity:type_name -> ramp.v1.ResourceIdentity 36, // 21: ramp.v1.Offer.attestations:type_name -> ramp.v1.ResourceAttestation - 98, // 22: ramp.v1.Offer.data_as_of:type_name -> google.protobuf.Timestamp + 101, // 22: ramp.v1.Offer.data_as_of:type_name -> google.protobuf.Timestamp 33, // 23: ramp.v1.Offer.subscription_quota:type_name -> ramp.v1.SubscriptionQuotaInfo 42, // 24: ramp.v1.Offer.previews:type_name -> ramp.v1.Preview 41, // 25: ramp.v1.Offer.terms:type_name -> ramp.v1.LicenseTerm - 97, // 26: ramp.v1.Offer.ext:type_name -> google.protobuf.Struct + 100, // 26: ramp.v1.Offer.ext:type_name -> google.protobuf.Struct 12, // 27: ramp.v1.ResourceIdentity.resource_mutability:type_name -> ramp.v1.ResourceMutability 11, // 28: ramp.v1.ResourceIdentity.c2pa_status:type_name -> ramp.v1.C2PAStatus - 97, // 29: ramp.v1.ResourceIdentity.ext:type_name -> google.protobuf.Struct - 98, // 30: ramp.v1.ResourceAttestation.attested_at:type_name -> google.protobuf.Timestamp - 97, // 31: ramp.v1.ResourceAttestation.claims:type_name -> google.protobuf.Struct + 100, // 29: ramp.v1.ResourceIdentity.ext:type_name -> google.protobuf.Struct + 101, // 30: ramp.v1.ResourceAttestation.attested_at:type_name -> google.protobuf.Timestamp + 100, // 31: ramp.v1.ResourceAttestation.claims:type_name -> google.protobuf.Struct 3, // 32: ramp.v1.Restriction.kind:type_name -> ramp.v1.RestrictionKind 4, // 33: ramp.v1.Quota.window:type_name -> ramp.v1.QuotaWindow 5, // 34: ramp.v1.Obligation.kind:type_name -> ramp.v1.ObligationKind @@ -10779,133 +11042,136 @@ var file_ramp_v1_ramp_proto_depIdxs = []int32{ 8, // 44: ramp.v1.Pricing.metering:type_name -> ramp.v1.PricingMetering 10, // 45: ramp.v1.Requester.type:type_name -> ramp.v1.RequesterType 45, // 46: ramp.v1.Requester.delegation:type_name -> ramp.v1.Delegation - 97, // 47: ramp.v1.Requester.ext:type_name -> google.protobuf.Struct - 98, // 48: ramp.v1.Delegation.expires_at:type_name -> google.protobuf.Timestamp - 96, // 49: ramp.v1.Delegation.quota_period:type_name -> google.protobuf.Duration - 97, // 50: ramp.v1.Delegation.ext:type_name -> google.protobuf.Struct - 44, // 51: ramp.v1.TransactionRequest.requester:type_name -> ramp.v1.Requester - 49, // 52: ramp.v1.TransactionRequest.items:type_name -> ramp.v1.TransactionItem - 97, // 53: ramp.v1.TransactionRequest.ext:type_name -> google.protobuf.Struct - 34, // 54: ramp.v1.TransactionItem.offer:type_name -> ramp.v1.Offer - 46, // 55: ramp.v1.TransactionItem.agent_acceptance:type_name -> ramp.v1.AgentAcceptance - 51, // 56: ramp.v1.TransactionResponse.items:type_name -> ramp.v1.TransactionResultItem - 52, // 57: ramp.v1.TransactionResponse.total_cost:type_name -> ramp.v1.Cost - 33, // 58: ramp.v1.TransactionResponse.subscription_quota:type_name -> ramp.v1.SubscriptionQuotaInfo - 97, // 59: ramp.v1.TransactionResponse.ext:type_name -> google.protobuf.Struct - 52, // 60: ramp.v1.TransactionResultItem.cost:type_name -> ramp.v1.Cost - 52, // 61: ramp.v1.TransactionResultItem.subscription_unit_value:type_name -> ramp.v1.Cost - 13, // 62: ramp.v1.TransactionResultItem.denial_reason:type_name -> ramp.v1.DenialReason - 3, // 63: ramp.v1.TransactionResultItem.restriction_mismatches:type_name -> ramp.v1.RestrictionKind - 98, // 64: ramp.v1.TransactionResultItem.expires_at:type_name -> google.protobuf.Timestamp - 9, // 65: ramp.v1.TransactionResultItem.delivery_method:type_name -> ramp.v1.DeliveryMethod - 60, // 66: ramp.v1.TransactionResultItem.reporting_obligation:type_name -> ramp.v1.ReportingObligation - 54, // 67: ramp.v1.PushResourcesRequest.entries:type_name -> ramp.v1.ResourceEntry - 97, // 68: ramp.v1.PushResourcesRequest.ext:type_name -> google.protobuf.Struct - 14, // 69: ramp.v1.ResourceEntry.source:type_name -> ramp.v1.IngestionSource - 98, // 70: ramp.v1.ResourceEntry.provenance_timestamp:type_name -> google.protobuf.Timestamp - 36, // 71: ramp.v1.ResourceEntry.attestations:type_name -> ramp.v1.ResourceAttestation - 41, // 72: ramp.v1.ResourceEntry.terms:type_name -> ramp.v1.LicenseTerm - 12, // 73: ramp.v1.ResourceEntry.resource_mutability:type_name -> ramp.v1.ResourceMutability - 97, // 74: ramp.v1.ResourceEntry.ext:type_name -> google.protobuf.Struct - 97, // 75: ramp.v1.PushResourcesResponse.ext:type_name -> google.protobuf.Struct - 96, // 76: ramp.v1.ReportingObligation.window:type_name -> google.protobuf.Duration - 97, // 77: ramp.v1.ReportingObligation.ext:type_name -> google.protobuf.Struct - 63, // 78: ramp.v1.UsageReport.usage:type_name -> ramp.v1.Usage - 98, // 79: ramp.v1.UsageReport.timestamp:type_name -> google.protobuf.Timestamp - 64, // 80: ramp.v1.UsageReport.assets:type_name -> ramp.v1.UsageAsset - 97, // 81: ramp.v1.UsageReport.ext:type_name -> google.protobuf.Struct - 15, // 82: ramp.v1.AttributionDetail.format:type_name -> ramp.v1.CitationFormat - 62, // 83: ramp.v1.Usage.attribution:type_name -> ramp.v1.AttributionDetail - 97, // 84: ramp.v1.UsageReportResponse.ext:type_name -> google.protobuf.Struct - 44, // 85: ramp.v1.DiscoveryRequest.requester:type_name -> ramp.v1.Requester - 28, // 86: ramp.v1.DiscoveryRequest.acceptable_restrictions:type_name -> ramp.v1.AcceptableRestriction - 67, // 87: ramp.v1.DiscoveryRequest.constraints:type_name -> ramp.v1.RequestConstraints - 97, // 88: ramp.v1.DiscoveryRequest.search_filters:type_name -> google.protobuf.Struct - 97, // 89: ramp.v1.DiscoveryRequest.ext:type_name -> google.protobuf.Struct - 52, // 90: ramp.v1.RequestConstraints.max_price:type_name -> ramp.v1.Cost - 9, // 91: ramp.v1.RequestConstraints.delivery_preference:type_name -> ramp.v1.DeliveryMethod - 52, // 92: ramp.v1.RequestConstraints.period_budget:type_name -> ramp.v1.Cost - 96, // 93: ramp.v1.RequestConstraints.budget_period:type_name -> google.protobuf.Duration - 96, // 94: ramp.v1.RequestConstraints.max_data_age:type_name -> google.protobuf.Duration - 97, // 95: ramp.v1.AccountRegistration.data_schema:type_name -> google.protobuf.Struct - 16, // 96: ramp.v1.WellKnownManifest.role:type_name -> ramp.v1.Role - 74, // 97: ramp.v1.WellKnownManifest.exchanges:type_name -> ramp.v1.AuthorizedExchange - 73, // 98: ramp.v1.WellKnownManifest.catalog_contributors:type_name -> ramp.v1.CatalogContributor - 7, // 99: ramp.v1.WellKnownManifest.pricing_models_supported:type_name -> ramp.v1.PricingModel - 9, // 100: ramp.v1.WellKnownManifest.delivery_methods_supported:type_name -> ramp.v1.DeliveryMethod - 18, // 101: ramp.v1.WellKnownManifest.supported_auth_methods:type_name -> ramp.v1.AuthMethod - 69, // 102: ramp.v1.WellKnownManifest.account_registration:type_name -> ramp.v1.AccountRegistration - 97, // 103: ramp.v1.WellKnownManifest.ext:type_name -> google.protobuf.Struct - 68, // 104: ramp.v1.WBAFile.keys:type_name -> ramp.v1.JsonWebKey - 98, // 105: ramp.v1.KeyRevocationList.as_of:type_name -> google.protobuf.Timestamp - 17, // 106: ramp.v1.AuthorizedExchange.relationship:type_name -> ramp.v1.ProviderRelationship - 97, // 107: ramp.v1.AuthorizedExchange.ext:type_name -> google.protobuf.Struct - 31, // 108: ramp.v1.DiscoveryResponse.offer_groups:type_name -> ramp.v1.OfferGroup - 1, // 109: ramp.v1.DiscoveryResponse.absence_reason:type_name -> ramp.v1.OfferAbsenceReason - 97, // 110: ramp.v1.DiscoveryResponse.ext:type_name -> google.protobuf.Struct - 19, // 111: ramp.v1.DisputeRequest.reason:type_name -> ramp.v1.DisputeReason - 97, // 112: ramp.v1.DisputeRequest.ext:type_name -> google.protobuf.Struct - 96, // 113: ramp.v1.DisputeResponse.estimated_resolution:type_name -> google.protobuf.Duration - 20, // 114: ramp.v1.DisputeResponse.status:type_name -> ramp.v1.DisputeStatus - 21, // 115: ramp.v1.DisputeResponse.resolution:type_name -> ramp.v1.ResolutionType - 97, // 116: ramp.v1.DisputeResponse.ext:type_name -> google.protobuf.Struct - 97, // 117: ramp.v1.DomainVerificationRequest.ext:type_name -> google.protobuf.Struct - 98, // 118: ramp.v1.DomainVerificationChallenge.expires_at:type_name -> google.protobuf.Timestamp - 97, // 119: ramp.v1.DomainVerificationChallenge.ext:type_name -> google.protobuf.Struct - 97, // 120: ramp.v1.DomainVerificationConfirmation.ext:type_name -> google.protobuf.Struct - 98, // 121: ramp.v1.DomainVerificationResult.valid_until:type_name -> google.protobuf.Timestamp - 97, // 122: ramp.v1.DomainVerificationResult.ext:type_name -> google.protobuf.Struct - 97, // 123: ramp.v1.RegisterRequest.registration_data:type_name -> google.protobuf.Struct - 97, // 124: ramp.v1.RegisterRequest.ext:type_name -> google.protobuf.Struct - 97, // 125: ramp.v1.RegisterResponse.ext:type_name -> google.protobuf.Struct - 97, // 126: ramp.v1.GetAccountStatusRequest.ext:type_name -> google.protobuf.Struct - 97, // 127: ramp.v1.GetAccountStatusResponse.ext:type_name -> google.protobuf.Struct - 95, // 128: ramp.v1.ErrorDetail.metadata:type_name -> ramp.v1.ErrorDetail.MetadataEntry - 87, // 129: ramp.v1.ErrorDetail.transaction_denial:type_name -> ramp.v1.TransactionDenial - 88, // 130: ramp.v1.ErrorDetail.catalog_rejection:type_name -> ramp.v1.CatalogRejection - 89, // 131: ramp.v1.ErrorDetail.registration_failure:type_name -> ramp.v1.RegistrationFailure - 91, // 132: ramp.v1.ErrorDetail.dispute_failure:type_name -> ramp.v1.DisputeFailure - 92, // 133: ramp.v1.ErrorDetail.domain_verification_failure:type_name -> ramp.v1.DomainVerificationFailure - 93, // 134: ramp.v1.ErrorDetail.retrieval_auth_failure:type_name -> ramp.v1.RetrievalAuthFailure - 94, // 135: ramp.v1.ErrorDetail.usage_report_rejection:type_name -> ramp.v1.UsageReportRejection - 13, // 136: ramp.v1.TransactionDenial.reason:type_name -> ramp.v1.DenialReason - 3, // 137: ramp.v1.TransactionDenial.restriction_mismatches:type_name -> ramp.v1.RestrictionKind - 22, // 138: ramp.v1.CatalogRejection.reason:type_name -> ramp.v1.CatalogRejectionReason - 23, // 139: ramp.v1.RegistrationFailure.reason:type_name -> ramp.v1.RegistrationFailureReason - 90, // 140: ramp.v1.RegistrationFailure.field_errors:type_name -> ramp.v1.RegistrationFieldError - 24, // 141: ramp.v1.DisputeFailure.reason:type_name -> ramp.v1.DisputeFailureReason - 25, // 142: ramp.v1.DomainVerificationFailure.reason:type_name -> ramp.v1.DomainVerificationFailureReason - 26, // 143: ramp.v1.RetrievalAuthFailure.reason:type_name -> ramp.v1.RetrievalAuthFailureReason - 27, // 144: ramp.v1.UsageReportRejection.reason:type_name -> ramp.v1.UsageReportRejectionReason - 29, // 145: ramp.v1.ExchangeService.DiscoverResources:input_type -> ramp.v1.ResourceQuery - 48, // 146: ramp.v1.ExchangeService.ExecuteTransaction:input_type -> ramp.v1.TransactionRequest - 61, // 147: ramp.v1.ExchangeService.ReportUsage:input_type -> ramp.v1.UsageReport - 76, // 148: ramp.v1.ExchangeService.DisputeTransaction:input_type -> ramp.v1.DisputeRequest - 78, // 149: ramp.v1.ExchangeService.RequestDomainVerification:input_type -> ramp.v1.DomainVerificationRequest - 80, // 150: ramp.v1.ExchangeService.ConfirmDomainVerification:input_type -> ramp.v1.DomainVerificationConfirmation - 82, // 151: ramp.v1.ExchangeService.Register:input_type -> ramp.v1.RegisterRequest - 84, // 152: ramp.v1.ExchangeService.GetAccountStatus:input_type -> ramp.v1.GetAccountStatusRequest - 53, // 153: ramp.v1.CatalogService.PushResources:input_type -> ramp.v1.PushResourcesRequest - 56, // 154: ramp.v1.CatalogService.RemoveResources:input_type -> ramp.v1.RemoveResourcesRequest - 58, // 155: ramp.v1.CatalogService.RefreshCatalog:input_type -> ramp.v1.RefreshCatalogRequest - 66, // 156: ramp.v1.BrokerService.Resolve:input_type -> ramp.v1.DiscoveryRequest - 30, // 157: ramp.v1.ExchangeService.DiscoverResources:output_type -> ramp.v1.ResourceResponse - 50, // 158: ramp.v1.ExchangeService.ExecuteTransaction:output_type -> ramp.v1.TransactionResponse - 65, // 159: ramp.v1.ExchangeService.ReportUsage:output_type -> ramp.v1.UsageReportResponse - 77, // 160: ramp.v1.ExchangeService.DisputeTransaction:output_type -> ramp.v1.DisputeResponse - 79, // 161: ramp.v1.ExchangeService.RequestDomainVerification:output_type -> ramp.v1.DomainVerificationChallenge - 81, // 162: ramp.v1.ExchangeService.ConfirmDomainVerification:output_type -> ramp.v1.DomainVerificationResult - 83, // 163: ramp.v1.ExchangeService.Register:output_type -> ramp.v1.RegisterResponse - 85, // 164: ramp.v1.ExchangeService.GetAccountStatus:output_type -> ramp.v1.GetAccountStatusResponse - 55, // 165: ramp.v1.CatalogService.PushResources:output_type -> ramp.v1.PushResourcesResponse - 57, // 166: ramp.v1.CatalogService.RemoveResources:output_type -> ramp.v1.RemoveResourcesResponse - 59, // 167: ramp.v1.CatalogService.RefreshCatalog:output_type -> ramp.v1.RefreshCatalogResponse - 75, // 168: ramp.v1.BrokerService.Resolve:output_type -> ramp.v1.DiscoveryResponse - 157, // [157:169] is the sub-list for method output_type - 145, // [145:157] is the sub-list for method input_type - 145, // [145:145] is the sub-list for extension type_name - 145, // [145:145] is the sub-list for extension extendee - 0, // [0:145] is the sub-list for field type_name + 100, // 47: ramp.v1.Requester.ext:type_name -> google.protobuf.Struct + 101, // 48: ramp.v1.Delegation.expires_at:type_name -> google.protobuf.Timestamp + 99, // 49: ramp.v1.Delegation.quota_period:type_name -> google.protobuf.Duration + 100, // 50: ramp.v1.Delegation.ext:type_name -> google.protobuf.Struct + 49, // 51: ramp.v1.AgentRequestAcceptance.payload:type_name -> ramp.v1.AgentRequestAcceptancePayload + 48, // 52: ramp.v1.AgentRequestAcceptancePayload.items:type_name -> ramp.v1.AgentRequestAcceptanceItem + 44, // 53: ramp.v1.TransactionRequest.requester:type_name -> ramp.v1.Requester + 52, // 54: ramp.v1.TransactionRequest.items:type_name -> ramp.v1.TransactionItem + 47, // 55: ramp.v1.TransactionRequest.agent_request_acceptance:type_name -> ramp.v1.AgentRequestAcceptance + 100, // 56: ramp.v1.TransactionRequest.ext:type_name -> google.protobuf.Struct + 34, // 57: ramp.v1.TransactionItem.offer:type_name -> ramp.v1.Offer + 46, // 58: ramp.v1.TransactionItem.agent_acceptance:type_name -> ramp.v1.AgentAcceptance + 54, // 59: ramp.v1.TransactionResponse.items:type_name -> ramp.v1.TransactionResultItem + 55, // 60: ramp.v1.TransactionResponse.total_cost:type_name -> ramp.v1.Cost + 33, // 61: ramp.v1.TransactionResponse.subscription_quota:type_name -> ramp.v1.SubscriptionQuotaInfo + 100, // 62: ramp.v1.TransactionResponse.ext:type_name -> google.protobuf.Struct + 55, // 63: ramp.v1.TransactionResultItem.cost:type_name -> ramp.v1.Cost + 55, // 64: ramp.v1.TransactionResultItem.subscription_unit_value:type_name -> ramp.v1.Cost + 13, // 65: ramp.v1.TransactionResultItem.denial_reason:type_name -> ramp.v1.DenialReason + 3, // 66: ramp.v1.TransactionResultItem.restriction_mismatches:type_name -> ramp.v1.RestrictionKind + 101, // 67: ramp.v1.TransactionResultItem.expires_at:type_name -> google.protobuf.Timestamp + 9, // 68: ramp.v1.TransactionResultItem.delivery_method:type_name -> ramp.v1.DeliveryMethod + 63, // 69: ramp.v1.TransactionResultItem.reporting_obligation:type_name -> ramp.v1.ReportingObligation + 57, // 70: ramp.v1.PushResourcesRequest.entries:type_name -> ramp.v1.ResourceEntry + 100, // 71: ramp.v1.PushResourcesRequest.ext:type_name -> google.protobuf.Struct + 14, // 72: ramp.v1.ResourceEntry.source:type_name -> ramp.v1.IngestionSource + 101, // 73: ramp.v1.ResourceEntry.provenance_timestamp:type_name -> google.protobuf.Timestamp + 36, // 74: ramp.v1.ResourceEntry.attestations:type_name -> ramp.v1.ResourceAttestation + 41, // 75: ramp.v1.ResourceEntry.terms:type_name -> ramp.v1.LicenseTerm + 12, // 76: ramp.v1.ResourceEntry.resource_mutability:type_name -> ramp.v1.ResourceMutability + 100, // 77: ramp.v1.ResourceEntry.ext:type_name -> google.protobuf.Struct + 100, // 78: ramp.v1.PushResourcesResponse.ext:type_name -> google.protobuf.Struct + 99, // 79: ramp.v1.ReportingObligation.window:type_name -> google.protobuf.Duration + 100, // 80: ramp.v1.ReportingObligation.ext:type_name -> google.protobuf.Struct + 66, // 81: ramp.v1.UsageReport.usage:type_name -> ramp.v1.Usage + 101, // 82: ramp.v1.UsageReport.timestamp:type_name -> google.protobuf.Timestamp + 67, // 83: ramp.v1.UsageReport.assets:type_name -> ramp.v1.UsageAsset + 100, // 84: ramp.v1.UsageReport.ext:type_name -> google.protobuf.Struct + 15, // 85: ramp.v1.AttributionDetail.format:type_name -> ramp.v1.CitationFormat + 65, // 86: ramp.v1.Usage.attribution:type_name -> ramp.v1.AttributionDetail + 100, // 87: ramp.v1.UsageReportResponse.ext:type_name -> google.protobuf.Struct + 44, // 88: ramp.v1.DiscoveryRequest.requester:type_name -> ramp.v1.Requester + 28, // 89: ramp.v1.DiscoveryRequest.acceptable_restrictions:type_name -> ramp.v1.AcceptableRestriction + 70, // 90: ramp.v1.DiscoveryRequest.constraints:type_name -> ramp.v1.RequestConstraints + 100, // 91: ramp.v1.DiscoveryRequest.search_filters:type_name -> google.protobuf.Struct + 100, // 92: ramp.v1.DiscoveryRequest.ext:type_name -> google.protobuf.Struct + 55, // 93: ramp.v1.RequestConstraints.max_price:type_name -> ramp.v1.Cost + 9, // 94: ramp.v1.RequestConstraints.delivery_preference:type_name -> ramp.v1.DeliveryMethod + 55, // 95: ramp.v1.RequestConstraints.period_budget:type_name -> ramp.v1.Cost + 99, // 96: ramp.v1.RequestConstraints.budget_period:type_name -> google.protobuf.Duration + 99, // 97: ramp.v1.RequestConstraints.max_data_age:type_name -> google.protobuf.Duration + 100, // 98: ramp.v1.AccountRegistration.data_schema:type_name -> google.protobuf.Struct + 16, // 99: ramp.v1.WellKnownManifest.role:type_name -> ramp.v1.Role + 77, // 100: ramp.v1.WellKnownManifest.exchanges:type_name -> ramp.v1.AuthorizedExchange + 76, // 101: ramp.v1.WellKnownManifest.catalog_contributors:type_name -> ramp.v1.CatalogContributor + 7, // 102: ramp.v1.WellKnownManifest.pricing_models_supported:type_name -> ramp.v1.PricingModel + 9, // 103: ramp.v1.WellKnownManifest.delivery_methods_supported:type_name -> ramp.v1.DeliveryMethod + 18, // 104: ramp.v1.WellKnownManifest.supported_auth_methods:type_name -> ramp.v1.AuthMethod + 72, // 105: ramp.v1.WellKnownManifest.account_registration:type_name -> ramp.v1.AccountRegistration + 100, // 106: ramp.v1.WellKnownManifest.ext:type_name -> google.protobuf.Struct + 71, // 107: ramp.v1.WBAFile.keys:type_name -> ramp.v1.JsonWebKey + 101, // 108: ramp.v1.KeyRevocationList.as_of:type_name -> google.protobuf.Timestamp + 17, // 109: ramp.v1.AuthorizedExchange.relationship:type_name -> ramp.v1.ProviderRelationship + 100, // 110: ramp.v1.AuthorizedExchange.ext:type_name -> google.protobuf.Struct + 31, // 111: ramp.v1.DiscoveryResponse.offer_groups:type_name -> ramp.v1.OfferGroup + 1, // 112: ramp.v1.DiscoveryResponse.absence_reason:type_name -> ramp.v1.OfferAbsenceReason + 100, // 113: ramp.v1.DiscoveryResponse.ext:type_name -> google.protobuf.Struct + 19, // 114: ramp.v1.DisputeRequest.reason:type_name -> ramp.v1.DisputeReason + 100, // 115: ramp.v1.DisputeRequest.ext:type_name -> google.protobuf.Struct + 99, // 116: ramp.v1.DisputeResponse.estimated_resolution:type_name -> google.protobuf.Duration + 20, // 117: ramp.v1.DisputeResponse.status:type_name -> ramp.v1.DisputeStatus + 21, // 118: ramp.v1.DisputeResponse.resolution:type_name -> ramp.v1.ResolutionType + 100, // 119: ramp.v1.DisputeResponse.ext:type_name -> google.protobuf.Struct + 100, // 120: ramp.v1.DomainVerificationRequest.ext:type_name -> google.protobuf.Struct + 101, // 121: ramp.v1.DomainVerificationChallenge.expires_at:type_name -> google.protobuf.Timestamp + 100, // 122: ramp.v1.DomainVerificationChallenge.ext:type_name -> google.protobuf.Struct + 100, // 123: ramp.v1.DomainVerificationConfirmation.ext:type_name -> google.protobuf.Struct + 101, // 124: ramp.v1.DomainVerificationResult.valid_until:type_name -> google.protobuf.Timestamp + 100, // 125: ramp.v1.DomainVerificationResult.ext:type_name -> google.protobuf.Struct + 100, // 126: ramp.v1.RegisterRequest.registration_data:type_name -> google.protobuf.Struct + 100, // 127: ramp.v1.RegisterRequest.ext:type_name -> google.protobuf.Struct + 100, // 128: ramp.v1.RegisterResponse.ext:type_name -> google.protobuf.Struct + 100, // 129: ramp.v1.GetAccountStatusRequest.ext:type_name -> google.protobuf.Struct + 100, // 130: ramp.v1.GetAccountStatusResponse.ext:type_name -> google.protobuf.Struct + 98, // 131: ramp.v1.ErrorDetail.metadata:type_name -> ramp.v1.ErrorDetail.MetadataEntry + 90, // 132: ramp.v1.ErrorDetail.transaction_denial:type_name -> ramp.v1.TransactionDenial + 91, // 133: ramp.v1.ErrorDetail.catalog_rejection:type_name -> ramp.v1.CatalogRejection + 92, // 134: ramp.v1.ErrorDetail.registration_failure:type_name -> ramp.v1.RegistrationFailure + 94, // 135: ramp.v1.ErrorDetail.dispute_failure:type_name -> ramp.v1.DisputeFailure + 95, // 136: ramp.v1.ErrorDetail.domain_verification_failure:type_name -> ramp.v1.DomainVerificationFailure + 96, // 137: ramp.v1.ErrorDetail.retrieval_auth_failure:type_name -> ramp.v1.RetrievalAuthFailure + 97, // 138: ramp.v1.ErrorDetail.usage_report_rejection:type_name -> ramp.v1.UsageReportRejection + 13, // 139: ramp.v1.TransactionDenial.reason:type_name -> ramp.v1.DenialReason + 3, // 140: ramp.v1.TransactionDenial.restriction_mismatches:type_name -> ramp.v1.RestrictionKind + 22, // 141: ramp.v1.CatalogRejection.reason:type_name -> ramp.v1.CatalogRejectionReason + 23, // 142: ramp.v1.RegistrationFailure.reason:type_name -> ramp.v1.RegistrationFailureReason + 93, // 143: ramp.v1.RegistrationFailure.field_errors:type_name -> ramp.v1.RegistrationFieldError + 24, // 144: ramp.v1.DisputeFailure.reason:type_name -> ramp.v1.DisputeFailureReason + 25, // 145: ramp.v1.DomainVerificationFailure.reason:type_name -> ramp.v1.DomainVerificationFailureReason + 26, // 146: ramp.v1.RetrievalAuthFailure.reason:type_name -> ramp.v1.RetrievalAuthFailureReason + 27, // 147: ramp.v1.UsageReportRejection.reason:type_name -> ramp.v1.UsageReportRejectionReason + 29, // 148: ramp.v1.ExchangeService.DiscoverResources:input_type -> ramp.v1.ResourceQuery + 51, // 149: ramp.v1.ExchangeService.ExecuteTransaction:input_type -> ramp.v1.TransactionRequest + 64, // 150: ramp.v1.ExchangeService.ReportUsage:input_type -> ramp.v1.UsageReport + 79, // 151: ramp.v1.ExchangeService.DisputeTransaction:input_type -> ramp.v1.DisputeRequest + 81, // 152: ramp.v1.ExchangeService.RequestDomainVerification:input_type -> ramp.v1.DomainVerificationRequest + 83, // 153: ramp.v1.ExchangeService.ConfirmDomainVerification:input_type -> ramp.v1.DomainVerificationConfirmation + 85, // 154: ramp.v1.ExchangeService.Register:input_type -> ramp.v1.RegisterRequest + 87, // 155: ramp.v1.ExchangeService.GetAccountStatus:input_type -> ramp.v1.GetAccountStatusRequest + 56, // 156: ramp.v1.CatalogService.PushResources:input_type -> ramp.v1.PushResourcesRequest + 59, // 157: ramp.v1.CatalogService.RemoveResources:input_type -> ramp.v1.RemoveResourcesRequest + 61, // 158: ramp.v1.CatalogService.RefreshCatalog:input_type -> ramp.v1.RefreshCatalogRequest + 69, // 159: ramp.v1.BrokerService.Resolve:input_type -> ramp.v1.DiscoveryRequest + 30, // 160: ramp.v1.ExchangeService.DiscoverResources:output_type -> ramp.v1.ResourceResponse + 53, // 161: ramp.v1.ExchangeService.ExecuteTransaction:output_type -> ramp.v1.TransactionResponse + 68, // 162: ramp.v1.ExchangeService.ReportUsage:output_type -> ramp.v1.UsageReportResponse + 80, // 163: ramp.v1.ExchangeService.DisputeTransaction:output_type -> ramp.v1.DisputeResponse + 82, // 164: ramp.v1.ExchangeService.RequestDomainVerification:output_type -> ramp.v1.DomainVerificationChallenge + 84, // 165: ramp.v1.ExchangeService.ConfirmDomainVerification:output_type -> ramp.v1.DomainVerificationResult + 86, // 166: ramp.v1.ExchangeService.Register:output_type -> ramp.v1.RegisterResponse + 88, // 167: ramp.v1.ExchangeService.GetAccountStatus:output_type -> ramp.v1.GetAccountStatusResponse + 58, // 168: ramp.v1.CatalogService.PushResources:output_type -> ramp.v1.PushResourcesResponse + 60, // 169: ramp.v1.CatalogService.RemoveResources:output_type -> ramp.v1.RemoveResourcesResponse + 62, // 170: ramp.v1.CatalogService.RefreshCatalog:output_type -> ramp.v1.RefreshCatalogResponse + 78, // 171: ramp.v1.BrokerService.Resolve:output_type -> ramp.v1.DiscoveryResponse + 160, // [160:172] is the sub-list for method output_type + 148, // [148:160] is the sub-list for method input_type + 148, // [148:148] is the sub-list for extension type_name + 148, // [148:148] is the sub-list for extension extendee + 0, // [0:148] is the sub-list for field type_name } func init() { file_ramp_v1_ramp_proto_init() } @@ -10928,28 +11194,29 @@ func file_ramp_v1_ramp_proto_init() { file_ramp_v1_ramp_proto_msgTypes[15].OneofWrappers = []any{} file_ramp_v1_ramp_proto_msgTypes[16].OneofWrappers = []any{} file_ramp_v1_ramp_proto_msgTypes[17].OneofWrappers = []any{} - file_ramp_v1_ramp_proto_msgTypes[21].OneofWrappers = []any{} - file_ramp_v1_ramp_proto_msgTypes[22].OneofWrappers = []any{} file_ramp_v1_ramp_proto_msgTypes[23].OneofWrappers = []any{} file_ramp_v1_ramp_proto_msgTypes[24].OneofWrappers = []any{} + file_ramp_v1_ramp_proto_msgTypes[25].OneofWrappers = []any{} file_ramp_v1_ramp_proto_msgTypes[26].OneofWrappers = []any{} - file_ramp_v1_ramp_proto_msgTypes[32].OneofWrappers = []any{} - file_ramp_v1_ramp_proto_msgTypes[34].OneofWrappers = []any{} + file_ramp_v1_ramp_proto_msgTypes[27].OneofWrappers = []any{} + file_ramp_v1_ramp_proto_msgTypes[29].OneofWrappers = []any{} file_ramp_v1_ramp_proto_msgTypes[35].OneofWrappers = []any{} - file_ramp_v1_ramp_proto_msgTypes[36].OneofWrappers = []any{} + file_ramp_v1_ramp_proto_msgTypes[37].OneofWrappers = []any{} file_ramp_v1_ramp_proto_msgTypes[38].OneofWrappers = []any{} file_ramp_v1_ramp_proto_msgTypes[39].OneofWrappers = []any{} + file_ramp_v1_ramp_proto_msgTypes[41].OneofWrappers = []any{} file_ramp_v1_ramp_proto_msgTypes[42].OneofWrappers = []any{} - file_ramp_v1_ramp_proto_msgTypes[43].OneofWrappers = []any{} - file_ramp_v1_ramp_proto_msgTypes[47].OneofWrappers = []any{} - file_ramp_v1_ramp_proto_msgTypes[48].OneofWrappers = []any{} - file_ramp_v1_ramp_proto_msgTypes[49].OneofWrappers = []any{} + file_ramp_v1_ramp_proto_msgTypes[45].OneofWrappers = []any{} + file_ramp_v1_ramp_proto_msgTypes[46].OneofWrappers = []any{} file_ramp_v1_ramp_proto_msgTypes[50].OneofWrappers = []any{} + file_ramp_v1_ramp_proto_msgTypes[51].OneofWrappers = []any{} file_ramp_v1_ramp_proto_msgTypes[52].OneofWrappers = []any{} file_ramp_v1_ramp_proto_msgTypes[53].OneofWrappers = []any{} - file_ramp_v1_ramp_proto_msgTypes[54].OneofWrappers = []any{} + file_ramp_v1_ramp_proto_msgTypes[55].OneofWrappers = []any{} + file_ramp_v1_ramp_proto_msgTypes[56].OneofWrappers = []any{} file_ramp_v1_ramp_proto_msgTypes[57].OneofWrappers = []any{} - file_ramp_v1_ramp_proto_msgTypes[58].OneofWrappers = []any{ + file_ramp_v1_ramp_proto_msgTypes[60].OneofWrappers = []any{} + file_ramp_v1_ramp_proto_msgTypes[61].OneofWrappers = []any{ (*ErrorDetail_TransactionDenial)(nil), (*ErrorDetail_CatalogRejection)(nil), (*ErrorDetail_RegistrationFailure)(nil), @@ -10958,14 +11225,14 @@ func file_ramp_v1_ramp_proto_init() { (*ErrorDetail_RetrievalAuthFailure)(nil), (*ErrorDetail_UsageReportRejection)(nil), } - file_ramp_v1_ramp_proto_msgTypes[59].OneofWrappers = []any{} + file_ramp_v1_ramp_proto_msgTypes[62].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ramp_v1_ramp_proto_rawDesc), len(file_ramp_v1_ramp_proto_rawDesc)), NumEnums: 28, - NumMessages: 68, + NumMessages: 71, NumExtensions: 0, NumServices: 3, }, diff --git a/gen/python/wire/models.py b/gen/python/wire/models.py index 47e460d1..8897e863 100644 --- a/gen/python/wire/models.py +++ b/gen/python/wire/models.py @@ -45,6 +45,23 @@ class AgentAcceptancePayload(WireModel): ) +class AgentRequestAcceptanceItem(WireModel): + exchange: constr(min_length=1) + offer_sig: constr(min_length=1) + + +class AgentRequestAcceptancePayload(WireModel): + idempotency_key: str | None = '' + items: list[AgentRequestAcceptanceItem] | None = Field( + None, + description="Complete original request order, before Broker fan-out. Capped at 256 —\n the same ceiling a discovery query's uris list carries, so one request\n can reference at most one offer per queried URI at the query cap. The Go\n verification helper enforces the same bound itself before doing any\n canonicalization work, because a verifier may run with wire validation\n off and the canonical rendering of an unbounded list is the expensive\n step an unauthenticated caller could otherwise buy for free.", + max_length=256, + min_length=1, + ) + requester_domain: str | None = '' + requester_id: str | None = '' + + class AuthMethod(Enum): AUTH_METHOD_GNAP = 'AUTH_METHOD_GNAP' AUTH_METHOD_OAUTH_DPOP = 'AUTH_METHOD_OAUTH_DPOP' @@ -1172,6 +1189,20 @@ class AcceptableRestriction(WireModel): ) +class AgentRequestAcceptance(WireModel): + payload: AgentRequestAcceptancePayload = Field( + ..., + description='The signed payload is carried because a projected subrequest does not carry\n offers addressed to other Exchanges and therefore cannot reconstruct the\n original complete set by itself.', + ) + signature: constr(min_length=1) = Field( + ..., + description='Hex-encoded detached Ed25519 signature over the canonical payload bytes.', + ) + signature_algorithm: str | None = Field( + '', description='Signature algorithm; "EdDSA" for Ed25519.' + ) + + class AttributionDetail(WireModel): displayed_url: str | None = Field( None, description='URL displayed to the user as the attribution link.' @@ -2037,6 +2068,10 @@ class TransactionItem(WireModel): class TransactionRequest(WireModel): + agent_request_acceptance: AgentRequestAcceptance | None = Field( + None, + description='Optional for wire compatibility. When present, an Exchange verifies this\n before creating or serving request-level idempotency state. A Broker MUST\n forward it unchanged on every projected subrequest. Older clients that omit\n it retain per-item execution semantics but receive no request-level claim.', + ) ext: dict[str, Any] | None = Field(None, description='Extension point') ext_critical: list[str] | None = Field( None, diff --git a/gen/ts/wire/schemas.ts b/gen/ts/wire/schemas.ts index 5d4aafb5..823c9dba 100644 --- a/gen/ts/wire/schemas.ts +++ b/gen/ts/wire/schemas.ts @@ -14,6 +14,12 @@ export const AgentAcceptanceSchema = wire(z.object({ "signature": z.string().min export const AgentAcceptancePayloadSchema = wire(z.object({ "idempotency_key": z.string().describe("The transaction's idempotency key — binds the acceptance to a single\n execute so it cannot be replayed under a different transaction.").default(""), "offer_sig": z.string().describe("The accepted Offer's signature (Offer.signature). Anchors the whole signed\n offer without re-serializing its terms/pricing/expiry.").default(""), "requester_domain": z.string().describe("Requester domain (Requester.domain) the acceptance is bound to.").default(""), "requester_id": z.string().describe("Requester identity (Requester.id) the acceptance is bound to.").default("") }).describe("AgentAcceptancePayload — the canonical signing structure for AgentAcceptance.\n It is NEVER sent on the wire; it exists solely so the signer (SDK) and the\n verifier (Exchange) derive BYTE-IDENTICAL signed bytes from the same proto\n schema. This message fixes the FIELD SET; the byte layout is the canonical\n signing form defined on Offer.signature — RFC 8785 JCS over canonical\n proto-JSON with the pinned option set. Underspecifying either half is the top\n cross-implementation drift risk, so both are pinned normatively.\n\nField provenance when building the payload for an execute request:\n - offer_sig = the accepted Offer.signature (the Exchange's hex\n signature; transitively binds pricing, terms,\n expires_at, and — via the offer — the issuing Exchange)\n - requester_id = TransactionRequest.requester.id\n - requester_domain = TransactionRequest.requester.domain\n - idempotency_key = TransactionRequest.idempotency_key\n For batch mode, requester_* and idempotency_key come from the ENCLOSING\n TransactionRequest (a TransactionItem carries neither); offer_sig is the\n per-item Offer.signature.")); +export const AgentRequestAcceptanceSchema = wire(z.object({ "payload": z.object({ "idempotency_key": z.string().default(""), "items": z.array(z.object({ "exchange": z.string().min(1), "offer_sig": z.string().min(1) }).describe("AgentRequestAcceptanceItem is the minimum reference needed to authorize an\n offer's membership, order, and fan-out destination without repeating the\n full Offer in every projected subrequest. Offer.signature transitively binds\n the full offer, including its exchange field; the explicit exchange lets a\n recipient derive which signed references must appear in its projection.")).min(1).max(256).describe("Complete original request order, before Broker fan-out. Capped at 256 —\n the same ceiling a discovery query's uris list carries, so one request\n can reference at most one offer per queried URI at the query cap. The Go\n verification helper enforces the same bound itself before doing any\n canonicalization work, because a verifier may run with wire validation\n off and the canonical rendering of an unbounded list is the expensive\n step an unauthenticated caller could otherwise buy for free.").optional(), "requester_domain": z.string().default(""), "requester_id": z.string().default("") }).describe("The signed payload is carried because a projected subrequest does not carry\n offers addressed to other Exchanges and therefore cannot reconstruct the\n original complete set by itself."), "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the canonical payload bytes."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("AgentRequestAcceptance — the agent's topology-independent authorization of\n one complete ordered execute set. A Broker forwards this envelope unchanged\n when it projects a mixed-Exchange request into per-Exchange subrequests.\n Each receiving Exchange verifies the signature, then requires its subrequest\n to equal the complete in-order projection of payload.items whose exchange\n names that Exchange. This is what makes removal, append, and reorder visible\n before request-level idempotency state is claimed.\n\nA projected subrequest is a NEW HTTP request its sender authors. The party\n that projects — a Broker, or the agent itself when it splits its own\n mixed-Exchange set — computes that subrequest's Content-Digest and signs it\n with its own RFC 9421 request signature. The agent's original RFC 9421\n signature covered the body the agent sent and does not travel with a\n projected body; that is expected, not a gap, and it is why this proof\n exists: like AgentAcceptance, it is a detached body signature that stays\n valid however the request travels. The hop-signature stack applies only to\n requests forwarded byte-for-byte. If a delegation rides the request, the\n holder-binding rule is unchanged — the wire signer must be the delegation's\n terminal holder — so a Broker may project a delegated request only when the\n agent has delegated to the Broker's key.\n\n The verification key is the agent key published for the requester the signed\n payload names. payload.requester_domain must equal the request's\n requester.domain, and the Exchange accepts the signature only if it verifies\n against an Ed25519 key currently valid in that domain's WBA directory\n ({requester_domain}/.well-known/http-message-signatures-directory), fetched\n under the same SSRF discipline as every directory fetch. The envelope\n carries no keyid, so the verifier tries the currently-valid Ed25519 keys of\n that directory; rotation overlap keeps that set small. When the requester\n itself signed the arriving request, the request-signing key the Exchange\n already resolved is that key, and no second fetch is needed. A signature\n that verifies against a key the requester's domain publishes is what turns\n the claimed requester identity into an authenticated one.")); + +export const AgentRequestAcceptanceItemSchema = wire(z.object({ "exchange": z.string().min(1), "offer_sig": z.string().min(1) }).describe("AgentRequestAcceptanceItem is the minimum reference needed to authorize an\n offer's membership, order, and fan-out destination without repeating the\n full Offer in every projected subrequest. Offer.signature transitively binds\n the full offer, including its exchange field; the explicit exchange lets a\n recipient derive which signed references must appear in its projection.")); + +export const AgentRequestAcceptancePayloadSchema = wire(z.object({ "idempotency_key": z.string().default(""), "items": z.array(z.object({ "exchange": z.string().min(1), "offer_sig": z.string().min(1) }).describe("AgentRequestAcceptanceItem is the minimum reference needed to authorize an\n offer's membership, order, and fan-out destination without repeating the\n full Offer in every projected subrequest. Offer.signature transitively binds\n the full offer, including its exchange field; the explicit exchange lets a\n recipient derive which signed references must appear in its projection.")).min(1).max(256).describe("Complete original request order, before Broker fan-out. Capped at 256 —\n the same ceiling a discovery query's uris list carries, so one request\n can reference at most one offer per queried URI at the query cap. The Go\n verification helper enforces the same bound itself before doing any\n canonicalization work, because a verifier may run with wire validation\n off and the canonical rendering of an unbounded list is the expensive\n step an unauthenticated caller could otherwise buy for free.").optional(), "requester_domain": z.string().default(""), "requester_id": z.string().default("") }).describe("AgentRequestAcceptancePayload fixes the field set signed by an\n AgentRequestAcceptance. Its canonical bytes are\n JCS(protojson(AgentRequestAcceptancePayload)) using the canonical-signing\n rules defined on Offer.signature.")); + export const AttributionDetailSchema = wire(z.object({ "displayed_url": z.string().describe("URL displayed to the user as the attribution link.").optional(), "format": z.enum(["CITATION_FORMAT_LINK","CITATION_FORMAT_FOOTNOTE","CITATION_FORMAT_INLINE"]).describe("How the citation was presented.").optional(), "visible_to_user": z.boolean().describe("Whether the attribution was visible to the end user.").optional() }).describe("AttributionDetail — Structured attribution metadata for usage reporting.")); export const AuthMethodSchema = wire(z.enum(["AUTH_METHOD_GNAP","AUTH_METHOD_OAUTH_DPOP","AUTH_METHOD_OAUTH_BEARER","AUTH_METHOD_OAUTH_MTLS"])); @@ -186,7 +192,7 @@ export const TransactionDenialSchema = wire(z.object({ "exchange": z.string().re export const TransactionItemSchema = wire(z.object({ "agent_acceptance": z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload\n bytes (see the canonical-signing definition on Offer.signature)."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("The agent's detached acceptance signature over this item's `offer`.\n Optional on the wire; the Exchange enforces presence per\n item at the service layer for relayed batches. Signed bytes = the canonical\n AgentAcceptancePayload form, with requester_* and idempotency_key\n taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.").optional(), "offer": z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-RAMP-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap that gates whether this LicenseTerm remains valid.\n\nQuotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs enforced. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Delegation scope-gating: the Exchange returns this term to an agent iff the\n agent's delegation grant covers ALL of these scopes (AND-semantics).\n Empty = public. A subscription term is Pricing{model:FREE} +\n scopes:[\"subscription:...\"].\n\nCoverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("The FULL signed Offer for this batch entry, reflected back exactly as\n received at discovery. The Exchange verifies `offer.signature` over these\n presented bytes — stateless, no reconstruct-from-catalog. REQUIRED: every\n batch item carries its offer.") }).describe("TransactionItem — A single offer commitment within a batch transaction.")); -export const TransactionRequestSchema = wire(z.object({ "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "idempotency_key": z.string().min(1).max(255).describe("Idempotency key (REQUIRED). The server MUST dedupe on this: a replay returns\n the original result rather than re-executing. The transaction's durable\n identity is the Exchange-assigned transaction_id in the response.\n Uniqueness is scoped to the verified RFC 9421 signer: the server dedupes per\n (authenticated caller, key), never globally, so a key chosen by one caller\n cannot collide with another's cached result."), "items": z.array(z.object({ "agent_acceptance": z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload\n bytes (see the canonical-signing definition on Offer.signature)."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("The agent's detached acceptance signature over this item's `offer`.\n Optional on the wire; the Exchange enforces presence per\n item at the service layer for relayed batches. Signed bytes = the canonical\n AgentAcceptancePayload form, with requester_* and idempotency_key\n taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.").optional(), "offer": z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-RAMP-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap that gates whether this LicenseTerm remains valid.\n\nQuotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs enforced. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Delegation scope-gating: the Exchange returns this term to an agent iff the\n agent's delegation grant covers ALL of these scopes (AND-semantics).\n Empty = public. A subscription term is Pricing{model:FREE} +\n scopes:[\"subscription:...\"].\n\nCoverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("The FULL signed Offer for this batch entry, reflected back exactly as\n received at discovery. The Exchange verifies `offer.signature` over these\n presented bytes — stateless, no reconstruct-from-catalog. REQUIRED: every\n batch item carries its offer.") }).describe("TransactionItem — A single offer commitment within a batch transaction.")).min(1).describe("The offers committed in this request (REQUIRED, min 1), each carrying its\n own reflected signed Offer + detached acceptance. A single offer is the\n degenerate 1-element list. The Exchange verifies each item's\n `offer.signature` (which covers pricing, terms, and expires_at) over the\n presented bytes against its own key — stateless, self-contained bearer\n tokens, with no reconstruct-from-catalog.").optional(), "requester": z.object({ "delegation": z.object({ "expires_at": z.string().datetime({ offset: true }).describe("When this delegation expires. Exchange MUST reject expired tokens.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Who granted this delegation (domain for public key lookup).").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).").optional(), "scopes": z.array(z.string()).describe("Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]*={0,2}$")).describe("Token bytes. A JWT (base64url-encoded JWS).").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Optional delegation — present when the requester acts on behalf of\n another entity (user, organization, upstream agent).").optional(), "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("Domain the requester belongs to. It carries the same bare-host shape\n \"Request recipient\" defines in the file header, for the same structural\n reason: a scheme, path or query smuggled in here would choose what gets\n fetched, not merely from where. It is NOT how a verifier finds this\n requester's keys: those live in the WBA directory, and verification resolves\n that directory from the COVERED `Signature-Agent` header, never from this\n self-asserted value."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "id": z.string().describe("Unique requester identifier (e.g., \"agent-research-bot-001\").").default(""), "name": z.string().describe("Human-readable name (e.g., \"Acme Research Assistant\").").optional(), "scopes": z.array(z.string()).max(64).describe("Entitlement scopes. Declare what the requester can access.\n\nThe Exchange filters its catalog to resources matching these scopes.\n Resources outside the scopes are not returned — the requester never\n learns they exist. This is the enforcement mechanism for both enterprise\n RBAC and open-market subscription entitlements.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).").optional(), "type": z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"]).describe("What kind of entity is making this request.") }).describe("Requester identity — forwarded for authorization and audit.").optional(), "ver": z.string().describe("RAMP protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("TransactionRequest — Commit to one or more offers.\n\nAfter selecting offers, the caller commits by sending this to the\n Exchange. Supports both single-offer and batch (multi-offer) modes.\n The Exchange validates eligibility, authorizes billing, creates\n delivery, and logs each transaction.")); +export const TransactionRequestSchema = wire(z.object({ "agent_request_acceptance": z.object({ "payload": z.object({ "idempotency_key": z.string().default(""), "items": z.array(z.object({ "exchange": z.string().min(1), "offer_sig": z.string().min(1) }).describe("AgentRequestAcceptanceItem is the minimum reference needed to authorize an\n offer's membership, order, and fan-out destination without repeating the\n full Offer in every projected subrequest. Offer.signature transitively binds\n the full offer, including its exchange field; the explicit exchange lets a\n recipient derive which signed references must appear in its projection.")).min(1).max(256).describe("Complete original request order, before Broker fan-out. Capped at 256 —\n the same ceiling a discovery query's uris list carries, so one request\n can reference at most one offer per queried URI at the query cap. The Go\n verification helper enforces the same bound itself before doing any\n canonicalization work, because a verifier may run with wire validation\n off and the canonical rendering of an unbounded list is the expensive\n step an unauthenticated caller could otherwise buy for free.").optional(), "requester_domain": z.string().default(""), "requester_id": z.string().default("") }).describe("The signed payload is carried because a projected subrequest does not carry\n offers addressed to other Exchanges and therefore cannot reconstruct the\n original complete set by itself."), "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the canonical payload bytes."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("Optional for wire compatibility. When present, an Exchange verifies this\n before creating or serving request-level idempotency state. A Broker MUST\n forward it unchanged on every projected subrequest. Older clients that omit\n it retain per-item execution semantics but receive no request-level claim.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "idempotency_key": z.string().min(1).max(255).describe("Idempotency key (REQUIRED). The server MUST dedupe on this: a replay returns\n the original result rather than re-executing. The transaction's durable\n identity is the Exchange-assigned transaction_id in the response.\n Uniqueness is scoped to the verified RFC 9421 signer: the server dedupes per\n (authenticated caller, key), never globally, so a key chosen by one caller\n cannot collide with another's cached result."), "items": z.array(z.object({ "agent_acceptance": z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload\n bytes (see the canonical-signing definition on Offer.signature)."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("The agent's detached acceptance signature over this item's `offer`.\n Optional on the wire; the Exchange enforces presence per\n item at the service layer for relayed batches. Signed bytes = the canonical\n AgentAcceptancePayload form, with requester_* and idempotency_key\n taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.").optional(), "offer": z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-RAMP-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap that gates whether this LicenseTerm remains valid.\n\nQuotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs enforced. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Delegation scope-gating: the Exchange returns this term to an agent iff the\n agent's delegation grant covers ALL of these scopes (AND-semantics).\n Empty = public. A subscription term is Pricing{model:FREE} +\n scopes:[\"subscription:...\"].\n\nCoverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("The FULL signed Offer for this batch entry, reflected back exactly as\n received at discovery. The Exchange verifies `offer.signature` over these\n presented bytes — stateless, no reconstruct-from-catalog. REQUIRED: every\n batch item carries its offer.") }).describe("TransactionItem — A single offer commitment within a batch transaction.")).min(1).describe("The offers committed in this request (REQUIRED, min 1), each carrying its\n own reflected signed Offer + detached acceptance. A single offer is the\n degenerate 1-element list. The Exchange verifies each item's\n `offer.signature` (which covers pricing, terms, and expires_at) over the\n presented bytes against its own key — stateless, self-contained bearer\n tokens, with no reconstruct-from-catalog.").optional(), "requester": z.object({ "delegation": z.object({ "expires_at": z.string().datetime({ offset: true }).describe("When this delegation expires. Exchange MUST reject expired tokens.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Who granted this delegation (domain for public key lookup).").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).").optional(), "scopes": z.array(z.string()).describe("Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]*={0,2}$")).describe("Token bytes. A JWT (base64url-encoded JWS).").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Optional delegation — present when the requester acts on behalf of\n another entity (user, organization, upstream agent).").optional(), "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("Domain the requester belongs to. It carries the same bare-host shape\n \"Request recipient\" defines in the file header, for the same structural\n reason: a scheme, path or query smuggled in here would choose what gets\n fetched, not merely from where. It is NOT how a verifier finds this\n requester's keys: those live in the WBA directory, and verification resolves\n that directory from the COVERED `Signature-Agent` header, never from this\n self-asserted value."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "id": z.string().describe("Unique requester identifier (e.g., \"agent-research-bot-001\").").default(""), "name": z.string().describe("Human-readable name (e.g., \"Acme Research Assistant\").").optional(), "scopes": z.array(z.string()).max(64).describe("Entitlement scopes. Declare what the requester can access.\n\nThe Exchange filters its catalog to resources matching these scopes.\n Resources outside the scopes are not returned — the requester never\n learns they exist. This is the enforcement mechanism for both enterprise\n RBAC and open-market subscription entitlements.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).").optional(), "type": z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"]).describe("What kind of entity is making this request.") }).describe("Requester identity — forwarded for authorization and audit.").optional(), "ver": z.string().describe("RAMP protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("TransactionRequest — Commit to one or more offers.\n\nAfter selecting offers, the caller commits by sending this to the\n Exchange. Supports both single-offer and batch (multi-offer) modes.\n The Exchange validates eligibility, authorizes billing, creates\n delivery, and logs each transaction.")); export const TransactionResponseSchema = wire(z.object({ "agent_identity_hash": z.string().describe("Identity that a delivered retrieval_endpoint is bound to: the RFC 7638 JWK\n Thumbprint of the agent's Ed25519 request-signing key (see \"Retrieval-URL\n identity binding\" above). Shared across the request; set once.").default(""), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "items": z.array(z.object({ "billing_id": z.string().describe("Billing record identifier minted by the Exchange's billing adapter for\n this transaction (not the account handle — see RegisterResponse.billing_ref).").default(""), "cost": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Cost for this item.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource is delivered for this item.").default(0), "denial_reason": z.enum(["DENIAL_REASON_ACCOUNT_INACTIVE","DENIAL_REASON_INSUFFICIENT_BALANCE","DENIAL_REASON_RATE_LIMITED","DENIAL_REASON_CONTENT_UNAVAILABLE","DENIAL_REASON_RESTRICTION_NOT_SATISFIED","DENIAL_REASON_REPORTING_OVERDUE","DENIAL_REASON_OFFER_EXPIRED","DENIAL_REASON_SIGNATURE_INVALID","DENIAL_REASON_QUOTA_EXCEEDED","DENIAL_REASON_DELEGATION_INVALID","DENIAL_REASON_SCOPE_INSUFFICIENT","DENIAL_REASON_ENTITLEMENT_MISSING","DENIAL_REASON_ENTITLEMENT_MALFORMED","DENIAL_REASON_ENTITLEMENT_EXPIRED","DENIAL_REASON_ENTITLEMENT_WRONG_BUYER","DENIAL_REASON_SUBSCRIPTION_LAPSED","DENIAL_REASON_ENTITLEMENT_NOT_GRANTED","DENIAL_REASON_ACCOUNT_NOT_REGISTERED"]).describe("Set if this specific item was denied (others may succeed).").optional(), "expires_at": z.string().datetime({ offset: true }).describe("When retrieval_endpoint expires.").optional(), "offer_id": z.string().describe("The offer_id this result is for.").default(""), "reporting_obligation": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Reporting requirements for this item.").optional(), "resource_title": z.string().describe("Resource title echoed from the Offer.").optional(), "restriction_mismatches": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When denial_reason = RESTRICTION_NOT_SATISFIED, the restriction axes the\n request failed, in the same RestrictionKind vocabulary the terms use.").optional(), "retrieval_endpoint": z.string().describe("Signed retrieval URL for this item. Bound to the requesting agent's identity\n via the parent TransactionResponse.agent_identity_hash (shared across all\n batch items); expires at expires_at. Absent if this item was denied or its\n delivery_method is not signed-URL-based.").optional(), "subscription_id": z.string().describe("If under subscription, no per-request charge.").optional(), "subscription_unit_value": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Computed per-unit cost for financial attribution on subscription transactions.\n Even when cost.amount=\"0\" (subscription), this field carries the value\n of the access for accounting purposes (e.g., ASC 606 prepaid drawdown).").optional(), "transaction_id": z.string().describe("Exchange-assigned transaction identifier.").default("") }).describe("TransactionResultItem — Result for a single offer in a batch transaction.")).describe("Per-offer results (one entry per committed item, in original order).").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Post-transaction quota state. Tells the agent how much quota remains\n after this transaction. Enables proactive throttling (\"1 access left\").\n Multiple entries for multi-dimensional quotas.").optional(), "total_cost": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Aggregate cost across all items.").optional(), "ver": z.string().describe("RAMP protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("TransactionResponse — Exchange confirms the transaction(s).\n\nItems-only: every per-result datum lives in `items`\n (one TransactionResultItem per committed offer, in original order); the\n top-level fields carry only the shared aggregate state. A single offer is the\n degenerate 1-element `items`. The per-item denials remain in-body on\n TransactionResultItem as partial results of a successful request.")); diff --git a/proto/CHANGELOG.md b/proto/CHANGELOG.md index 525dce50..1e31c204 100644 --- a/proto/CHANGELOG.md +++ b/proto/CHANGELOG.md @@ -2,6 +2,45 @@ ## Unreleased +**`TransactionRequest.agent_request_acceptance` adds an agent-signed complete +ordered request-set proof (additive wire change).** The proof signs ordered +`(offer_sig, exchange)` references plus requester and idempotency key using the +same RFC 8785 JCS / detached Ed25519 convention as `AgentAcceptance`. A Broker +forwards the envelope unchanged while projecting a mixed-Exchange request; each +Exchange can then require the exact in-order projection addressed to itself +before creating or serving request-level idempotency state. This prevents a +relay from consuming an agent's key with an appended, removed, reordered, or +valid-subset-first request. + +The field is optional for wire compatibility. The Go, Python, and TypeScript +SDK clients emit it for valid routed offers, and all three signing/verifying +faces are pinned to shared cross-language canonicalization and ordering vectors. + +The payload's item list is capped at 256 entries (`repeated.max_items`, the +same ceiling a discovery query's `uris` list carries). The Go verification +helper enforces the same bound itself and decodes and size-checks the Ed25519 +signature before rendering the payload to canonical JSON, because a verifier +may run with wire validation off and the canonical rendering of an unbounded +caller-controlled list is the expensive step. A test pins the helper's bound to +the wire rule so the two cannot drift. The projection check also refuses an +empty subrequest outright: without that, a request carrying zero items for an +Exchange the signed set never names would compare zero against zero and report +a verified projection. + +Who authenticates a projected subrequest is now written down. A projected +subrequest is a new HTTP request its sender authors and RFC 9421-signs — a +Broker, or the agent itself when it splits its own mixed-Exchange set. The +agent's original HTTP signature covered the body the agent sent and does not +travel with a projected body; the detached body signatures (`agent_acceptance` +per item, `agent_request_acceptance` for the set) are what carry the agent's +authorization across projection, which is why they exist. The Exchange resolves +the acceptance verification key from the WBA directory of the requester domain +the signed payload names, which must equal the request's `requester.domain`; +when the requester itself signed the arriving request, that is the +request-signing key already resolved. Holder binding for delegations still +matches the wire signer, so a Broker may project a delegated request only when +the agent has delegated to the Broker's key. + **Signed delivery URLs are documented as Ed25519 signed by the Exchange and verified with its published public key, not HMAC-SHA256 over a shared secret (documentation correction; no wire change).** Since the initial public snapshot diff --git a/proto/ramp/v1/ramp.proto b/proto/ramp/v1/ramp.proto index 9c935eb9..f823f803 100644 --- a/proto/ramp/v1/ramp.proto +++ b/proto/ramp/v1/ramp.proto @@ -1939,6 +1939,84 @@ message AgentAcceptance { string signature_algorithm = 2; } +// AgentRequestAcceptance — the agent's topology-independent authorization of +// one complete ordered execute set. A Broker forwards this envelope unchanged +// when it projects a mixed-Exchange request into per-Exchange subrequests. +// Each receiving Exchange verifies the signature, then requires its subrequest +// to equal the complete in-order projection of payload.items whose exchange +// names that Exchange. This is what makes removal, append, and reorder visible +// before request-level idempotency state is claimed. +// +// A projected subrequest is a NEW HTTP request its sender authors. The party +// that projects — a Broker, or the agent itself when it splits its own +// mixed-Exchange set — computes that subrequest's Content-Digest and signs it +// with its own RFC 9421 request signature. The agent's original RFC 9421 +// signature covered the body the agent sent and does not travel with a +// projected body; that is expected, not a gap, and it is why this proof +// exists: like AgentAcceptance, it is a detached body signature that stays +// valid however the request travels. The hop-signature stack applies only to +// requests forwarded byte-for-byte. If a delegation rides the request, the +// holder-binding rule is unchanged — the wire signer must be the delegation's +// terminal holder — so a Broker may project a delegated request only when the +// agent has delegated to the Broker's key. +// +// The verification key is the agent key published for the requester the signed +// payload names. payload.requester_domain must equal the request's +// requester.domain, and the Exchange accepts the signature only if it verifies +// against an Ed25519 key currently valid in that domain's WBA directory +// ({requester_domain}/.well-known/http-message-signatures-directory), fetched +// under the same SSRF discipline as every directory fetch. The envelope +// carries no keyid, so the verifier tries the currently-valid Ed25519 keys of +// that directory; rotation overlap keeps that set small. When the requester +// itself signed the arriving request, the request-signing key the Exchange +// already resolved is that key, and no second fetch is needed. A signature +// that verifies against a key the requester's domain publishes is what turns +// the claimed requester identity into an authenticated one. +message AgentRequestAcceptance { + // The signed payload is carried because a projected subrequest does not carry + // offers addressed to other Exchanges and therefore cannot reconstruct the + // original complete set by itself. + AgentRequestAcceptancePayload payload = 1 [(buf.validate.field).required = true]; + + // Hex-encoded detached Ed25519 signature over the canonical payload bytes. + string signature = 2 [(buf.validate.field).string.min_len = 1]; + + // Signature algorithm; "EdDSA" for Ed25519. + string signature_algorithm = 3; +} + +// AgentRequestAcceptanceItem is the minimum reference needed to authorize an +// offer's membership, order, and fan-out destination without repeating the +// full Offer in every projected subrequest. Offer.signature transitively binds +// the full offer, including its exchange field; the explicit exchange lets a +// recipient derive which signed references must appear in its projection. +message AgentRequestAcceptanceItem { + string offer_sig = 1 [(buf.validate.field).string.min_len = 1]; + string exchange = 2 [(buf.validate.field).string.min_len = 1]; +} + +// AgentRequestAcceptancePayload fixes the field set signed by an +// AgentRequestAcceptance. Its canonical bytes are +// JCS(protojson(AgentRequestAcceptancePayload)) using the canonical-signing +// rules defined on Offer.signature. +message AgentRequestAcceptancePayload { + // Complete original request order, before Broker fan-out. Capped at 256 — + // the same ceiling a discovery query's uris list carries, so one request + // can reference at most one offer per queried URI at the query cap. The Go + // verification helper enforces the same bound itself before doing any + // canonicalization work, because a verifier may run with wire validation + // off and the canonical rendering of an unbounded list is the expensive + // step an unauthenticated caller could otherwise buy for free. + repeated AgentRequestAcceptanceItem items = 1 [ + (buf.validate.field).repeated.min_items = 1, + (buf.validate.field).repeated.max_items = 256 + ]; + + string requester_id = 2; + string requester_domain = 3; + string idempotency_key = 4; +} + // AgentAcceptancePayload — the canonical signing structure for AgentAcceptance. // It is NEVER sent on the wire; it exists solely so the signer (SDK) and the // verifier (Exchange) derive BYTE-IDENTICAL signed bytes from the same proto @@ -2009,6 +2087,12 @@ message TransactionRequest { // tokens, with no reconstruct-from-catalog. repeated TransactionItem items = 7 [(buf.validate.field).repeated.min_items = 1]; + // Optional for wire compatibility. When present, an Exchange verifies this + // before creating or serving request-level idempotency state. A Broker MUST + // forward it unchanged on every projected subrequest. Older clients that omit + // it retain per-item execution semantics but receive no request-level claim. + optional AgentRequestAcceptance agent_request_acceptance = 8; + // Extension point google.protobuf.Struct ext = 15; diff --git a/sdk/go/connect/client.go b/sdk/go/connect/client.go index 3ba81660..47bd1842 100644 --- a/sdk/go/connect/client.go +++ b/sdk/go/connect/client.go @@ -369,6 +369,13 @@ func (c *Client) Execute(ctx context.Context, offer core.VerifiedOffer, opts ... }, }}, } + if offer.Offer().GetExchange() != "" { + requestAcceptance, signErr := helpers.SignRequestAcceptanceWith(ctx, signer, req) + if signErr != nil { + return nil, &CallError{Kind: CallNotSignable, Op: op, Err: signErr} + } + req.AgentRequestAcceptance = requestAcceptance + } resp, err := c.rpc.ExecuteTransaction(ctx, connectrpc.NewRequest(req)) if err != nil { return nil, sendError(op, err) diff --git a/sdk/go/connect/client_verify_test.go b/sdk/go/connect/client_verify_test.go index b1f09526..380129ca 100644 --- a/sdk/go/connect/client_verify_test.go +++ b/sdk/go/connect/client_verify_test.go @@ -125,7 +125,11 @@ func newOfferFixture(t *testing.T) offerFixture { // rejected on freshness grounds. func sampleOffer(id string) *rampv1.Offer { return &rampv1.Offer{ - OfferId: id, + OfferId: id, + // A wire-valid offer always names its exchange, and the execute path + // signs a request acceptance only for offers that do — leaving this + // empty would route every test around the production branch. + Exchange: "exchange.test", ExpiresAt: timestampProto(time.Now().Add(1 * time.Hour)), Pricing: &rampv1.Pricing{Rate: "0.05", Currency: "USD"}, } diff --git a/sdk/go/connect/gen_client_request_vectors_test.go b/sdk/go/connect/gen_client_request_vectors_test.go index 96454528..5601257b 100644 --- a/sdk/go/connect/gen_client_request_vectors_test.go +++ b/sdk/go/connect/gen_client_request_vectors_test.go @@ -304,7 +304,7 @@ func verifyOne(t *testing.T, offers offerFixture) core.VerifiedOffer { t.Helper() sorted := core.NewVerifier( core.Strict, - helpers.NewStaticKeyResolver(map[string]ed25519.PublicKey{"": offers.exchangePub}), + helpers.NewStaticKeyResolver(map[string]ed25519.PublicKey{"exchange.test": offers.exchangePub}), time.Now, ).Sort(context.Background(), []*rampv1.Offer{offers.good}) if len(sorted.Verified) != 1 { diff --git a/sdk/go/connect/verbs_test.go b/sdk/go/connect/verbs_test.go index 290a3786..b1d990f0 100644 --- a/sdk/go/connect/verbs_test.go +++ b/sdk/go/connect/verbs_test.go @@ -2,6 +2,7 @@ package connect_test import ( "context" + "crypto/ed25519" "encoding/json" "errors" "net/http" @@ -253,6 +254,48 @@ func TestExecute_SendsRequesterAndAVerifyingAcceptance(t *testing.T) { ); err != nil { t.Errorf("the acceptance an Exchange would check does not verify: %v", err) } + // The request-level acceptance travels beside the per-item one, and the + // receiving Exchange checks it as the complete in-order projection for + // itself — so the test verifies exactly what that Exchange would. + ra := got.GetAgentRequestAcceptance() + if ra.GetSignatureAlgorithm() != helpers.AcceptanceSignatureAlgorithm { + t.Errorf("request-acceptance algorithm = %q", ra.GetSignatureAlgorithm()) + } + if _, err = helpers.VerifyRequestAcceptanceProjection(got, ra, "exchange.test", sig.pub); err != nil { + t.Errorf("the request acceptance an Exchange would check does not verify: %v", err) + } +} + +// An offer that names no exchange cannot appear in a request-acceptance item +// (the item requires a recipient), so the client sends the request without the +// field instead of constructing an acceptance no verifier could accept. +func TestExecute_SkipsRequestAcceptanceWhenTheOfferNamesNoExchange(t *testing.T) { + sig := newSigningFixture(t) + origin := &recordingExecute{} + srv := serveExchange(t, sig, origin) + + _, exchangePriv, err := ed25519.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + offer := sampleOffer("offer-no-exchange") + offer.Exchange = "" + offerSig, err := helpers.SignOffer(exchangePriv, offer) + if err != nil { + t.Fatal(err) + } + offer.Signature = offerSig + offer.SignatureAlgorithm = helpers.OfferSignatureAlgorithm + + client := rampconnect.NewClient(srv.URL, + rampconnect.WithSigner(sig.signer), rampconnect.WithRequester(testRequester())) + if _, err := client.Execute(context.Background(), + core.RejectedOffer{Offer: offer}.Unsafe()); err != nil { + t.Fatalf("Execute: %v", err) + } + if origin.req.GetAgentRequestAcceptance() != nil { + t.Error("an exchange-less offer must not carry a request acceptance") + } } // Every precondition refuses BEFORE anything leaves the process — an unsigned diff --git a/sdk/go/helpers/export_test.go b/sdk/go/helpers/export_test.go index 633d7792..877e16a0 100644 --- a/sdk/go/helpers/export_test.go +++ b/sdk/go/helpers/export_test.go @@ -7,3 +7,9 @@ import "github.com/santhosh-tekuri/jsonschema/v6" // surface and never reaches the API-parity gate — the loader is an implementation // detail that only a test needs to reach past the scan to exercise. func RefusingSchemaLoaderForTest() jsonschema.URLLoader { return refusingSchemaLoader{} } + +// MaxRequestAcceptanceItemsForTest exposes the canonicalization item cap so the +// external test binary can pin it to the wire rule on +// AgentRequestAcceptancePayload.items. Declared in a _test.go file, so it never +// reaches the API-parity gate. +const MaxRequestAcceptanceItemsForTest = maxRequestAcceptanceItems diff --git a/sdk/go/helpers/gen_request_acceptance_vectors_test.go b/sdk/go/helpers/gen_request_acceptance_vectors_test.go new file mode 100644 index 00000000..a302b154 --- /dev/null +++ b/sdk/go/helpers/gen_request_acceptance_vectors_test.go @@ -0,0 +1,85 @@ +package helpers + +import ( + "crypto/ed25519" + "encoding/base64" + "encoding/hex" + "os" + "path/filepath" + "testing" + + rampv1 "github.com/RAMP-Protocol/protocol/gen/go/ramp/v1" +) + +type requestAcceptanceVectorItem struct { + OfferSig string `json:"offer_sig"` + Exchange string `json:"exchange"` +} + +type requestAcceptanceVector struct { + Name string `json:"name"` + Items []requestAcceptanceVectorItem `json:"items"` + RequesterID string `json:"requester_id"` + RequesterDomain string `json:"requester_domain"` + IdempotencyKey string `json:"idempotency_key"` + CanonicalJCS string `json:"canonical_jcs"` + SignatureHex string `json:"signature_hex"` + PubkeyB64 string `json:"pubkey_b64"` + SeedHex string `json:"seed_hex"` +} + +func TestGenerateRequestAcceptanceVectors(t *testing.T) { + seed, err := hex.DecodeString(acceptanceSeedHex) + if err != nil { + t.Fatal(err) + } + priv := ed25519.NewKeyFromSeed(seed) + pub := priv.Public().(ed25519.PublicKey) + specs := []requestAcceptanceVector{ + { + Name: "mixed_exchange_order", + Items: []requestAcceptanceVectorItem{ + {OfferSig: "sig-a", Exchange: "one.example"}, + {OfferSig: "sig-b", Exchange: "two.example"}, + {OfferSig: "sig-c", Exchange: "one.example"}, + }, + RequesterID: "agent-1", RequesterDomain: "agent.example", IdempotencyKey: "idem-1", + }, + { + Name: "empty_requester_domain", + Items: []requestAcceptanceVectorItem{{OfferSig: "sig-z", Exchange: "one.example"}}, + RequesterID: "agent-2", IdempotencyKey: "idem-2", + }, + } + for i := range specs { + v := &specs[i] + req := &rampv1.TransactionRequest{ + IdempotencyKey: v.IdempotencyKey, + Requester: &rampv1.Requester{Id: v.RequesterID, Domain: v.RequesterDomain}, + } + for _, item := range v.Items { + req.Items = append(req.Items, &rampv1.TransactionItem{Offer: &rampv1.Offer{ + Signature: item.OfferSig, Exchange: item.Exchange, + }}) + } + acceptance, err := SignRequestAcceptance(priv, req) + if err != nil { + t.Fatalf("%s: %v", v.Name, err) + } + canonical, err := CanonicalRequestAcceptanceBytes(acceptance.GetPayload()) + if err != nil { + t.Fatalf("%s: %v", v.Name, err) + } + v.CanonicalJCS = string(canonical) + v.SignatureHex = acceptance.GetSignature() + v.PubkeyB64 = base64.StdEncoding.EncodeToString(pub) + v.SeedHex = acceptanceSeedHex + } + doc := map[string]any{"canonicalization": "jcs", "vectors": specs} + path := filepath.Join("testdata", "request-acceptance-vectors.json") + if os.Getenv("RAMP_UPDATE_VECTORS") == "1" { + writeJSON(t, path, doc) + return + } + assertMatches(t, path, doc) +} diff --git a/sdk/go/helpers/request_acceptance.go b/sdk/go/helpers/request_acceptance.go new file mode 100644 index 00000000..63f86435 --- /dev/null +++ b/sdk/go/helpers/request_acceptance.go @@ -0,0 +1,222 @@ +package helpers + +import ( + "context" + "crypto/ed25519" + "encoding/hex" + "errors" + "fmt" + + rampv1 "github.com/RAMP-Protocol/protocol/gen/go/ramp/v1" +) + +// ErrRequestAcceptanceSignatureInvalid signals that the agent did not sign the +// request-acceptance payload presented by the caller. +var ErrRequestAcceptanceSignatureInvalid = errors.New("helpers: request-acceptance signature invalid") + +// maxRequestAcceptanceItems mirrors the repeated.max_items rule on +// AgentRequestAcceptancePayload.items. The helper enforces it itself, before +// any canonicalization work, because a verifier may run with wire validation +// off and rendering an unbounded caller-controlled list to canonical JSON is +// the expensive step. A test pins this constant to the wire rule. +const maxRequestAcceptanceItems = 256 + +// RequestAcceptancePayload builds the complete ordered request-set payload an +// agent signs before any Broker fan-out. +func RequestAcceptancePayload(req *rampv1.TransactionRequest) (*rampv1.AgentRequestAcceptancePayload, error) { + if req == nil { + return nil, errors.New("helpers: transaction request is nil") + } + if req.GetRequester() == nil { + return nil, errors.New("helpers: requester is nil") + } + if len(req.GetItems()) == 0 { + return nil, errors.New("helpers: transaction request has no items") + } + items := make([]*rampv1.AgentRequestAcceptanceItem, 0, len(req.GetItems())) + for i, item := range req.GetItems() { + offer := item.GetOffer() + if offer == nil { + return nil, fmt.Errorf("helpers: item %d offer is nil", i) + } + if offer.GetSignature() == "" { + return nil, fmt.Errorf("helpers: item %d offer is unsigned", i) + } + if offer.GetExchange() == "" { + return nil, fmt.Errorf("helpers: item %d offer exchange is empty", i) + } + items = append(items, &rampv1.AgentRequestAcceptanceItem{ + OfferSig: offer.GetSignature(), + Exchange: offer.GetExchange(), + }) + } + return &rampv1.AgentRequestAcceptancePayload{ + Items: items, + RequesterId: req.GetRequester().GetId(), + RequesterDomain: req.GetRequester().GetDomain(), + IdempotencyKey: req.GetIdempotencyKey(), + }, nil +} + +// CanonicalRequestAcceptanceBytes returns the exact JCS(protojson(...)) bytes +// covered by an AgentRequestAcceptance signature. +func CanonicalRequestAcceptanceBytes(payload *rampv1.AgentRequestAcceptancePayload) ([]byte, error) { + if payload == nil { + return nil, errors.New("helpers: request-acceptance payload is nil") + } + if len(payload.GetItems()) == 0 { + return nil, errors.New("helpers: request-acceptance payload has no items") + } + if len(payload.GetItems()) > maxRequestAcceptanceItems { + return nil, fmt.Errorf("helpers: request-acceptance payload has %d items, the maximum is %d", + len(payload.GetItems()), maxRequestAcceptanceItems) + } + for i, item := range payload.GetItems() { + if item.GetOfferSig() == "" { + return nil, fmt.Errorf("helpers: request-acceptance item %d offer signature is empty", i) + } + if item.GetExchange() == "" { + return nil, fmt.Errorf("helpers: request-acceptance item %d exchange is empty", i) + } + } + return canonicalSignPayload(payload) +} + +// SignRequestAcceptance signs req's complete ordered request set with priv. +func SignRequestAcceptance(priv ed25519.PrivateKey, req *rampv1.TransactionRequest) (*rampv1.AgentRequestAcceptance, error) { + if len(priv) != ed25519.PrivateKeySize { + return nil, fmt.Errorf("helpers: ed25519 private key must be %d bytes, got %d", ed25519.PrivateKeySize, len(priv)) + } + payload, err := RequestAcceptancePayload(req) + if err != nil { + return nil, err + } + canonical, err := CanonicalRequestAcceptanceBytes(payload) + if err != nil { + return nil, err + } + return &rampv1.AgentRequestAcceptance{ + Payload: payload, + Signature: hex.EncodeToString(ed25519.Sign(priv, canonical)), + SignatureAlgorithm: AcceptanceSignatureAlgorithm, + }, nil +} + +// SignRequestAcceptanceWith is SignRequestAcceptance for a KMS/HSM-backed +// Signer. +func SignRequestAcceptanceWith(ctx context.Context, signer Signer, req *rampv1.TransactionRequest) (*rampv1.AgentRequestAcceptance, error) { + if signer == nil { + return nil, errors.New("helpers: request-acceptance signer is nil") + } + if signer.Algorithm() != AlgEd25519 { + return nil, fmt.Errorf("%w: request acceptance requires %q, signer offers %q", + ErrUnsupportedAlgorithm, AlgEd25519, signer.Algorithm()) + } + payload, err := RequestAcceptancePayload(req) + if err != nil { + return nil, err + } + canonical, err := CanonicalRequestAcceptanceBytes(payload) + if err != nil { + return nil, err + } + sig, err := signer.Sign(ctx, canonical) + if err != nil { + return nil, fmt.Errorf("helpers: sign request acceptance: %w", err) + } + return &rampv1.AgentRequestAcceptance{ + Payload: payload, + Signature: hex.EncodeToString(sig), + SignatureAlgorithm: AcceptanceSignatureAlgorithm, + }, nil +} + +// VerifyRequestAcceptance verifies the signature and the shared request +// envelope fields. It deliberately does not apply a fan-out projection rule; +// an Exchange must call VerifyRequestAcceptanceProjection instead. +func VerifyRequestAcceptance(req *rampv1.TransactionRequest, acceptance *rampv1.AgentRequestAcceptance, pub ed25519.PublicKey) ([]byte, error) { + if len(pub) != ed25519.PublicKeySize { + return nil, fmt.Errorf("helpers: ed25519 public key must be %d bytes, got %d", ed25519.PublicKeySize, len(pub)) + } + if req == nil || req.GetRequester() == nil { + return nil, errors.New("helpers: transaction request or requester is nil") + } + if acceptance == nil || acceptance.GetPayload() == nil { + return nil, errors.New("helpers: request acceptance or payload is nil") + } + if acceptance.GetSignatureAlgorithm() != AcceptanceSignatureAlgorithm { + return nil, fmt.Errorf("helpers: request acceptance algorithm must be %q", AcceptanceSignatureAlgorithm) + } + payload := acceptance.GetPayload() + if payload.GetRequesterId() != req.GetRequester().GetId() || + payload.GetRequesterDomain() != req.GetRequester().GetDomain() || + payload.GetIdempotencyKey() != req.GetIdempotencyKey() { + return nil, ErrRequestAcceptanceSignatureInvalid + } + // The signature is decoded and size-checked before canonicalization: the + // canonical rendering is the expensive step, and a caller whose signature + // cannot possibly verify must not be able to buy that work. + sig, err := hex.DecodeString(acceptance.GetSignature()) + if err != nil { + return nil, fmt.Errorf("helpers: decode request-acceptance signature: %w", err) + } + if len(sig) != ed25519.SignatureSize { + return nil, ErrRequestAcceptanceSignatureInvalid + } + canonical, err := CanonicalRequestAcceptanceBytes(payload) + if err != nil { + return nil, err + } + if !ed25519.Verify(pub, canonical, sig) { + return nil, ErrRequestAcceptanceSignatureInvalid + } + return canonical, nil +} + +// VerifyRequestAcceptanceProjection additionally proves that req.items is the +// complete ordered projection of the signed original set addressed to exchange. +func VerifyRequestAcceptanceProjection(req *rampv1.TransactionRequest, acceptance *rampv1.AgentRequestAcceptance, exchange string, pub ed25519.PublicKey) ([]byte, error) { + // An empty subrequest must be refused outright: for an exchange the signed + // set never names, the projection is also empty, zero equals zero, and the + // comparison loop below would report a verified projection for a request + // addressed to nobody. Wire validation (items.min_items = 1) catches this + // on the request path, but a verifier may run with validation off, and a + // security primitive does not assume its caller validated first. + if len(req.GetItems()) == 0 { + return nil, ErrRequestAcceptanceSignatureInvalid + } + canonical, err := VerifyRequestAcceptance(req, acceptance, pub) + if err != nil { + return nil, err + } + // Membership uses the Exchange identity rule CheckAudience owns — case + // folds, an explicit :443 equals the omitted HTTPS-default port — not raw + // string equality. With raw equality, a signed set holding one.example and + // one.example:443 lets a relay drop the differently spelled item and still + // pass, while an honest complete forward is refused. A value that is not a + // bare domain names nobody and never matches. + if !IsBareDomain(exchange) { + return nil, fmt.Errorf("helpers: projection exchange %q is not a bare domain", exchange) + } + namesExchange := func(v string) bool { + verdict, err := CheckAudience(exchange, v) + return err == nil && verdict == AudienceAccepted + } + want := make([]*rampv1.AgentRequestAcceptanceItem, 0, len(req.GetItems())) + for _, ref := range acceptance.GetPayload().GetItems() { + if namesExchange(ref.GetExchange()) { + want = append(want, ref) + } + } + if len(want) != len(req.GetItems()) { + return nil, ErrRequestAcceptanceSignatureInvalid + } + for i, item := range req.GetItems() { + offer := item.GetOffer() + if offer == nil || !namesExchange(offer.GetExchange()) || + offer.GetSignature() != want[i].GetOfferSig() { + return nil, ErrRequestAcceptanceSignatureInvalid + } + } + return canonical, nil +} diff --git a/sdk/go/helpers/request_acceptance_test.go b/sdk/go/helpers/request_acceptance_test.go new file mode 100644 index 00000000..0c976c5a --- /dev/null +++ b/sdk/go/helpers/request_acceptance_test.go @@ -0,0 +1,427 @@ +package helpers_test + +import ( + "context" + "crypto/ed25519" + "errors" + "testing" + + "buf.build/go/protovalidate" + + rampv1 "github.com/RAMP-Protocol/protocol/gen/go/ramp/v1" + "github.com/RAMP-Protocol/protocol/sdk/go/helpers" +) + +func requestAcceptanceFixture() *rampv1.TransactionRequest { + return &rampv1.TransactionRequest{ + IdempotencyKey: "idem-1", + Requester: &rampv1.Requester{Id: "agent-1", Domain: "agent.example"}, + Items: []*rampv1.TransactionItem{ + {Offer: &rampv1.Offer{Signature: "sig-a", Exchange: "one.example"}}, + {Offer: &rampv1.Offer{Signature: "sig-b", Exchange: "two.example"}}, + {Offer: &rampv1.Offer{Signature: "sig-c", Exchange: "one.example"}}, + }, + } +} + +func TestRequestAcceptanceProjection_roundTrip(t *testing.T) { + pub, priv, _ := ed25519.GenerateKey(nil) + original := requestAcceptanceFixture() + acceptance, err := helpers.SignRequestAcceptance(priv, original) + if err != nil { + t.Fatal(err) + } + projected := &rampv1.TransactionRequest{ + IdempotencyKey: original.GetIdempotencyKey(), + Requester: original.GetRequester(), + Items: []*rampv1.TransactionItem{original.GetItems()[0], original.GetItems()[2]}, + } + if _, err := helpers.VerifyRequestAcceptanceProjection(projected, acceptance, "one.example", pub); err != nil { + t.Fatalf("verify exact projection: %v", err) + } +} + +func TestRequestAcceptanceProjection_membershipAndOrderAreClosed(t *testing.T) { + pub, priv, _ := ed25519.GenerateKey(nil) + original := requestAcceptanceFixture() + acceptance, err := helpers.SignRequestAcceptance(priv, original) + if err != nil { + t.Fatal(err) + } + cases := map[string][]*rampv1.TransactionItem{ + "removed": {original.GetItems()[0]}, + "reordered": {original.GetItems()[2], original.GetItems()[0]}, + "appended": {original.GetItems()[0], original.GetItems()[2], original.GetItems()[0]}, + } + for name, items := range cases { + t.Run(name, func(t *testing.T) { + projected := &rampv1.TransactionRequest{ + IdempotencyKey: original.GetIdempotencyKey(), + Requester: original.GetRequester(), + Items: items, + } + if _, err := helpers.VerifyRequestAcceptanceProjection(projected, acceptance, "one.example", pub); !errors.Is(err, helpers.ErrRequestAcceptanceSignatureInvalid) { + t.Fatalf("expected invalid request acceptance, got %v", err) + } + }) + } +} + +func TestRequestAcceptance_tamperRejected(t *testing.T) { + pub, priv, _ := ed25519.GenerateKey(nil) + req := requestAcceptanceFixture() + acceptance, err := helpers.SignRequestAcceptance(priv, req) + if err != nil { + t.Fatal(err) + } + acceptance.Payload.Items[0].Exchange = "evil.example" + if _, err := helpers.VerifyRequestAcceptance(req, acceptance, pub); !errors.Is(err, helpers.ErrRequestAcceptanceSignatureInvalid) { + t.Fatalf("expected invalid request acceptance, got %v", err) + } +} + +// A subrequest with zero items must be refused even when the signed set names +// zero items for that exchange: zero equals zero, the comparison loop runs no +// iterations, and without the guard a request addressed to nobody would report +// a verified projection. +func TestRequestAcceptanceProjection_emptySubrequestRejected(t *testing.T) { + pub, priv, _ := ed25519.GenerateKey(nil) + original := requestAcceptanceFixture() + acceptance, err := helpers.SignRequestAcceptance(priv, original) + if err != nil { + t.Fatal(err) + } + empty := &rampv1.TransactionRequest{ + IdempotencyKey: original.GetIdempotencyKey(), + Requester: original.GetRequester(), + } + // "three.example" is absent from the signed set, so its projection is also + // empty — the exact shape the guard exists to refuse. + if _, err := helpers.VerifyRequestAcceptanceProjection(empty, acceptance, "three.example", pub); !errors.Is(err, helpers.ErrRequestAcceptanceSignatureInvalid) { + t.Fatalf("expected invalid request acceptance, got %v", err) + } +} + +// mixedSpellingFixture spells one Exchange identity three ways the SDK treats +// as equal — bare, upper-cased, and with the HTTPS-default port written out — +// plus one genuinely different party. +func mixedSpellingFixture() *rampv1.TransactionRequest { + return &rampv1.TransactionRequest{ + IdempotencyKey: "idem-1", + Requester: &rampv1.Requester{Id: "agent-1", Domain: "agent.example"}, + Items: []*rampv1.TransactionItem{ + {Offer: &rampv1.Offer{Signature: "sig-a", Exchange: "one.example"}}, + {Offer: &rampv1.Offer{Signature: "sig-b", Exchange: "ONE.EXAMPLE"}}, + {Offer: &rampv1.Offer{Signature: "sig-c", Exchange: "one.example:443"}}, + {Offer: &rampv1.Offer{Signature: "sig-d", Exchange: "two.example"}}, + }, + } +} + +// Projection membership uses the CheckAudience identity rule, not raw string +// equality: an honest complete forward whose items spell one Exchange three +// equivalent ways verifies, whichever accepted spelling the verifier holds as +// its own identity. +func TestRequestAcceptanceProjection_equivalentSpellingsVerify(t *testing.T) { + pub, priv, _ := ed25519.GenerateKey(nil) + original := mixedSpellingFixture() + acceptance, err := helpers.SignRequestAcceptance(priv, original) + if err != nil { + t.Fatal(err) + } + projected := &rampv1.TransactionRequest{ + IdempotencyKey: original.GetIdempotencyKey(), + Requester: original.GetRequester(), + Items: original.GetItems()[:3], + } + for _, self := range []string{"one.example", "One.Example:443"} { + if _, err := helpers.VerifyRequestAcceptanceProjection(projected, acceptance, self, pub); err != nil { + t.Fatalf("verify mixed-spelling projection as %q: %v", self, err) + } + } +} + +// With raw equality, a relay could remove the differently spelled item and +// still pass, because the count of raw-equal refs would shrink to match. Under +// the identity rule, dropping ANY of the three equivalent items is an +// incomplete projection and is refused. +func TestRequestAcceptanceProjection_equivalentSpellingRemovalRejected(t *testing.T) { + pub, priv, _ := ed25519.GenerateKey(nil) + original := mixedSpellingFixture() + acceptance, err := helpers.SignRequestAcceptance(priv, original) + if err != nil { + t.Fatal(err) + } + for drop := 0; drop < 3; drop++ { + items := make([]*rampv1.TransactionItem, 0, 2) + for i, item := range original.GetItems()[:3] { + if i != drop { + items = append(items, item) + } + } + projected := &rampv1.TransactionRequest{ + IdempotencyKey: original.GetIdempotencyKey(), + Requester: original.GetRequester(), + Items: items, + } + if _, err := helpers.VerifyRequestAcceptanceProjection(projected, acceptance, "one.example", pub); !errors.Is(err, helpers.ErrRequestAcceptanceSignatureInvalid) { + t.Fatalf("dropped item %d: expected invalid request acceptance, got %v", drop, err) + } + } + // The sharpest cut: forward only the item whose spelling raw-equals the + // verifier's own. Under raw equality the shrunken filter count would match + // and this would pass; under the identity rule the projection is three + // items and one is an incomplete forward. + rawOnly := &rampv1.TransactionRequest{ + IdempotencyKey: original.GetIdempotencyKey(), + Requester: original.GetRequester(), + Items: original.GetItems()[:1], + } + if _, err := helpers.VerifyRequestAcceptanceProjection(rawOnly, acceptance, "one.example", pub); !errors.Is(err, helpers.ErrRequestAcceptanceSignatureInvalid) { + t.Fatalf("raw-equal subset: expected invalid request acceptance, got %v", err) + } +} + +// The identity rule folds only the HTTPS-default port, and a value that is not +// a bare domain names nobody — neither may fail open. +func TestRequestAcceptanceProjection_portsAndMalformedValuesStayClosed(t *testing.T) { + pub, priv, _ := ed25519.GenerateKey(nil) + + t.Run("port 80 is a different identity", func(t *testing.T) { + original := &rampv1.TransactionRequest{ + IdempotencyKey: "idem-1", + Requester: &rampv1.Requester{Id: "agent-1", Domain: "agent.example"}, + Items: []*rampv1.TransactionItem{ + {Offer: &rampv1.Offer{Signature: "sig-a", Exchange: "one.example"}}, + {Offer: &rampv1.Offer{Signature: "sig-b", Exchange: "one.example:80"}}, + }, + } + acceptance, err := helpers.SignRequestAcceptance(priv, original) + if err != nil { + t.Fatal(err) + } + // Forwarding both items to one.example must fail: the :80 item belongs + // to a different identity, so the projection for one.example is one item. + if _, err := helpers.VerifyRequestAcceptanceProjection(original, acceptance, "one.example", pub); !errors.Is(err, helpers.ErrRequestAcceptanceSignatureInvalid) { + t.Fatalf("expected invalid request acceptance, got %v", err) + } + }) + + t.Run("malformed projection exchange is an error", func(t *testing.T) { + original := requestAcceptanceFixture() + acceptance, err := helpers.SignRequestAcceptance(priv, original) + if err != nil { + t.Fatal(err) + } + for _, self := range []string{"", "https://one.example"} { + if _, err := helpers.VerifyRequestAcceptanceProjection(original, acceptance, self, pub); err == nil { + t.Fatalf("projection exchange %q: expected an error", self) + } + } + }) + + t.Run("malformed forwarded offer exchange is refused", func(t *testing.T) { + original := requestAcceptanceFixture() + acceptance, err := helpers.SignRequestAcceptance(priv, original) + if err != nil { + t.Fatal(err) + } + projected := &rampv1.TransactionRequest{ + IdempotencyKey: original.GetIdempotencyKey(), + Requester: original.GetRequester(), + Items: []*rampv1.TransactionItem{ + {Offer: &rampv1.Offer{Signature: "sig-a", Exchange: "https://one.example"}}, + {Offer: &rampv1.Offer{Signature: "sig-c", Exchange: "one.example"}}, + }, + } + if _, err := helpers.VerifyRequestAcceptanceProjection(projected, acceptance, "one.example", pub); !errors.Is(err, helpers.ErrRequestAcceptanceSignatureInvalid) { + t.Fatalf("expected invalid request acceptance, got %v", err) + } + }) +} + +// oversizedFixture returns a request carrying n items, every one signed-offer +// shaped, so payload construction succeeds and only the cap can refuse it. +func oversizedFixture(n int) *rampv1.TransactionRequest { + items := make([]*rampv1.TransactionItem, n) + for i := range items { + items[i] = &rampv1.TransactionItem{ + Offer: &rampv1.Offer{Signature: "sig", Exchange: "one.example"}, + } + } + return &rampv1.TransactionRequest{ + IdempotencyKey: "idem-1", + Requester: &rampv1.Requester{Id: "agent-1", Domain: "agent.example"}, + Items: items, + } +} + +// The canonicalization cap: a payload at the maximum still signs and verifies, +// one past it is refused before any canonical rendering happens, and the Go +// constant cannot drift from the wire rule. +func TestRequestAcceptance_itemCapEnforcedBeforeCanonicalization(t *testing.T) { + pub, priv, _ := ed25519.GenerateKey(nil) + + atCap, err := helpers.SignRequestAcceptance(priv, oversizedFixture(helpers.MaxRequestAcceptanceItemsForTest)) + if err != nil { + t.Fatalf("a payload at the maximum must sign: %v", err) + } + if _, err := helpers.VerifyRequestAcceptance(oversizedFixture(helpers.MaxRequestAcceptanceItemsForTest), atCap, pub); err != nil { + t.Fatalf("a payload at the maximum must verify: %v", err) + } + + over, err := helpers.RequestAcceptancePayload(oversizedFixture(helpers.MaxRequestAcceptanceItemsForTest + 1)) + if err != nil { + t.Fatal(err) + } + if _, err := helpers.CanonicalRequestAcceptanceBytes(over); err == nil { + t.Fatal("expected the item cap to refuse maximum+1") + } + + fd := (&rampv1.AgentRequestAcceptancePayload{}).ProtoReflect().Descriptor().Fields().ByName("items") + rules, err := protovalidate.ResolveFieldRules(fd) + if err != nil { + t.Fatal(err) + } + if got := rules.GetRepeated().GetMaxItems(); got != uint64(helpers.MaxRequestAcceptanceItemsForTest) { + t.Fatalf("wire repeated.max_items = %d, the helper enforces %d — the two bounds drifted", + got, helpers.MaxRequestAcceptanceItemsForTest) + } +} + +// The signature is decoded and size-checked before canonicalization. The probe +// pairs an over-cap payload with a wrong-size signature: the signature refusal +// winning proves the cheap check ran first, so a caller whose signature cannot +// possibly verify never buys the canonical rendering of a large payload. +func TestRequestAcceptance_signatureCheckedBeforeCanonicalization(t *testing.T) { + pub, priv, _ := ed25519.GenerateKey(nil) + req := requestAcceptanceFixture() + acceptance, err := helpers.SignRequestAcceptance(priv, req) + if err != nil { + t.Fatal(err) + } + + badHex := &rampv1.AgentRequestAcceptance{ + Payload: acceptance.GetPayload(), + Signature: "not-hex", + SignatureAlgorithm: acceptance.GetSignatureAlgorithm(), + } + if _, err := helpers.VerifyRequestAcceptance(req, badHex, pub); err == nil { + t.Fatal("expected malformed signature hex to be refused") + } + + overReq := oversizedFixture(helpers.MaxRequestAcceptanceItemsForTest + 1) + overPayload, err := helpers.RequestAcceptancePayload(overReq) + if err != nil { + t.Fatal(err) + } + truncated := &rampv1.AgentRequestAcceptance{ + Payload: overPayload, + Signature: "abcd", // valid hex, 2 bytes — not an Ed25519 signature + SignatureAlgorithm: acceptance.GetSignatureAlgorithm(), + } + if _, err := helpers.VerifyRequestAcceptance(overReq, truncated, pub); !errors.Is(err, helpers.ErrRequestAcceptanceSignatureInvalid) { + t.Fatalf("expected the size check to refuse before the cap could, got %v", err) + } +} + +// The envelope-binding block: a valid acceptance replayed under a request whose +// requester or idempotency key differs is refused before the signature is even +// checked, so an acceptance cannot be transplanted onto another request. +func TestRequestAcceptance_envelopeMismatchRejected(t *testing.T) { + pub, priv, _ := ed25519.GenerateKey(nil) + cases := map[string]func(*rampv1.TransactionRequest){ + "different requester id": func(r *rampv1.TransactionRequest) { r.Requester.Id = "agent-2" }, + "different requester domain": func(r *rampv1.TransactionRequest) { r.Requester.Domain = "other.example" }, + "different idempotency key": func(r *rampv1.TransactionRequest) { r.IdempotencyKey = "idem-2" }, + } + for name, mutate := range cases { + t.Run(name, func(t *testing.T) { + req := requestAcceptanceFixture() + acceptance, err := helpers.SignRequestAcceptance(priv, req) + if err != nil { + t.Fatal(err) + } + mutate(req) + if _, err := helpers.VerifyRequestAcceptance(req, acceptance, pub); !errors.Is(err, helpers.ErrRequestAcceptanceSignatureInvalid) { + t.Fatalf("expected invalid request acceptance, got %v", err) + } + }) + } +} + +// The algorithm field is advisory but the verifier still refuses anything that +// does not name the one supported scheme, so a caller cannot smuggle a +// differently-signed envelope past a verifier that assumes Ed25519. +func TestRequestAcceptance_wrongAlgorithmRejected(t *testing.T) { + pub, priv, _ := ed25519.GenerateKey(nil) + req := requestAcceptanceFixture() + acceptance, err := helpers.SignRequestAcceptance(priv, req) + if err != nil { + t.Fatal(err) + } + acceptance.SignatureAlgorithm = "RS256" + if _, err := helpers.VerifyRequestAcceptance(req, acceptance, pub); err == nil { + t.Fatal("expected a refusal for a non-EdDSA algorithm") + } +} + +// notEd25519Signer satisfies helpers.Signer but reports an unsupported +// algorithm, standing in for a KMS configured with the wrong key type. +type notEd25519Signer struct{} + +func (notEd25519Signer) KeyID() string { return "kms.v1" } +func (notEd25519Signer) Algorithm() string { return "rsa-pss-sha512" } +func (notEd25519Signer) Sign(context.Context, []byte) ([]byte, error) { + return nil, errors.New("must not be reached") +} + +// SignRequestAcceptanceWith is the face the connect client calls in production: +// it must produce an acceptance the verifier accepts, and refuse a signer whose +// algorithm is not Ed25519 before asking it to sign anything. +func TestSignRequestAcceptanceWith_roundTripAndAlgorithmGate(t *testing.T) { + pub, priv, _ := ed25519.GenerateKey(nil) + req := requestAcceptanceFixture() + + signer, err := helpers.NewEd25519Signer("agent.v1", priv) + if err != nil { + t.Fatal(err) + } + acceptance, err := helpers.SignRequestAcceptanceWith(context.Background(), signer, req) + if err != nil { + t.Fatalf("SignRequestAcceptanceWith: %v", err) + } + if _, err := helpers.VerifyRequestAcceptance(req, acceptance, pub); err != nil { + t.Fatalf("signer-produced acceptance does not verify: %v", err) + } + + if _, err := helpers.SignRequestAcceptanceWith(context.Background(), notEd25519Signer{}, req); !errors.Is(err, helpers.ErrUnsupportedAlgorithm) { + t.Fatalf("expected ErrUnsupportedAlgorithm, got %v", err) + } +} + +func TestAgentRequestAcceptancePayload_fieldSetIsPinned(t *testing.T) { + want := []string{"items", "requester_id", "requester_domain", "idempotency_key"} + fields := (&rampv1.AgentRequestAcceptancePayload{}).ProtoReflect().Descriptor().Fields() + if fields.Len() != len(want) { + t.Fatalf("AgentRequestAcceptancePayload has %d fields, want %d", fields.Len(), len(want)) + } + for i, name := range want { + if got := string(fields.Get(i).Name()); got != name { + t.Errorf("field %d = %q, want %q", i+1, got, name) + } + } +} + +func TestAgentRequestAcceptanceItem_fieldSetIsPinned(t *testing.T) { + want := []string{"offer_sig", "exchange"} + fields := (&rampv1.AgentRequestAcceptanceItem{}).ProtoReflect().Descriptor().Fields() + if fields.Len() != len(want) { + t.Fatalf("AgentRequestAcceptanceItem has %d fields, want %d", fields.Len(), len(want)) + } + for i, name := range want { + if got := string(fields.Get(i).Name()); got != name { + t.Errorf("field %d = %q, want %q", i+1, got, name) + } + } +} diff --git a/sdk/go/helpers/testdata/request-acceptance-vectors.json b/sdk/go/helpers/testdata/request-acceptance-vectors.json new file mode 100644 index 00000000..4051e12f --- /dev/null +++ b/sdk/go/helpers/testdata/request-acceptance-vectors.json @@ -0,0 +1,45 @@ +{ + "canonicalization": "jcs", + "vectors": [ + { + "name": "mixed_exchange_order", + "items": [ + { + "offer_sig": "sig-a", + "exchange": "one.example" + }, + { + "offer_sig": "sig-b", + "exchange": "two.example" + }, + { + "offer_sig": "sig-c", + "exchange": "one.example" + } + ], + "requester_id": "agent-1", + "requester_domain": "agent.example", + "idempotency_key": "idem-1", + "canonical_jcs": "{\"idempotency_key\":\"idem-1\",\"items\":[{\"exchange\":\"one.example\",\"offer_sig\":\"sig-a\"},{\"exchange\":\"two.example\",\"offer_sig\":\"sig-b\"},{\"exchange\":\"one.example\",\"offer_sig\":\"sig-c\"}],\"requester_domain\":\"agent.example\",\"requester_id\":\"agent-1\"}", + "signature_hex": "bfe5bc3e042022c621c4160bbe7015220cb9210eb13a7a849df19163374ce4bbb98e4846dc92053eddb2c7a97759e0232ccfabd580cda82fdd7f2165d374a10c", + "pubkey_b64": "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=", + "seed_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + }, + { + "name": "empty_requester_domain", + "items": [ + { + "offer_sig": "sig-z", + "exchange": "one.example" + } + ], + "requester_id": "agent-2", + "requester_domain": "", + "idempotency_key": "idem-2", + "canonical_jcs": "{\"idempotency_key\":\"idem-2\",\"items\":[{\"exchange\":\"one.example\",\"offer_sig\":\"sig-z\"}],\"requester_id\":\"agent-2\"}", + "signature_hex": "76bf03cc025b6a32e8da0868aa53f769cf301b5c04a7ae1d57d37fbe1adb41e6a4c09e6d4faf8c816ac1fe9805635c92184035b672f300cec43f9695ef6b990c", + "pubkey_b64": "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=", + "seed_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + } + ] +} diff --git a/sdk/parity/symbol-map.json b/sdk/parity/symbol-map.json index 433edab2..1b519595 100644 --- a/sdk/parity/symbol-map.json +++ b/sdk/parity/symbol-map.json @@ -88,6 +88,7 @@ "helpers.ComponentParam": "Go value type for an RFC 9421 covered-component parameter; py/ts model components inline.", "helpers.CoveredComponent": "Go value type for an RFC 9421 covered component; py/ts model components inline.", "helpers.ErrAcceptanceSignatureInvalid": "Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels.", + "helpers.ErrRequestAcceptanceSignatureInvalid": "Go errors.Is sentinel; py/ts return false for a request-acceptance mismatch rather than exporting a sentinel.", "helpers.ErrAudienceIdentity": "Go errors.Is sentinel for an unusable configured Exchange identity; py/ts raise/throw instead of exporting sentinels.", "helpers.ErrBrokenSignatureChain": "Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels.", "helpers.ErrDigestMismatch": "Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels.", @@ -134,6 +135,7 @@ "helpers.RegistrationDataTooManyMembers": "Member of the mapped helpers.RegistrationDataVerdict vocabulary. Python and TypeScript spell it as a literal; the shared registration-schema corpus pins the token.", "helpers.RegistrationDataUncanonicalizable": "Member of the mapped helpers.RegistrationDataVerdict vocabulary. Python and TypeScript spell it as a literal; the shared registration-schema corpus pins the token.", "helpers.RegistrationSchemaCompileTimeout": "Go-only wall-clock backstop on compilation, not part of the accepted/refused contract and deliberately absent from the ports: Go's runtime preempts, while a CPU-bound spin holds CPython's interpreter and blocks Node's event loop, so a timer there cannot interrupt the work it names. What bounds all three identically is static — the size, depth and evaluation caps and the pattern alphabet — and no admitted schema should ever reach this timeout.", + "helpers.RequestAcceptancePayload": "Go typed-protobuf builder for the request-acceptance payload; Python and TypeScript build their language-native payload objects inside the mapped canonicalizer/client.", "helpers.RetrievalAuthFailureReasonFromToken": "Go lookup from the delivery edge's refusal token to the typed enum; py/ts branch on the token string directly.", "helpers.SchemaAccepted": "Member of the mapped helpers.SchemaVerdict vocabulary. Python and TypeScript spell it as a literal; the shared registration-schema corpus pins the token.", "helpers.SchemaCompileTimeout": "Member of the mapped helpers.SchemaVerdict vocabulary. Python and TypeScript spell it as a literal; the shared registration-schema corpus pins the token.", @@ -151,6 +153,7 @@ "helpers.SchemaWrongDialect": "Member of the mapped helpers.SchemaVerdict vocabulary. Python and TypeScript spell it as a literal; the shared registration-schema corpus pins the token.", "helpers.SharedValidator": "Go protovalidate validator singleton; TS/Python ship no protovalidate face.", "helpers.SignOfferAcceptanceWith": "Go Signer-custody variant of SignOfferAcceptance so the SDK never holds the key; py/ts pass key material directly to their single acceptance signer.", + "helpers.SignRequestAcceptanceWith": "Go Signer-custody variant of SignRequestAcceptance; Python custody is a SigningTransport method and TypeScript passes its CryptoKey directly.", "helpers.SignOptions": "Go options struct for SignRequest; py/ts pass options via kwargs/options objects.", "helpers.SignatureAgentFromContext": "Go context.Context accessor; py/ts thread signature-agent state explicitly.", "helpers.SignedURL": "Go signed-URL result value type; py/ts return language-native result objects.", @@ -163,6 +166,7 @@ "helpers.VerifyOffer": "Go low-level offer-signature verify; py/ts route offer verification through the Verifier face (core.Verifier).", "helpers.VerifyOptions": "Go options struct for verify; py/ts pass options via kwargs/options objects.", "helpers.VerifyPresentedOffer": "Go low-level presented-offer freshness verify; py/ts route offer verification through the Verifier face.", + "helpers.VerifyRequestAcceptanceProjection": "Go Exchange-server projection gate; Python and TypeScript currently ship agent clients, while canonical sign/verify remains at parity and shared vectors pin the payload.", "helpers.VerifyRequestResolved": "Go resolver-injected VerifyRequest overload; py/ts expose a single verify entry point.", "helpers.WithSignatureAgent": "Go functional-option builder; py/ts pass options via kwargs/options objects.", "resolvers.ActiveKeyScanOptions": "Go scan-options struct; py/ts pass scan options inline.", @@ -375,6 +379,11 @@ "python": "jcs_acceptance_payload", "ts": "acceptancePayload" }, + "helpers.CanonicalRequestAcceptanceBytes": { + "allowlist_reason": null, + "python": "jcs_request_acceptance_payload", + "ts": "requestAcceptancePayload" + }, "helpers.CanonicalOfferBytes": { "allowlist_reason": null, "python": "canonical_offer_payload", @@ -685,6 +694,11 @@ "python": "sign_offer_acceptance_jcs", "ts": "signOfferAcceptance" }, + "helpers.SignRequestAcceptance": { + "allowlist_reason": null, + "python": "sign_request_acceptance_jcs", + "ts": "signRequestAcceptance" + }, "helpers.SignRequest": { "allowlist_reason": null, "python": "sign_request", @@ -750,6 +764,11 @@ "python": "verify_offer_acceptance_jcs", "ts": "verifyOfferAcceptance" }, + "helpers.VerifyRequestAcceptance": { + "allowlist_reason": null, + "python": "verify_request_acceptance_jcs", + "ts": "verifyRequestAcceptance" + }, "helpers.VerifyRequest": { "allowlist_reason": null, "python": "verify_request", diff --git a/sdk/python/ramp_sdk/__init__.py b/sdk/python/ramp_sdk/__init__.py index c4cef78a..cc087268 100644 --- a/sdk/python/ramp_sdk/__init__.py +++ b/sdk/python/ramp_sdk/__init__.py @@ -48,9 +48,12 @@ Verifier, canonical_offer_payload, jcs_acceptance_payload, + jcs_request_acceptance_payload, sign_offer_acceptance_jcs, sign_offer_jcs, + sign_request_acceptance_jcs, verify_offer_acceptance_jcs, + verify_request_acceptance_jcs, ) from .crossfield import cross_field_rule_ids from .errordetail import ( @@ -257,6 +260,7 @@ "is_bare_host", "is_safe_schema_pattern", "jcs_acceptance_payload", + "jcs_request_acceptance_payload", "known_restriction_token", "monotonic_window", "normalize_license_term", @@ -273,6 +277,7 @@ "sign_offer_acceptance", "sign_offer_acceptance_jcs", "sign_offer_jcs", + "sign_request_acceptance_jcs", "sign_request", "thumbprint", "transaction_denial_detail", @@ -285,6 +290,7 @@ "verify_multisig_request_server", "verify_offer_acceptance", "verify_offer_acceptance_jcs", + "verify_request_acceptance_jcs", "verify_request", "verify_request_server", "wba_directory_url", diff --git a/sdk/python/ramp_sdk/client/_verbs.py b/sdk/python/ramp_sdk/client/_verbs.py index b20c5b47..245ed161 100644 --- a/sdk/python/ramp_sdk/client/_verbs.py +++ b/sdk/python/ramp_sdk/client/_verbs.py @@ -342,13 +342,35 @@ def plan_execute( # The acceptance covers the offer, the requester and the idempotency key, so a retry # that pins the same key reproduces byte-identical acceptance bytes. That is the # deliberate-replay semantic, not an accident. + requester_id = _str_field(cfg.requester, "id") + requester_domain = _str_field(cfg.requester, "domain") + exchange = _str_field(wire, "exchange") + request_items = [(offer_sig, exchange)] + request_acceptance: dict[str, Any] | None = None try: signature, _algorithm = cfg.signer.sign_offer_acceptance( offer_sig=offer_sig, - requester_id=_str_field(cfg.requester, "id"), - requester_domain=_str_field(cfg.requester, "domain"), + requester_id=requester_id, + requester_domain=requester_domain, idempotency_key=key, ) + if exchange != "": + request_signature, _request_algorithm = cfg.signer.sign_request_acceptance( + items=request_items, + requester_id=requester_id, + requester_domain=requester_domain, + idempotency_key=key, + ) + request_acceptance = { + "payload": { + "items": [{"offer_sig": offer_sig, "exchange": exchange}], + "requester_id": requester_id, + "requester_domain": requester_domain, + "idempotency_key": key, + }, + "signature": request_signature, + "signature_algorithm": ACCEPTANCE_SIGNATURE_ALGORITHM, + } except Exception as exc: # custody can fail any way it likes raise CallError(CallErrorKind.NOT_SIGNABLE, op, cause=exc) from exc # Items-only wire shape: a single offer is the degenerate 1-element items list, each @@ -369,6 +391,8 @@ def plan_execute( } ], } + if request_acceptance is not None: + sent["agent_request_acceptance"] = request_acceptance validate_request(op, sent, TransactionRequest, cfg.validation) return _plan(cfg, _Route(op, cfg.base_url, EXCHANGE_SERVICE, "ExecuteTransaction"), sent) diff --git a/sdk/python/ramp_sdk/core.py b/sdk/python/ramp_sdk/core.py index 7c8f1648..d2c346a3 100644 --- a/sdk/python/ramp_sdk/core.py +++ b/sdk/python/ramp_sdk/core.py @@ -482,3 +482,79 @@ def verify_offer_acceptance_jcs( except (InvalidSignature, ValueError): return False return True + +def jcs_request_acceptance_payload( + *, + items: list[tuple[str, str]], + requester_id: str, + requester_domain: str, + idempotency_key: str, +) -> bytes: + """Canonical bytes for the complete ordered execute-set acceptance.""" + if not items: + raise ValueError("request acceptance requires at least one item") + refs: list[dict[str, str]] = [] + for index, (offer_sig, exchange) in enumerate(items): + if offer_sig == "": + raise ValueError(f"request acceptance item {index} has an empty offer signature") + if exchange == "": + raise ValueError(f"request acceptance item {index} has an empty exchange") + refs.append({"offer_sig": offer_sig, "exchange": exchange}) + payload: dict[str, object] = { + "items": refs, + "requester_id": requester_id, + "requester_domain": requester_domain, + "idempotency_key": idempotency_key, + } + return rfc8785.dumps( + {k: v for k, v in payload.items() if not isinstance(v, str) or v != ""} + ) + + +def sign_request_acceptance_jcs( + *, + seed: bytes, + items: list[tuple[str, str]], + requester_id: str, + requester_domain: str, + idempotency_key: str, +) -> tuple[str, str]: + """Sign the complete ordered request set; return ``(hex_signature, alg)``.""" + payload = jcs_request_acceptance_payload( + items=items, + requester_id=requester_id, + requester_domain=requester_domain, + idempotency_key=idempotency_key, + ) + priv = Ed25519PrivateKey.from_private_bytes(seed) + return priv.sign(payload).hex(), ACCEPTANCE_SIGNATURE_ALGORITHM + + +def verify_request_acceptance_jcs( + *, + pubkey_b64: str, + signature_hex: str, + items: list[tuple[str, str]], + requester_id: str, + requester_domain: str, + idempotency_key: str, +) -> bool: + """Verify a complete ordered request-set acceptance.""" + try: + payload = jcs_request_acceptance_payload( + items=items, + requester_id=requester_id, + requester_domain=requester_domain, + idempotency_key=idempotency_key, + ) + except ValueError: + return False + signature = _hex_bytes(signature_hex) + if signature is None: + return False + try: + pub = base64.b64decode(pubkey_b64) + Ed25519PublicKey.from_public_bytes(pub).verify(signature, payload) + except (InvalidSignature, ValueError): + return False + return True diff --git a/sdk/python/ramp_sdk/signing_transport.py b/sdk/python/ramp_sdk/signing_transport.py index ef3309d6..e4093047 100644 --- a/sdk/python/ramp_sdk/signing_transport.py +++ b/sdk/python/ramp_sdk/signing_transport.py @@ -17,6 +17,7 @@ from typing import TYPE_CHECKING from ramp_sdk.core import sign_offer_acceptance_jcs +from ramp_sdk.core import sign_request_acceptance_jcs from ramp_sdk.httpsig import sign_request from ramp_sdk.pop import sign_agent_binding from ramp_sdk.window import Window, clock_window @@ -114,6 +115,23 @@ def sign_offer_acceptance( idempotency_key=idempotency_key, ) + def sign_request_acceptance( + self, + *, + items: list[tuple[str, str]], + requester_id: str, + requester_domain: str, + idempotency_key: str, + ) -> tuple[str, str]: + """Sign the complete ordered execute set with the request-signing key.""" + return sign_request_acceptance_jcs( + seed=self._signer_seed, + items=items, + requester_id=requester_id, + requester_domain=requester_domain, + idempotency_key=idempotency_key, + ) + def sign_agent_binding(self, *, url: str, window: Window) -> tuple[str, str, str]: """Mint the proof of possession for one bound delivery GET; return the agent-key header value, the Signature-Input and the Signature. diff --git a/sdk/python/tests/test_client.py b/sdk/python/tests/test_client.py index e5f0a9d2..f53babf3 100644 --- a/sdk/python/tests/test_client.py +++ b/sdk/python/tests/test_client.py @@ -385,6 +385,47 @@ def test_execute_sends_the_reflected_offer_and_a_verifying_acceptance(face: Face requester_domain=REQUESTER["domain"], idempotency_key="idem-1", ) + # The request-level acceptance travels beside the per-item one. The wire + # payload must spell exactly what was signed, and the signature must verify + # the way a receiving Exchange would check it. + from ramp_sdk.core import verify_request_acceptance_jcs + + request_acceptance = body["agent_request_acceptance"] + assert request_acceptance["payload"] == { + "items": [{"offer_sig": offer["signature"], "exchange": "exchange.test"}], + "requester_id": REQUESTER["id"], + "requester_domain": REQUESTER["domain"], + "idempotency_key": "idem-1", + } + assert request_acceptance["signature_algorithm"] == "EdDSA" + assert verify_request_acceptance_jcs( + pubkey_b64=base64.b64encode(agent_public).decode(), + signature_hex=request_acceptance["signature"], + items=[(offer["signature"], "exchange.test")], + requester_id=REQUESTER["id"], + requester_domain=REQUESTER["domain"], + idempotency_key="idem-1", + ) + + +@pytest.mark.parametrize("face", FACES, ids=_IDS) +def test_execute_omits_request_acceptance_when_the_offer_names_no_exchange(face: Face) -> None: + # An offer with no exchange cannot appear in a request-acceptance item (the + # item requires a recipient), so the client sends the request without the + # field. A wire-valid offer always names its exchange, so this path exists + # only for offers that bypass the strict checks — surfaced here the same way + # the unsigned-offer test does, through a verification-off Verifier, with + # request validation off for the same reason. + offer, _public = _signed_offer(exchange="") + surfaced = Verifier( + mode=Mode.OFF, resolver=StaticOfferKeyResolver({}), now=lambda: _NOW + ).sort([offer]).verified[0] + rec = Recorder({"ver": "1.0"}) + client = face.client(_config(validation="off"), rec) + + face.run(client.execute(surfaced, idempotency_key="idem-1")) + + assert "agent_request_acceptance" not in rec.body() @pytest.mark.parametrize("face", FACES, ids=_IDS) diff --git a/sdk/python/tests/test_request_acceptance_jcs.py b/sdk/python/tests/test_request_acceptance_jcs.py new file mode 100644 index 00000000..f1be875a --- /dev/null +++ b/sdk/python/tests/test_request_acceptance_jcs.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import pytest + +from conftest import GO_TESTDATA, load_json +from ramp_sdk.core import ( + jcs_request_acceptance_payload, + sign_request_acceptance_jcs, + verify_request_acceptance_jcs, +) + +_DOC = load_json(GO_TESTDATA / "request-acceptance-vectors.json") +_VECTORS = _DOC["vectors"] + + +@pytest.mark.parametrize("vector", _VECTORS, ids=[v["name"] for v in _VECTORS]) +def test_request_acceptance_matches_go_oracle(vector: dict[str, object]) -> None: + items = [ + (str(item["offer_sig"]), str(item["exchange"])) + for item in vector["items"] # type: ignore[union-attr] + ] + kwargs = { + "items": items, + "requester_id": str(vector["requester_id"]), + "requester_domain": str(vector["requester_domain"]), + "idempotency_key": str(vector["idempotency_key"]), + } + assert jcs_request_acceptance_payload(**kwargs).decode() == vector["canonical_jcs"] + signature, algorithm = sign_request_acceptance_jcs( + seed=bytes.fromhex(str(vector["seed_hex"])), **kwargs + ) + assert algorithm == "EdDSA" + assert signature == vector["signature_hex"] + assert verify_request_acceptance_jcs( + pubkey_b64=str(vector["pubkey_b64"]), + signature_hex=str(vector["signature_hex"]), + **kwargs, + ) + + +def test_request_acceptance_order_is_signed() -> None: + vector = _VECTORS[0] + items = [ + (str(item["offer_sig"]), str(item["exchange"])) + for item in vector["items"] + ] + assert not verify_request_acceptance_jcs( + pubkey_b64=str(vector["pubkey_b64"]), + signature_hex=str(vector["signature_hex"]), + items=list(reversed(items)), + requester_id=str(vector["requester_id"]), + requester_domain=str(vector["requester_domain"]), + idempotency_key=str(vector["idempotency_key"]), + ) diff --git a/sdk/ts/client/index.ts b/sdk/ts/client/index.ts index a8fbf79c..244ad2f5 100644 --- a/sdk/ts/client/index.ts +++ b/sdk/ts/client/index.ts @@ -21,7 +21,11 @@ import type { z } from "zod"; import { clockWindow, type Window } from "../core/window.ts"; import { fromWireOffer } from "../core/wire-canon.ts"; -import { signOfferAcceptance, ACCEPTANCE_SIGNATURE_ALGORITHM } from "../src/acceptance.ts"; +import { + signOfferAcceptance, + signRequestAcceptance, + ACCEPTANCE_SIGNATURE_ALGORITHM, +} from "../src/acceptance.ts"; import { redactUserinfo } from "../src/host-ref.ts"; import { isBareDomain } from "../src/hosts.ts"; import { generateIdempotencyKey } from "../src/idempotency.ts"; @@ -500,6 +504,9 @@ async function execute( ? opts.idempotencyKey : generateIdempotencyKey(); const requester = r.opts.requester; + const requesterId = stringField(requester, "id"); + const requesterDomain = stringField(requester, "domain"); + const requestItems = [{ offerSig, exchange: stringField(wire, "exchange") }]; // The acceptance covers the offer, the requester and the idempotency key, so a retry // that pins the same key reproduces byte-identical acceptance bytes. That is the // deliberate-replay semantic, not an accident. @@ -508,8 +515,8 @@ async function execute( signature = await signOfferAcceptance( { offerSig, - requesterId: stringField(requester, "id"), - requesterDomain: stringField(requester, "domain"), + requesterId, + requesterDomain, idempotencyKey: key, }, r.opts.signer.privKey, @@ -517,6 +524,17 @@ async function execute( } catch (cause) { throw new RampCallError({ kind: "not_signable", op, cause }); } + let requestSignature: string | undefined; + if (requestItems[0]!.exchange !== "") { + try { + requestSignature = await signRequestAcceptance( + { items: requestItems, requesterId, requesterDomain, idempotencyKey: key }, + r.opts.signer.privKey, + ); + } catch (cause) { + throw new RampCallError({ kind: "not_signable", op, cause }); + } + } // Items-only wire shape: a single offer is the degenerate 1-element items list, each // item reflecting its signed Offer back exactly as received at discovery. The // authoritative identity is the reflected offer; the optional top-level offer_id @@ -534,6 +552,20 @@ async function execute( }, }, ], + ...(requestSignature === undefined + ? {} : { agent_request_acceptance: { + payload: { + items: requestItems.map((item) => ({ + offer_sig: item.offerSig, + exchange: item.exchange, + })), + requester_id: requesterId, + requester_domain: requesterDomain, + idempotency_key: key, + }, + signature: requestSignature, + signature_algorithm: ACCEPTANCE_SIGNATURE_ALGORITHM, + } }), }; validateRequest(op, request, TransactionRequestSchema, r.opts.validation ?? "strict"); const raw = await call( diff --git a/sdk/ts/src/acceptance.ts b/sdk/ts/src/acceptance.ts index f7d44581..66887d06 100644 --- a/sdk/ts/src/acceptance.ts +++ b/sdk/ts/src/acceptance.ts @@ -134,3 +134,78 @@ export async function verifyOfferAcceptance( return false; } } + +export interface RequestAcceptanceItemInput { + offerSig: string; + exchange: string; +} + +export interface RequestAcceptanceInput { + items: RequestAcceptanceItemInput[]; + requesterId: string; + requesterDomain: string; + idempotencyKey: string; +} + +/** Canonical JCS(protojson(...)) bytes for the complete ordered execute set. */ +export function requestAcceptancePayload( + input: RequestAcceptanceInput, +): Uint8Array { + if (input.items.length === 0) { + throw new Error("ramp/acceptance: request acceptance requires at least one item"); + } + const items = input.items.map((item, index) => { + if (item.offerSig === "") { + throw new Error( + `ramp/acceptance: request item ${index} has an empty offer signature`, + ); + } + if (item.exchange === "") { + throw new Error(`ramp/acceptance: request item ${index} has an empty exchange`); + } + return { offer_sig: item.offerSig, exchange: item.exchange }; + }); + const payload: Record = { + items, + requester_id: input.requesterId, + requester_domain: input.requesterDomain, + idempotency_key: input.idempotencyKey, + }; + const obj = Object.fromEntries( + Object.entries(payload).filter(([, value]) => value !== ""), + ); + const jcs = canonicalize(obj); + if (jcs === undefined) { + throw new Error("ramp/acceptance: request payload is not JSON-serializable"); + } + return utf8Bytes(jcs); +} + +export async function signRequestAcceptance( + input: RequestAcceptanceInput, + privateKey: CryptoKey, +): Promise { + const payload = requestAcceptancePayload(input); + const sig = new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, payload)); + return bytesToHex(sig); +} + +export async function verifyRequestAcceptance( + input: RequestAcceptanceInput, + signatureHex: string, + publicKey: CryptoKey, +): Promise { + let payload: Uint8Array; + try { + payload = requestAcceptancePayload(input); + } catch { + return false; + } + const signature = hexToBytes(signatureHex); + if (signature === undefined) return false; + try { + return await crypto.subtle.verify("Ed25519", publicKey, signature, payload); + } catch { + return false; + } +} diff --git a/sdk/ts/tests/client.test.ts b/sdk/ts/tests/client.test.ts index ae6f736e..01a2ded0 100644 --- a/sdk/ts/tests/client.test.ts +++ b/sdk/ts/tests/client.test.ts @@ -17,7 +17,7 @@ import { } from "../client/index.ts"; import { createVerifier } from "../core/verifier.ts"; import { signOffer } from "../src/offer-sign.ts"; -import { verifyOfferAcceptance } from "../src/acceptance.ts"; +import { verifyOfferAcceptance, verifyRequestAcceptance } from "../src/acceptance.ts"; const REQUESTER = { id: "agent-1", domain: "agent.test", type: "REQUESTER_TYPE_AGENT" }; @@ -372,6 +372,58 @@ describe("execute", () => { keys.publicKey, ), ).resolves.toBe(true); + // The request-level acceptance travels beside the per-item one. The wire + // payload must spell exactly what was signed, and the signature must + // verify the way a receiving Exchange would check it. + const requestAcceptance = body["agent_request_acceptance"] as Record; + expect(requestAcceptance["payload"]).toEqual({ + items: [{ offer_sig: offer["signature"], exchange: "exchange.test" }], + requester_id: REQUESTER.id, + requester_domain: REQUESTER.domain, + idempotency_key: "idem-1", + }); + expect(requestAcceptance["signature_algorithm"]).toBe("EdDSA"); + await expect( + verifyRequestAcceptance( + { + items: [{ offerSig: offer["signature"] as string, exchange: "exchange.test" }], + requesterId: REQUESTER.id, + requesterDomain: REQUESTER.domain, + idempotencyKey: "idem-1", + }, + requestAcceptance["signature"] as string, + keys.publicKey, + ), + ).resolves.toBe(true); + }); + + it("omits the request acceptance when the offer names no exchange", async () => { + // An offer with no exchange cannot appear in a request-acceptance item + // (the item requires a recipient), so the client sends the request + // without the field. A wire-valid offer always names its exchange, so + // this path is reachable only through verification "off" — surfaced the + // same way the unsigned-offer test does, with request validation off + // for the same reason. + const { offer } = await signedOffer(""); + const verifier = createVerifier("off", { + resolve: async () => undefined, + now: () => 0, + }); + const surfaced = (await verifier.sort([offer])).verified[0]; + const keys = await agentKeys(); + const { send, seen } = recordingSend({ ver: "1.0" }); + const client = createClient("https://exchange.test", { + requester: REQUESTER, + signer: { privKey: keys.privateKey, keyid: "agent.v1" }, + send, + validation: "off", + }); + + await client.execute(surfaced as NonNullable, { + idempotencyKey: "idem-1", + }); + + expect(bodyOf(seen[0] as UnaryRequest)).not.toHaveProperty("agent_request_acceptance"); }); it("mints a fresh idempotency key when none is pinned", async () => { diff --git a/sdk/ts/tests/request-acceptance.parity.test.ts b/sdk/ts/tests/request-acceptance.parity.test.ts new file mode 100644 index 00000000..1614dc1b --- /dev/null +++ b/sdk/ts/tests/request-acceptance.parity.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect } from "vitest"; +import { + requestAcceptancePayload, + signRequestAcceptance, + verifyRequestAcceptance, +} from "../src/acceptance.ts"; +import vectors from "../../go/helpers/testdata/request-acceptance-vectors.json"; + +const PKCS8_ED25519_PREFIX = Uint8Array.from([ + 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, + 0x22, 0x04, 0x20, +]); + +function hexToBytes(hex: string): Uint8Array { + const out = new Uint8Array(hex.length / 2); + for (let i = 0; i < out.length; i += 1) { + out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + return out; +} + +async function privateKey(seedHex: string): Promise { + const seed = hexToBytes(seedHex); + const pkcs8 = new Uint8Array(PKCS8_ED25519_PREFIX.length + seed.length); + pkcs8.set(PKCS8_ED25519_PREFIX); + pkcs8.set(seed, PKCS8_ED25519_PREFIX.length); + return crypto.subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, false, ["sign"]); +} + +async function publicKey(value: string): Promise { + const bin = atob(value); + const raw = Uint8Array.from(bin, (char) => char.charCodeAt(0)); + return crypto.subtle.importKey("raw", raw, { name: "Ed25519" }, false, ["verify"]); +} + +describe("request acceptance matches the Go oracle", () => { + for (const vector of vectors.vectors) { + const input = { + items: vector.items.map((item) => ({ + offerSig: item.offer_sig, + exchange: item.exchange, + })), + requesterId: vector.requester_id, + requesterDomain: vector.requester_domain, + idempotencyKey: vector.idempotency_key, + }; + + it(`${vector.name}: canonical bytes and signature`, async () => { + expect(new TextDecoder().decode(requestAcceptancePayload(input))).toBe( + vector.canonical_jcs, + ); + expect(await signRequestAcceptance(input, await privateKey(vector.seed_hex))).toBe( + vector.signature_hex, + ); + expect( + await verifyRequestAcceptance( + input, + vector.signature_hex, + await publicKey(vector.pubkey_b64), + ), + ).toBe(true); + }); + } + + it("signs item order", async () => { + const vector = vectors.vectors[0]!; + const input = { + items: vector.items + .map((item) => ({ offerSig: item.offer_sig, exchange: item.exchange })) + .reverse(), + requesterId: vector.requester_id, + requesterDomain: vector.requester_domain, + idempotencyKey: vector.idempotency_key, + }; + expect( + await verifyRequestAcceptance( + input, + vector.signature_hex, + await publicKey(vector.pubkey_b64), + ), + ).toBe(false); + }); +}); diff --git a/website/src/content/docs/components/broker/overview.mdx b/website/src/content/docs/components/broker/overview.mdx index a34d3233..d4f28f31 100644 --- a/website/src/content/docs/components/broker/overview.mdx +++ b/website/src/content/docs/components/broker/overview.mdx @@ -122,6 +122,19 @@ func (o *Broker) Execute(ctx context.Context, offer *rampv1.Offer, req *rampv1.D } ``` +### Mixed-Exchange commits: fan-out is projection + +When the agent commits to offers from **more than one** Exchange in one go, there is still no new RPC: the flow is `Resolve` for discovery, then one `ExecuteTransaction` per Exchange named in the chosen set. What holds the set together is `TransactionRequest.agent_request_acceptance` — a detached Ed25519 signature by the **agent** over the complete ordered list of `(offer signature, exchange)` references plus the requester and the idempotency key. + +The sequence: + +1. The agent (the holder of the signing key) commits to the whole chosen set at once: it signs one `AgentRequestAcceptance` covering every selected offer, in order, across all Exchanges. +2. Whoever executes — the agent directly, or the Broker on its behalf — sends one `ExecuteTransaction` to each Exchange in the set. Each subrequest carries only the items addressed to that Exchange, plus the **unchanged** acceptance envelope, plus the same idempotency key. +3. Each per-Exchange subrequest is a new HTTP request its sender authors and RFC 9421-signs. When the Broker executes, the Broker is the authenticated wire caller of every subrequest; the agent's authorization travels in the body as the detached acceptance signatures, which survive projection because they never depended on the HTTP envelope. See [Projected execute requests](/protocol/authentication/#projected-execute-requests) for who signs what and how the Exchange resolves the acceptance verification key. +4. Each Exchange verifies the acceptance signature and requires its subrequest to equal the complete in-order projection of the signed set addressed to itself — so the Broker cannot drop, reorder, append, or split the items for that Exchange without being refused. + +A single-Exchange commit is the degenerate case: the projection is the whole set, and the SDK clients attach the acceptance there too. + ## Request Signing and the Forwarding Signature Stack RAMP uses a two-layer signing model: the **agent's request signature** proves who originated the request, and the **forwarding signature stack** proves which intermediaries forwarded it. diff --git a/website/src/content/docs/components/exchange/request-flows.mdx b/website/src/content/docs/components/exchange/request-flows.mdx index 5c9adf12..e039b62d 100644 --- a/website/src/content/docs/components/exchange/request-flows.mdx +++ b/website/src/content/docs/components/exchange/request-flows.mdx @@ -314,7 +314,7 @@ This is ongoing verification, not just onboarding. Providers can revoke authoriz ## ExecuteTransaction -The critical path. Stateless offer verification — no offer storage or re-resolution. A request carries one or more `items` (items-only model): each item presents the FULL signed offer it was issued, and the Exchange verifies that presented offer against its own offer-signing key. A single offer is the degenerate 1-element `items` list. Per item the pipeline is: validate -> verify the presented offer's signature (Ed25519 default) -> check idempotency -> authorize billing -> write transaction log -> sign URL -> respond. +The critical path. Stateless offer verification — no offer storage or re-resolution. A request carries one or more `items` (items-only model): each item presents the FULL signed offer it was issued, and the Exchange verifies that presented offer against its own offer-signing key. A single offer is the degenerate 1-element `items` list. Per item the pipeline is: validate -> verify the presented offer's signature (Ed25519 default) -> check idempotency -> authorize billing -> write transaction log -> sign URL -> respond. Before the idempotency check, a request that carries `agent_request_acceptance` has that set claim verified first — the signature, and that this request's items are the complete in-order projection of the signed set addressed to this Exchange (see [Projected execute requests](/protocol/authentication/#projected-execute-requests)). Idempotency state is never created or served for a request whose set claim failed. ```mermaid sequenceDiagram diff --git a/website/src/content/docs/protocol/authentication.mdx b/website/src/content/docs/protocol/authentication.mdx index db4b38c9..0ead141c 100644 --- a/website/src/content/docs/protocol/authentication.mdx +++ b/website/src/content/docs/protocol/authentication.mdx @@ -312,6 +312,21 @@ When only the agent's signature is present, the agent is querying the Exchange d See [Multi-Hop Example (Broker)](#multi-hop-example-broker) below for the concrete header layout. +### Projected execute requests + +The hop-signature stack above applies to requests forwarded **byte-for-byte**. Projection is the other case. When a Broker splits a mixed-Exchange `TransactionRequest` into per-Exchange subrequests, each subrequest has a new body, so the agent's original signature — which covered the original body through `Content-Digest` — cannot travel with it. There is no way to forward a projected body under the agent's HTTP signature, and the protocol does not ask for one. + +Instead, each projected subrequest is a **new HTTP request its sender authors**. The projecting party — a Broker, or the agent itself when it splits its own set — computes the subrequest's `Content-Digest` and signs it with its own RFC 9421 signature, exactly as any sender does. The Exchange authenticates the wire caller as that sender. + +The agent's cryptographic authorization does not ride the transport layer at all. It rides in the body, as two detached Ed25519 signatures that stay valid however the request travels: + +- **`agent_acceptance`** on each item — binds that offer to the requester and the idempotency key. +- **`agent_request_acceptance`** on the request — binds the **complete ordered set** of `(offer signature, exchange)` references, so each Exchange can require its subrequest to be the exact in-order projection of the set the agent signed. See [Broker fan-out and request projection](/components/broker/overview/#execution-separate-step) for the flow. + +The Exchange resolves the `agent_request_acceptance` verification key from the requester the signed payload names: the payload's `requester_domain` must equal the request's `requester.domain`, and the signature is accepted only if it verifies against an Ed25519 key currently valid in that domain's WBA directory (`{requester_domain}/.well-known/http-message-signatures-directory`). The envelope carries no `keyid`, so the verifier tries the currently-valid Ed25519 keys of that directory — rotation overlap keeps that set small. When the requester itself signed the arriving request, the request-signing key the Exchange already resolved is that key, and no second fetch is needed. A signature that verifies against a key the requester's domain publishes is what makes the claimed requester identity an authenticated one, the same anchoring every other directory lookup uses. + +Delegation is unchanged by projection: the holder-binding check still matches the key that RFC 9421-signed the **arriving** request (see [Which signature binds in a brokered request](#verification)). A projecting Broker is the wire signer, so it may project a delegated request only when the agent has explicitly delegated to the Broker's key — the same terminal-holder rule as narrowing. A pure relay cannot project a delegated request; without a delegation, the Broker simply authenticates as itself while the body's detached signatures carry the agent's authorization. + ## Request Signatures (RFC 9421) ### Why headers, not message fields diff --git a/website/src/content/docs/protocol/scenario-walkthrough.mdx b/website/src/content/docs/protocol/scenario-walkthrough.mdx index 1abde2d3..a23761a9 100644 --- a/website/src/content/docs/protocol/scenario-walkthrough.mdx +++ b/website/src/content/docs/protocol/scenario-walkthrough.mdx @@ -731,6 +731,12 @@ The request body is identical to the direct case; the forwarding chain lives in keyid thumbprint in the Exchange's configured key set (Signature-Agent stays the agent's directory) - Ed25519 verify each hop's signature -> pass +2c. Verify `agent_request_acceptance` when the request carries it (this example + omits the optional field): Ed25519 verify the signed request set against the + requester's published agent key, then require this request's items to be the + complete in-order projection addressed to this Exchange. This runs before + the idempotency check — see + [Projected execute requests](/protocol/authentication/#projected-execute-requests) 3. Check idempotency: tx-claude-001 not seen before -> proceed 4. Verify offer signature: - Ed25519 verify exchange_signature on offer -> pass diff --git a/website/src/content/docs/protocol/transaction-flow.mdx b/website/src/content/docs/protocol/transaction-flow.mdx index 15bf0af5..7885ca30 100644 --- a/website/src/content/docs/protocol/transaction-flow.mdx +++ b/website/src/content/docs/protocol/transaction-flow.mdx @@ -313,6 +313,8 @@ Content-Type: application/json Each item's `offer` is the full signed Offer reflected back exactly as received at discovery; a single-offer transaction is the degenerate one-element `items` list. The Exchange verifies each item's `offer.signature` over the presented Offer bytes to confirm the offer has not been tampered with. The Exchange is stateless -- it verifies the self-contained signed Offer rather than storing offers. +The request may also carry `agent_request_acceptance` — the agent's detached Ed25519 signature over the complete ordered set of offers it committed to, across all Exchanges. The SDK clients attach it whenever the offer names its Exchange. It is what lets an Exchange receiving a Broker-projected subrequest prove the items are the exact in-order subset the agent signed — see [Projected execute requests](/protocol/authentication/#projected-execute-requests). The field is optional for wire compatibility: an older client that omits it keeps per-item execution semantics and simply gets no request-level claim. + ### Response (`TransactionResponse`) ```json @@ -347,13 +349,14 @@ The `retrieval_endpoint` is a CDN signed URL bound to `agent_identity_hash`. A b ### What Happens Inside the Exchange 1. Validate request (proto validation) -2. Verify the agent's RFC 9421 HTTP Message Signature (alg=ed25519) in the request headers (and each broker hop signature if present) -3. Check idempotency key (prevent double-charge on retry) -4. Verify offer signature (reconstruct offer from signed token) -5. Authorize billing (`BillingAdapter.Authorize`) -- or skip for subscription offers -6. Write transaction to WAL (must succeed before signing URL) -7. Generate the Ed25519 signed delivery URL (or a CloudFront RSA canned policy, per the tenant's signing scheme) -8. Create reporting obligation with deadline +2. Verify the sender's RFC 9421 HTTP Message Signature (alg=ed25519) in the request headers — the agent's on a direct request, a projecting Broker's on a projected one — and each prior hop signature if present +3. Verify `agent_request_acceptance` when present: the signature, and that this request's items are the complete in-order projection of the signed set addressed to this Exchange. This runs **before** the idempotency lookup — request-level idempotency state is never created or served for a request whose set claim has not been proven, otherwise a mutated request could consume the key and a later honest retry would replay its result +4. Check idempotency key (prevent double-charge on retry) +5. Verify offer signature (reconstruct offer from signed token) +6. Authorize billing (`BillingAdapter.Authorize`) -- or skip for subscription offers +7. Write transaction to WAL (must succeed before signing URL) +8. Generate the Ed25519 signed delivery URL (or a CloudFront RSA canned policy, per the tenant's signing scheme) +9. Create reporting obligation with deadline ### Batch TransactionRequest diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index e39f9eff..c69f288a 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -8,6 +8,27 @@ and protocol history, see [`proto/CHANGELOG.md`](https://github.com/RAMP-Protoco ## Unreleased +**`TransactionRequest.agent_request_acceptance` adds an agent-signed proof of +the complete ordered request set (additive wire change).** The agent signs the +ordered list of `(offer signature, exchange)` references it committed to, plus +the requester and the idempotency key, with the same detached-Ed25519 +convention `AgentAcceptance` uses. When a Broker projects a mixed-Exchange +request into per-Exchange subrequests it forwards the envelope unchanged, and +each Exchange requires its subrequest to be the complete in-order projection of +the signed set addressed to itself before creating or serving request-level +idempotency state. That is what stops a relay from consuming an agent's +idempotency key with a removed, appended, reordered, or valid-subset-first +request — a property the RFC 9421 forwarding-signature stack does not provide, +since a projected subrequest is a new HTTP request its sender authors and signs +(see [Projected execute +requests](/protocol/authentication/#projected-execute-requests)). + +The field is optional for wire compatibility: an older client that omits it +keeps per-item execution semantics and gets no request-level claim. The signed +item list is capped at 256 entries, the same ceiling a discovery query's `uris` +list carries, and verifiers bound their own work to that cap before rendering +the payload to canonical form. + **Signed delivery URLs are documented as Ed25519 signed by the Exchange and verified with its published public key, not HMAC-SHA256 over a shared secret (documentation correction; no wire change).** Since the initial public snapshot diff --git a/website/src/content/docs/reference/proto-ramp.mdx b/website/src/content/docs/reference/proto-ramp.mdx index 25dc321b..b5cfbf7c 100644 --- a/website/src/content/docs/reference/proto-ramp.mdx +++ b/website/src/content/docs/reference/proto-ramp.mdx @@ -164,6 +164,24 @@ The agent's detached acceptance signature over an accepted Offer. It travels in ::proto-message{name=AgentAcceptance} +### AgentRequestAcceptance + +The agent's detached authorization of one complete ordered execute set. Its payload travels with the signature so a Broker can forward the same proof unchanged on every per-Exchange projection. A receiving Exchange verifies the agent signature and requires its subrequest to equal the complete in-order projection of signed items addressed to that Exchange before creating or serving request-level idempotency state. + +::proto-message{name=AgentRequestAcceptance} + +### AgentRequestAcceptancePayload + +The signed field set: ordered [`AgentRequestAcceptanceItem`](#agentrequestacceptanceitem) references plus the requester identity and idempotency key. Canonical bytes use the same RFC 8785 JCS over canonical proto-JSON rule as Offer and AgentAcceptance signatures. + +::proto-message{name=AgentRequestAcceptancePayload} + +### AgentRequestAcceptanceItem + +A signed request-set reference containing `offer_sig` and `exchange`. The offer signature binds the complete Offer; the explicit issuing Exchange lets each fan-out recipient derive the exact subset it must receive without copying every full Offer into every subrequest. + +::proto-message{name=AgentRequestAcceptanceItem} + ### AgentAcceptancePayload The canonical signing structure for [`AgentAcceptance`](#agentacceptance). It is **never sent on the wire** — this message fixes the *field set*, and the *byte layout* is the canonical signing form defined on `Offer.signature`: RFC 8785 JCS over canonical proto-JSON with a pinned option set. Both halves are normative, so signer and verifier derive byte-identical signed bytes in any language without a protobuf binary codec, and the contract cannot drift between implementations. `offer_sig` is the accepted `Offer.signature` (which transitively binds pricing, terms, and expiry); `requester_id`, `requester_domain`, and `idempotency_key` come from the enclosing `TransactionRequest`. diff --git a/website/src/content/docs/security/threat-model.mdx b/website/src/content/docs/security/threat-model.mdx index 55c69c6e..c0121b0b 100644 --- a/website/src/content/docs/security/threat-model.mdx +++ b/website/src/content/docs/security/threat-model.mdx @@ -116,6 +116,11 @@ This single pattern accounts for most ad-tech fraud: domain spoofing (self-repor **Attack**: Broker logs every DiscoverResources query, sells the data. **Countermeasure**: Not solvable at protocol level. Data handling agreements required. +### T-BRK-1: Request-set mutation during execute fan-out +**Attack**: The agent commits to an ordered set of offers and hands execution to a Broker. While projecting the set into per-Exchange `ExecuteTransaction` subrequests, the Broker mutates it at the **item** level: it removes an item, appends one the agent never chose, reorders the items, or sends a valid subset first so the agent's idempotency key is consumed by a request the agent did not make — a later honest retry then replays the mutilated result instead of executing the real set. +**Why the hop stack does not cover this**: The RFC 9421 forwarding-signature stack (T20, T-DEL-4) proves who forwarded a request that travels **byte-for-byte**. A projected subrequest is a new HTTP request the Broker authors and signs itself, so hop-chain integrity says nothing about which items the projected body carries — see [Projected execute requests](/protocol/authentication/#projected-execute-requests). +**Countermeasure (protocol)**: **`TransactionRequest.agent_request_acceptance`.** The agent signs the complete ordered set of `(offer signature, exchange)` references plus the requester and idempotency key with its own Ed25519 key, and the Broker must forward the envelope unchanged on every subrequest. Each Exchange verifies the signature and requires its subrequest to equal the complete in-order projection of the signed set addressed to itself, **before** creating or serving any request-level idempotency state. A removed, appended, reordered, or subset request fails the projection check and claims nothing. The field is optional for wire compatibility: an older client that omits it keeps per-item execution semantics (each item still carries its own `agent_acceptance`) but gets no request-level set claim. + ## 5. Third-Party Attacks ### T22: Exchange impersonation @@ -172,7 +177,7 @@ This single pattern accounts for most ad-tech fraud: domain spoofing (self-repor | Level | Threat IDs | What It Means | |---|---|---| -| **Preventable at protocol level** | T3, T4, T5, T6, T7, T8, T9, T12, T16, T19, T22, T23, T24, T25, T26, T-ATT-1, T-ATT-2, T-ATT-3, T-ATT-5, T-DEL-1, T-DEL-2, T-DEL-3, T-DEL-4, T-DEL-5, T-DEL-6 | Protocol changes make the attack structurally impossible | +| **Preventable at protocol level** | T3, T4, T5, T6, T7, T8, T9, T12, T16, T19, T22, T23, T24, T25, T26, T-BRK-1, T-ATT-1, T-ATT-2, T-ATT-3, T-ATT-5, T-DEL-1, T-DEL-2, T-DEL-3, T-DEL-4, T-DEL-5, T-DEL-6 | Protocol changes make the attack structurally impossible | | **Detectable via reconciliation** | T1, T2, T4, T5, T13, T14, T15, T17, T27, T-ATT-4 | Three-sided reconciliation catches it | | **Legal/contractual only** | T10, T11, T13, T17, T18, T20, T21, T28, T29, T30 | Can't enforce technically | @@ -181,8 +186,10 @@ This single pattern accounts for most ad-tech fraud: domain spoofing (self-repor ``` Line 1: Protocol-level prevention Signed offers, content hashes, agent-bound URLs, idempotency keys, - holder-bound JWT delegation chain (cnf), RFC 9421 forwarding-chain verification - (stack of per-hop HTTP Message Signatures), subscription-level quota tracking + agent-signed request-set proof (request acceptance, verified before + idempotency state), holder-bound JWT delegation chain (cnf), + RFC 9421 forwarding-chain verification (stack of per-hop HTTP Message + Signatures), subscription-level quota tracking Makes attacks structurally impossible Line 2: Three-sided reconciliation