From 58a9a8e6de6cdb9c56596006d9daf7cab8c451e3 Mon Sep 17 00:00:00 2001 From: legendko Date: Wed, 22 Jul 2026 18:55:57 +0200 Subject: [PATCH 01/21] feat(sdk-go): export CanonicalOfferBytes Expose the previously-unexported offer canonicalization as CanonicalOfferBytes: RFC 8785 JCS over canonical proto-JSON with signature/signature_algorithm cleared and expires_at included -- the exact bytes an Offer signature is computed over. Callers persisting a signed offer can now store and re-verify those bytes verbatim, independent of how the Offer message later evolves. SignOffer and VerifyOffer route through it, so there is a single canonicalization path and behavior is unchanged (conformance vectors byte-identical). Go-only: Python/TS have no consumer for the raw signed bytes and route offer signing/verification through their sign/verify faces, so the symbol is registered in symbol-map.json go_exclusions and the parity matrix regenerated. No proto or wire change. --- docs/sdk-parity-matrix.md | 3 +- sdk/go/helpers/gen_vectors_test.go | 6 +- sdk/go/helpers/offer.go | 23 +++--- sdk/go/helpers/offer_test.go | 71 +++++++++++++++++++ sdk/parity/symbol-map.json | 1 + .../src/content/docs/reference/changelog.mdx | 9 +++ 6 files changed, 98 insertions(+), 15 deletions(-) diff --git a/docs/sdk-parity-matrix.md b/docs/sdk-parity-matrix.md index fa933c9..97fc235 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:** 72 symbols at cross-language parity · 14 documented divergences · 98 Go-idiomatic exclusions · 23 conformance corpora, each tri-replayed. +**At a glance:** 72 symbols at cross-language parity · 14 documented divergences · 99 Go-idiomatic exclusions · 23 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). @@ -198,6 +198,7 @@ Go constructs (functional-option builders, `errors.Is` sentinels, value types, c | `helpers.AlgEd25519` | RFC 9421 alg tag constant; inlined per language. | | `helpers.AllSignaturesFromContext` | Go context.Context accessor; py/ts thread multisig state explicitly. | | `helpers.BrokerKeyIDPrefix` | Relay keyID wire-prefix constant; inlined per language. | +| `helpers.CanonicalOfferBytes` | Go-only canonical-offer-bytes accessor (the verbatim JCS-over-proto-JSON bytes an Offer signature covers, for callers persisting re-verifiable offer evidence); py/ts route offer signing/verification through SignOffer / the Verifier face and never need the raw signed bytes. | | `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. | diff --git a/sdk/go/helpers/gen_vectors_test.go b/sdk/go/helpers/gen_vectors_test.go index 99bb19d..2ca8d8b 100644 --- a/sdk/go/helpers/gen_vectors_test.go +++ b/sdk/go/helpers/gen_vectors_test.go @@ -317,7 +317,7 @@ type offerVerifyVector struct { NowUnix int64 `json:"now_unix"` ExpectedVerified bool `json:"expected_verified"` // ExchangeSeedHex is the exchange offer-signing seed (fixedSeed 0x66), so a - // sign-face port re-signs canonicalOfferPayload(offer_json) and byte-matches + // sign-face port re-signs CanonicalOfferBytes(offer_json) and byte-matches // the signature already embedded in offer_json. ExchangeSeedHex string `json:"exchange_seed_hex"` } @@ -483,7 +483,7 @@ func buildOfferVerifyVectors(t *testing.T) []offerVerifyVector { // offer exactly as the Connect codec emits it (camelCase json_names, enums as // names, EmitUnpopulated zero-inflation; JCS-stabilized so the committed file is // deterministic), canonical_json is the byte sequence the offer signature covers -// (canonicalOfferPayload: signature/signature_algorithm cleared, snake_case, +// (CanonicalOfferBytes: signature/signature_algorithm cleared, snake_case, // omit-unpopulated, JCS). A from-wire canonicalizer in any language must map // wire_json to canonical_json exactly. type wireCanonicalVector struct { @@ -516,7 +516,7 @@ func buildWireCanonicalVectors(t *testing.T) []wireCanonicalVector { if err != nil { t.Fatalf("%s: wire JCS: %v", name, err) } - canonical, err := canonicalOfferPayload(offer) + canonical, err := CanonicalOfferBytes(offer) if err != nil { t.Fatalf("%s: canonical payload: %v", name, err) } diff --git a/sdk/go/helpers/offer.go b/sdk/go/helpers/offer.go index 5054a25..fa97b52 100644 --- a/sdk/go/helpers/offer.go +++ b/sdk/go/helpers/offer.go @@ -31,7 +31,7 @@ func SignOffer(priv ed25519.PrivateKey, offer *rampv1.Offer) (string, error) { if len(priv) != ed25519.PrivateKeySize { return "", fmt.Errorf("helpers: ed25519 private key must be %d bytes, got %d", ed25519.PrivateKeySize, len(priv)) } - payload, err := canonicalOfferPayload(offer) + payload, err := CanonicalOfferBytes(offer) if err != nil { return "", err } @@ -53,7 +53,7 @@ func VerifyOffer(offer *rampv1.Offer, signatureHex string, pub ed25519.PublicKey // signature must land on the same rejected path as a forged one. return fmt.Errorf("%w: decode hex: %v", ErrOfferSignatureInvalid, err) } - payload, err := canonicalOfferPayload(offer) + payload, err := CanonicalOfferBytes(offer) if err != nil { return err } @@ -63,21 +63,22 @@ func VerifyOffer(offer *rampv1.Offer, signatureHex string, pub ed25519.PublicKey return nil } -// canonicalOfferPayload is the canonical byte sequence the offer signature -// covers: the ENTIRE Offer (pricing, terms, expires_at, …) with ONLY the -// signature and signature_algorithm fields cleared, per ramp.proto Offer.signature. +// CanonicalOfferBytes returns the exact canonical byte sequence an Offer's +// signature is computed over: the ENTIRE Offer (pricing, terms, expires_at, …) +// with ONLY the signature and signature_algorithm fields cleared, per +// ramp.proto Offer.signature. The returned bytes are byte-identical to what +// SignOffer signs and VerifyOffer verifies over — persist them to re-verify an +// Offer signature verbatim, independent of how the Offer message later evolves. // // The canonical form is RFC 8785 JCS over canonical proto-JSON — // JCS(protojson(offer with sig cleared)) — via canonicalSignPayload, so any // language (Go/TS/Python) reproduces the exact signed bytes without a protobuf // binary codec. See canonicalsign.go for the pinned proto-JSON option set. // -// NOTE (expires_at reconciliation): the protocol spec covers expires_at so a -// relaying Broker cannot extend/shorten a signed offer's TTL. The pre-SDK -// service-internal signer additionally cleared expires_at (a stateless-reissue -// workaround); this SDK follows the protocol. The platform adopts this behavior -// when it re-pins onto the SDK — see the filed reconciliation task. -func canonicalOfferPayload(offer *rampv1.Offer) ([]byte, error) { +// expires_at is covered (only signature/signature_algorithm are cleared), so a +// relaying Broker cannot extend or shorten a signed offer's TTL under an +// otherwise-valid signature. +func CanonicalOfferBytes(offer *rampv1.Offer) ([]byte, error) { if offer == nil { return nil, errors.New("helpers: offer is nil") } diff --git a/sdk/go/helpers/offer_test.go b/sdk/go/helpers/offer_test.go index 1907571..036687e 100644 --- a/sdk/go/helpers/offer_test.go +++ b/sdk/go/helpers/offer_test.go @@ -1,7 +1,9 @@ package helpers_test import ( + "bytes" "crypto/ed25519" + "encoding/hex" "errors" "testing" "time" @@ -98,3 +100,72 @@ func TestVerifyOffer_nil(t *testing.T) { t.Error("nil offer should error") } } + +func TestCanonicalOfferBytes_matchesSignOffer(t *testing.T) { + // The bytes CanonicalOfferBytes returns are exactly what SignOffer signed: + // verifying the SignOffer signature directly over them must hold. This is the + // property a caller relies on to persist verbatim, re-verifiable offer evidence. + pub, priv, _ := ed25519.GenerateKey(nil) + offer := sampleOffer() + sigHex, err := helpers.SignOffer(priv, offer) + if err != nil { + t.Fatal(err) + } + canon, err := helpers.CanonicalOfferBytes(offer) + if err != nil { + t.Fatal(err) + } + sig, err := hex.DecodeString(sigHex) + if err != nil { + t.Fatal(err) + } + if !ed25519.Verify(pub, canon, sig) { + t.Error("SignOffer signature must verify over CanonicalOfferBytes") + } +} + +func TestCanonicalOfferBytes_ignoresSignatureFields(t *testing.T) { + // signature/signature_algorithm are cleared on a clone before canonicalizing, + // so an already-signed offer yields the same bytes as the unsigned one — the + // signature cannot cover itself. + offer := sampleOffer() + before, err := helpers.CanonicalOfferBytes(offer) + if err != nil { + t.Fatal(err) + } + offer.Signature = "deadbeef" + offer.SignatureAlgorithm = helpers.OfferSignatureAlgorithm + after, err := helpers.CanonicalOfferBytes(offer) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(before, after) { + t.Error("CanonicalOfferBytes must clear signature/signature_algorithm; bytes differ") + } +} + +func TestCanonicalOfferBytes_coversExpiresAt(t *testing.T) { + // expires_at is inside the signed bytes (only the signature fields are + // cleared), so changing it changes the canonical output — the property that + // stops a relaying Broker from re-stretching a signed offer's TTL. + a := sampleOffer() + b := sampleOffer() + b.ExpiresAt = timestamppb.New(time.Unix(1799999999, 0)) + ba, err := helpers.CanonicalOfferBytes(a) + if err != nil { + t.Fatal(err) + } + bb, err := helpers.CanonicalOfferBytes(b) + if err != nil { + t.Fatal(err) + } + if bytes.Equal(ba, bb) { + t.Error("offers differing only in expires_at must produce different CanonicalOfferBytes") + } +} + +func TestCanonicalOfferBytes_nil(t *testing.T) { + if _, err := helpers.CanonicalOfferBytes(nil); err == nil { + t.Error("nil offer should error") + } +} diff --git a/sdk/parity/symbol-map.json b/sdk/parity/symbol-map.json index a19c0c6..7eba860 100644 --- a/sdk/parity/symbol-map.json +++ b/sdk/parity/symbol-map.json @@ -48,6 +48,7 @@ "helpers.AlgEd25519": "RFC 9421 alg tag constant; inlined per language.", "helpers.AllSignaturesFromContext": "Go context.Context accessor; py/ts thread multisig state explicitly.", "helpers.BrokerKeyIDPrefix": "Relay keyID wire-prefix constant; inlined per language.", + "helpers.CanonicalOfferBytes": "Go-only canonical-offer-bytes accessor (the verbatim JCS-over-proto-JSON bytes an Offer signature covers, for callers persisting re-verifiable offer evidence); py/ts route offer signing/verification through SignOffer / the Verifier face and never need the raw signed bytes.", "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.", diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index 457d64e..598e763 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -32,6 +32,15 @@ TS signed-URL verify (`Ed25519Verifier`), and cross-language `ErrorDetail` reade in Python; `parseErrorDetail` / `errorDetailFrom` in TS). The SSRF-guarded transport is a single env-driven client governed by two flags (`SKIP_SSRF`, `ALLOW_INSECURE`). +**Go SDK: `helpers.CanonicalOfferBytes` (additive, Go-only, no wire change).** +Returns the exact canonical bytes an `Offer` signature is computed over — RFC 8785 +JCS over canonical proto-JSON with `signature`/`signature_algorithm` cleared and +`expires_at` included — byte-identical to what `SignOffer` signs and `VerifyOffer` +verifies. It exposes the canonicalization the signer and verifier already share, for +callers that persist the signed offer as verbatim, independently re-verifiable +evidence. Go-only: Python/TS route offer signing and verification through the +`signOffer`/`sign_offer` and verifier faces and never need the raw signed bytes. + **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written billing label on `Requester` is gone; the field is deleted outright with no `reserved` statement — pre-v1 the number returns to the free pool, and From ef01de648ce69e891edec681c1329be248c3b9b1 Mon Sep 17 00:00:00 2001 From: legendko Date: Thu, 23 Jul 2026 09:58:05 +0200 Subject: [PATCH 02/21] fix(sdk): map CanonicalOfferBytes to its Python/TS faces in the parity matrix CanonicalOfferBytes was registered as a Go-only exclusion, but Python (canonical_offer_payload) and TypeScript (canonicalOfferPayload) already export the exact equivalent as curated public symbols. Reclassify it as a mapped symbol and regenerate the matrix (72->73 parity, 99->98 exclusions), so the generated parity doc no longer asserts a cross-language face is absent when it exists. Alongside: - refresh the two "Go oracle" byte-identity comments in the Python/TS cores to the exported name (canonicalOfferPayload -> CanonicalOfferBytes) - record the export in proto/CHANGELOG.md and its website mirror, dropping the inaccurate "Go-only" framing and a non-existent sign_offer reference - note in the godoc that re-verification also needs the stored Offer.signature and signer key; the canonical bytes are the signed message, not sufficient alone - assert CanonicalOfferBytes does not mutate the caller's offer (clears on a clone) --- docs/sdk-parity-matrix.md | 4 ++-- proto/CHANGELOG.md | 9 +++++++++ sdk/go/helpers/offer.go | 3 +++ sdk/go/helpers/offer_test.go | 4 ++++ sdk/parity/symbol-map.json | 6 +++++- sdk/python/ramp_sdk/core.py | 2 +- sdk/ts/core/verifier.ts | 2 +- website/src/content/docs/reference/changelog.mdx | 16 ++++++++-------- 8 files changed, 33 insertions(+), 13 deletions(-) diff --git a/docs/sdk-parity-matrix.md b/docs/sdk-parity-matrix.md index 97fc235..7563420 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:** 72 symbols at cross-language parity · 14 documented divergences · 99 Go-idiomatic exclusions · 23 conformance corpora, each tri-replayed. +**At a glance:** 73 symbols at cross-language parity · 14 documented divergences · 98 Go-idiomatic exclusions · 23 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). @@ -27,6 +27,7 @@ Legend: a name = the public face in that language · `—` = intentionally none | `AcceptanceSignatureAlgorithm` | `ACCEPTANCE_SIGNATURE_ALGORITHM` | `ACCEPTANCE_SIGNATURE_ALGORITHM` | | `AppendSignature` | `append_signature` | `appendSignature` | | `ApplyScopes` | `apply_scopes` | `applyScopes` | +| `CanonicalOfferBytes` | `canonical_offer_payload` | `canonicalOfferPayload` | | `CanonicalizeMoney` | `canonicalize_money` | `canonicalizeMoney` | | `CatalogRejectionDetail` | `catalog_rejection_detail` | `catalogRejectionDetail` | | `ConnectProtocolVersion` | `ConnectProtocolVersion` | `ConnectProtocolVersion` | @@ -198,7 +199,6 @@ Go constructs (functional-option builders, `errors.Is` sentinels, value types, c | `helpers.AlgEd25519` | RFC 9421 alg tag constant; inlined per language. | | `helpers.AllSignaturesFromContext` | Go context.Context accessor; py/ts thread multisig state explicitly. | | `helpers.BrokerKeyIDPrefix` | Relay keyID wire-prefix constant; inlined per language. | -| `helpers.CanonicalOfferBytes` | Go-only canonical-offer-bytes accessor (the verbatim JCS-over-proto-JSON bytes an Offer signature covers, for callers persisting re-verifiable offer evidence); py/ts route offer signing/verification through SignOffer / the Verifier face and never need the raw signed bytes. | | `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. | diff --git a/proto/CHANGELOG.md b/proto/CHANGELOG.md index 31dd870..5f55e02 100644 --- a/proto/CHANGELOG.md +++ b/proto/CHANGELOG.md @@ -49,6 +49,15 @@ oracle. The SSRF-guarded transport is now a single env-driven client two flags (`SKIP_SSRF`, `ALLOW_INSECURE`). See `docs/sdk-parity-matrix.md` for the per-language surface. +**Go SDK: `helpers.CanonicalOfferBytes` exported (additive, no wire change).** +The offer-canonical-bytes accessor — RFC 8785 JCS over canonical proto-JSON with +`signature`/`signature_algorithm` cleared, `expires_at` included, byte-identical to +what `SignOffer` signs and `VerifyOffer` verifies — is now a public Go symbol. It +exposes the single canonicalization the signer and verifier already share, so a +caller can persist the signed offer as verbatim, independently re-verifiable +evidence. Python (`canonical_offer_payload`) and TS (`canonicalOfferPayload`) already +expose the equivalent public accessor; this brings the Go surface to parity. + **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written billing label on `Requester` is gone; the field is deleted outright with no `reserved` statement — pre-v1 the number returns to the free pool, and diff --git a/sdk/go/helpers/offer.go b/sdk/go/helpers/offer.go index fa97b52..e9350d5 100644 --- a/sdk/go/helpers/offer.go +++ b/sdk/go/helpers/offer.go @@ -69,6 +69,9 @@ func VerifyOffer(offer *rampv1.Offer, signatureHex string, pub ed25519.PublicKey // ramp.proto Offer.signature. The returned bytes are byte-identical to what // SignOffer signs and VerifyOffer verifies over — persist them to re-verify an // Offer signature verbatim, independent of how the Offer message later evolves. +// Re-verification also needs the persisted Offer.signature and the signer's trusted +// public key: these bytes are the signed message, necessary but not by themselves +// sufficient. // // The canonical form is RFC 8785 JCS over canonical proto-JSON — // JCS(protojson(offer with sig cleared)) — via canonicalSignPayload, so any diff --git a/sdk/go/helpers/offer_test.go b/sdk/go/helpers/offer_test.go index 036687e..a1c7420 100644 --- a/sdk/go/helpers/offer_test.go +++ b/sdk/go/helpers/offer_test.go @@ -142,6 +142,10 @@ func TestCanonicalOfferBytes_ignoresSignatureFields(t *testing.T) { if !bytes.Equal(before, after) { t.Error("CanonicalOfferBytes must clear signature/signature_algorithm; bytes differ") } + // The clear happens on a clone: the caller's offer keeps the fields it set. + if offer.Signature != "deadbeef" || offer.SignatureAlgorithm != helpers.OfferSignatureAlgorithm { + t.Error("CanonicalOfferBytes must not mutate the caller's offer (clears on a clone)") + } } func TestCanonicalOfferBytes_coversExpiresAt(t *testing.T) { diff --git a/sdk/parity/symbol-map.json b/sdk/parity/symbol-map.json index 7eba860..7f2849b 100644 --- a/sdk/parity/symbol-map.json +++ b/sdk/parity/symbol-map.json @@ -48,7 +48,6 @@ "helpers.AlgEd25519": "RFC 9421 alg tag constant; inlined per language.", "helpers.AllSignaturesFromContext": "Go context.Context accessor; py/ts thread multisig state explicitly.", "helpers.BrokerKeyIDPrefix": "Relay keyID wire-prefix constant; inlined per language.", - "helpers.CanonicalOfferBytes": "Go-only canonical-offer-bytes accessor (the verbatim JCS-over-proto-JSON bytes an Offer signature covers, for callers persisting re-verifiable offer evidence); py/ts route offer signing/verification through SignOffer / the Verifier face and never need the raw signed bytes.", "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.", @@ -211,6 +210,11 @@ "python": "apply_scopes", "ts": "applyScopes" }, + "helpers.CanonicalOfferBytes": { + "allowlist_reason": null, + "python": "canonical_offer_payload", + "ts": "canonicalOfferPayload" + }, "helpers.CanonicalizeMoney": { "allowlist_reason": null, "python": "canonicalize_money", diff --git a/sdk/python/ramp_sdk/core.py b/sdk/python/ramp_sdk/core.py index 79ba58c..07255ad 100644 --- a/sdk/python/ramp_sdk/core.py +++ b/sdk/python/ramp_sdk/core.py @@ -147,7 +147,7 @@ def canonical_offer_payload(offer: dict[str, Any]) -> bytes: """Reproduce the offer's signed bytes: clear signature + signature_algorithm from the canonical proto-JSON, then apply RFC 8785 JCS. - MUST stay byte-identical to the Go oracle (helpers.canonicalOfferPayload). The + MUST stay byte-identical to the Go oracle (helpers.CanonicalOfferBytes). The offer is already canonical proto-JSON (snake_case, enums-as-names, omit-unpopulated) — the core only clears the two signature keys and re-JCS-es. """ diff --git a/sdk/ts/core/verifier.ts b/sdk/ts/core/verifier.ts index 0da91f0..9da9268 100644 --- a/sdk/ts/core/verifier.ts +++ b/sdk/ts/core/verifier.ts @@ -144,7 +144,7 @@ function hexToBytes(hex: string): Uint8Array | undefined { /** * canonicalOfferPayload reproduces the signed bytes: clear signature + * signature_algorithm from the offer's canonical proto-JSON, then apply RFC 8785 - * JCS. MUST stay byte-identical to the Go oracle (helpers.canonicalOfferPayload). + * JCS. MUST stay byte-identical to the Go oracle (helpers.CanonicalOfferBytes). * The offer is already canonical proto-JSON (snake_case, enums-as-names, * omit-unpopulated) — the core only clears the two signature fields and re-JCS-es. */ diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index 598e763..3acb4a1 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -32,14 +32,14 @@ TS signed-URL verify (`Ed25519Verifier`), and cross-language `ErrorDetail` reade in Python; `parseErrorDetail` / `errorDetailFrom` in TS). The SSRF-guarded transport is a single env-driven client governed by two flags (`SKIP_SSRF`, `ALLOW_INSECURE`). -**Go SDK: `helpers.CanonicalOfferBytes` (additive, Go-only, no wire change).** -Returns the exact canonical bytes an `Offer` signature is computed over — RFC 8785 -JCS over canonical proto-JSON with `signature`/`signature_algorithm` cleared and -`expires_at` included — byte-identical to what `SignOffer` signs and `VerifyOffer` -verifies. It exposes the canonicalization the signer and verifier already share, for -callers that persist the signed offer as verbatim, independently re-verifiable -evidence. Go-only: Python/TS route offer signing and verification through the -`signOffer`/`sign_offer` and verifier faces and never need the raw signed bytes. +**Go SDK: `helpers.CanonicalOfferBytes` exported (additive, no wire change).** +The offer-canonical-bytes accessor — RFC 8785 JCS over canonical proto-JSON with +`signature`/`signature_algorithm` cleared, `expires_at` included, byte-identical to +what `SignOffer` signs and `VerifyOffer` verifies — is now a public Go symbol. It +exposes the single canonicalization the signer and verifier already share, so a +caller can persist the signed offer as verbatim, independently re-verifiable +evidence. Python (`canonical_offer_payload`) and TS (`canonicalOfferPayload`) already +expose the equivalent public accessor; this brings the Go surface to parity. **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written billing label on `Requester` is gone; the field is deleted outright with no From 874ab83c7d0e0382be0e2ca8e7f499965a16f66c Mon Sep 17 00:00:00 2001 From: legendko Date: Thu, 23 Jul 2026 14:36:24 +0200 Subject: [PATCH 03/21] feat(sdk-go): export CanonicalAcceptanceBytes Expose the previously-unexported acceptance canonicalization as CanonicalAcceptanceBytes: RFC 8785 JCS over canonical proto-JSON of AgentAcceptancePayload{offer_sig, requester_id, requester_domain, idempotency_key} -- the exact bytes an agent's offer-acceptance signature is computed over. A caller persisting an acceptance can now store and re-verify those bytes verbatim instead of re-deriving them at verification time, which would pin an already-signed acceptance to whatever canonicalization the SDK implements later; that form has already changed once, from deterministic protobuf to JCS over proto-JSON. SignOfferAcceptance, VerifyOfferAcceptance and the golden-vector emitter route through it, so there is one canonicalization path and behavior is unchanged (acceptance vectors byte-identical). Python (jcs_acceptance_payload) and TypeScript (acceptancePayload) already export the equivalent public accessor, so the symbol is registered in symbol-map.json as a mapped symbol -- not a Go-only exclusion -- and the parity matrix regenerated (73->74 parity). No proto or wire change. --- docs/sdk-parity-matrix.md | 3 +- proto/CHANGELOG.md | 12 ++ sdk/go/helpers/acceptance.go | 36 ++++-- sdk/go/helpers/acceptance_test.go | 118 ++++++++++++++++++ sdk/go/helpers/gen_vectors_test.go | 2 +- sdk/parity/symbol-map.json | 5 + sdk/python/ramp_sdk/core.py | 2 +- sdk/ts/src/acceptance.ts | 2 +- .../src/content/docs/reference/changelog.mdx | 12 ++ 9 files changed, 176 insertions(+), 16 deletions(-) diff --git a/docs/sdk-parity-matrix.md b/docs/sdk-parity-matrix.md index 7563420..787608d 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:** 73 symbols at cross-language parity · 14 documented divergences · 98 Go-idiomatic exclusions · 23 conformance corpora, each tri-replayed. +**At a glance:** 74 symbols at cross-language parity · 14 documented divergences · 98 Go-idiomatic exclusions · 23 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). @@ -27,6 +27,7 @@ Legend: a name = the public face in that language · `—` = intentionally none | `AcceptanceSignatureAlgorithm` | `ACCEPTANCE_SIGNATURE_ALGORITHM` | `ACCEPTANCE_SIGNATURE_ALGORITHM` | | `AppendSignature` | `append_signature` | `appendSignature` | | `ApplyScopes` | `apply_scopes` | `applyScopes` | +| `CanonicalAcceptanceBytes` | `jcs_acceptance_payload` | `acceptancePayload` | | `CanonicalOfferBytes` | `canonical_offer_payload` | `canonicalOfferPayload` | | `CanonicalizeMoney` | `canonicalize_money` | `canonicalizeMoney` | | `CatalogRejectionDetail` | `catalog_rejection_detail` | `catalogRejectionDetail` | diff --git a/proto/CHANGELOG.md b/proto/CHANGELOG.md index 5f55e02..3455984 100644 --- a/proto/CHANGELOG.md +++ b/proto/CHANGELOG.md @@ -58,6 +58,18 @@ caller can persist the signed offer as verbatim, independently re-verifiable evidence. Python (`canonical_offer_payload`) and TS (`canonicalOfferPayload`) already expose the equivalent public accessor; this brings the Go surface to parity. +**Go SDK: `helpers.CanonicalAcceptanceBytes` exported (additive, no wire change).** +The acceptance-canonical-bytes accessor — RFC 8785 JCS over canonical proto-JSON of +`AgentAcceptancePayload{offer_sig, requester_id, requester_domain, idempotency_key}`, +byte-identical to what `SignOfferAcceptance` signs and `VerifyOfferAcceptance` +verifies — is now a public Go symbol, completing the pair with +`CanonicalOfferBytes`. A caller can persist an agent's acceptance as verbatim, +independently re-verifiable evidence rather than re-deriving the bytes at +verification time, which would pin an already-signed acceptance to whatever +canonicalization the SDK implements later. Python (`jcs_acceptance_payload`) and TS +(`acceptancePayload`) already expose the equivalent public accessor; this brings the +Go surface to parity. + **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written billing label on `Requester` is gone; the field is deleted outright with no `reserved` statement — pre-v1 the number returns to the free pool, and diff --git a/sdk/go/helpers/acceptance.go b/sdk/go/helpers/acceptance.go index 2dd5727..5b4a3be 100644 --- a/sdk/go/helpers/acceptance.go +++ b/sdk/go/helpers/acceptance.go @@ -19,7 +19,7 @@ import ( // // The signed bytes are pinned by the AgentAcceptancePayload proto message so the // signer (agent) and the verifier (Exchange) derive them identically; both go -// through canonicalAcceptancePayload, the single source of the byte layout. +// through CanonicalAcceptanceBytes, the single source of the byte layout. // AcceptanceSignatureAlgorithm is the alg advertised on AgentAcceptance. // Always EdDSA for Ed25519. @@ -30,18 +30,30 @@ const AcceptanceSignatureAlgorithm = "EdDSA" // idempotency key). var ErrAcceptanceSignatureInvalid = errors.New("helpers: offer-acceptance signature invalid") -// canonicalAcceptancePayload is the canonical byte sequence an agent's offer -// acceptance covers: the accepted Offer.signature (which transitively binds the -// offer's pricing, terms, expiry, and issuing Exchange), plus the requester -// identity and the transaction's idempotency key. The offer must be signed — an -// empty anchor would let the acceptance float free of any concrete offer, so it -// is rejected fail-closed. +// CanonicalAcceptanceBytes returns the exact canonical byte sequence an agent's +// offer acceptance covers: the accepted Offer.signature (which transitively binds +// the offer's pricing, terms, expiry, and issuing Exchange), plus the requester +// identity and the transaction's idempotency key. The returned bytes are +// byte-identical to what SignOfferAcceptance signs and VerifyOfferAcceptance +// verifies over — persist them to re-verify an acceptance verbatim, independent of +// how the canonical form later evolves. Re-verification also needs the persisted +// AgentAcceptance.signature and the signer's trusted public key: these bytes are the +// signed message, necessary but not by themselves sufficient. // // The canonical form is RFC 8785 JCS over canonical proto-JSON — // JCS(protojson(AgentAcceptancePayload)) — via canonicalSignPayload, the same -// primitive the offer signature uses, so any language reproduces the exact signed -// bytes without a protobuf binary codec. See canonicalsign.go for the option set. -func canonicalAcceptancePayload(offer *rampv1.Offer, requester *rampv1.Requester, idempotencyKey string) ([]byte, error) { +// primitive the offer signature uses, so any language (Go/TS/Python) reproduces the +// exact signed bytes without a protobuf binary codec. See canonicalsign.go for the +// pinned proto-JSON option set. +// +// Unpopulated fields are OMITTED before JCS, so an empty requester domain is absent +// from the object entirely rather than emitted as "": the bytes for an empty domain +// are not the bytes for any populated one. +// +// Fails closed on a nil offer, a nil requester, or an unsigned offer (empty +// Offer.signature) — an empty anchor would let the acceptance float free of any +// concrete offer. +func CanonicalAcceptanceBytes(offer *rampv1.Offer, requester *rampv1.Requester, idempotencyKey string) ([]byte, error) { if offer == nil { return nil, errors.New("helpers: offer is nil") } @@ -68,7 +80,7 @@ func SignOfferAcceptance(priv ed25519.PrivateKey, offer *rampv1.Offer, requester if len(priv) != ed25519.PrivateKeySize { return "", fmt.Errorf("helpers: ed25519 private key must be %d bytes, got %d", ed25519.PrivateKeySize, len(priv)) } - payload, err := canonicalAcceptancePayload(offer, requester, idempotencyKey) + payload, err := CanonicalAcceptanceBytes(offer, requester, idempotencyKey) if err != nil { return "", err } @@ -86,7 +98,7 @@ func VerifyOfferAcceptance(offer *rampv1.Offer, requester *rampv1.Requester, ide if err != nil { return fmt.Errorf("helpers: decode acceptance signature: %w", err) } - payload, err := canonicalAcceptancePayload(offer, requester, idempotencyKey) + payload, err := CanonicalAcceptanceBytes(offer, requester, idempotencyKey) if err != nil { return err } diff --git a/sdk/go/helpers/acceptance_test.go b/sdk/go/helpers/acceptance_test.go index 67319c4..f7266c5 100644 --- a/sdk/go/helpers/acceptance_test.go +++ b/sdk/go/helpers/acceptance_test.go @@ -1,8 +1,13 @@ package helpers_test import ( + "bytes" "crypto/ed25519" + "encoding/hex" + "encoding/json" "errors" + "os" + "path/filepath" "testing" rampv1 "github.com/RAMP-Protocol/protocol/gen/go/ramp/v1" @@ -114,3 +119,116 @@ func TestSignOfferAcceptance_emptyOfferSignatureRejected(t *testing.T) { t.Fatal("expected error signing acceptance over an unsigned offer (empty anchor)") } } + +func TestCanonicalAcceptanceBytes_matchesSignOfferAcceptance(t *testing.T) { + // The bytes CanonicalAcceptanceBytes returns are exactly what + // SignOfferAcceptance signed: verifying that signature directly over them must + // hold. This is the property a caller relies on to persist verbatim, + // re-verifiable acceptance evidence. + pub, priv, _ := ed25519.GenerateKey(nil) + offer, requester, idem := acceptanceFixture() + sigHex, err := helpers.SignOfferAcceptance(priv, offer, requester, idem) + if err != nil { + t.Fatal(err) + } + canon, err := helpers.CanonicalAcceptanceBytes(offer, requester, idem) + if err != nil { + t.Fatal(err) + } + sig, err := hex.DecodeString(sigHex) + if err != nil { + t.Fatal(err) + } + if !ed25519.Verify(pub, canon, sig) { + t.Error("SignOfferAcceptance signature must verify over CanonicalAcceptanceBytes") + } +} + +// acceptanceCorpus is the cross-language acceptance fixture: the Go-emitted +// golden the TS and Python acceptance faces replay byte-for-byte. Asserting the +// exported accessor against canonical_jcs pins Go/TS/Python to one byte layout +// through the corpus that already gates the other two. +const acceptanceCorpus = "testdata/acceptance-vectors.json" + +type acceptanceCorpusFile struct { + Canonicalization string `json:"canonicalization"` + Vectors []struct { + Name string `json:"name"` + OfferSig string `json:"offer_sig"` + RequesterID string `json:"requester_id"` + RequesterDomain string `json:"requester_domain"` + IdempotencyKey string `json:"idempotency_key"` + CanonicalJCS string `json:"canonical_jcs"` + } `json:"vectors"` +} + +func TestCanonicalAcceptanceBytes_matchesCorpus(t *testing.T) { + t.Parallel() + raw, err := os.ReadFile(filepath.Clean(acceptanceCorpus)) + if err != nil { + t.Fatalf("read acceptance corpus: %v", err) + } + var corpus acceptanceCorpusFile + if err := json.Unmarshal(raw, &corpus); err != nil { + t.Fatalf("unmarshal acceptance corpus: %v", err) + } + if corpus.Canonicalization != "jcs" { + t.Fatalf("acceptance corpus canonicalization = %q, want jcs", corpus.Canonicalization) + } + if len(corpus.Vectors) == 0 { + t.Fatal("acceptance corpus has no vectors") + } + for _, v := range corpus.Vectors { + t.Run(v.Name, func(t *testing.T) { + t.Parallel() + offer := &rampv1.Offer{Signature: v.OfferSig} + requester := &rampv1.Requester{Id: v.RequesterID, Domain: v.RequesterDomain} + got, err := helpers.CanonicalAcceptanceBytes(offer, requester, v.IdempotencyKey) + if err != nil { + t.Fatalf("CanonicalAcceptanceBytes: %v", err) + } + if string(got) != v.CanonicalJCS { + t.Errorf("canonical bytes\n got %s\n want %s", got, v.CanonicalJCS) + } + }) + } +} + +func TestCanonicalAcceptanceBytes_failsClosed(t *testing.T) { + offer, requester, idem := acceptanceFixture() + if _, err := helpers.CanonicalAcceptanceBytes(nil, requester, idem); err == nil { + t.Error("nil offer should error") + } + if _, err := helpers.CanonicalAcceptanceBytes(offer, nil, idem); err == nil { + t.Error("nil requester should error") + } + // An unsigned offer is an empty anchor: the acceptance would float free of any + // concrete offer, so the bytes are refused rather than produced. + unsigned := &rampv1.Offer{OfferId: "of_1"} + if _, err := helpers.CanonicalAcceptanceBytes(unsigned, requester, idem); err == nil { + t.Error("unsigned offer (empty offer signature) should error") + } +} + +func TestCanonicalAcceptanceBytes_doesNotMutateInputs(t *testing.T) { + // The payload is built from getters onto a fresh message, so a caller can hand + // in the live Offer/Requester it is about to persist and get them back untouched. + offer, requester, idem := acceptanceFixture() + before, err := helpers.CanonicalAcceptanceBytes(offer, requester, idem) + if err != nil { + t.Fatal(err) + } + if offer.GetSignature() != "ex-offer-sig-hex" || offer.GetOfferId() != "of_1" { + t.Error("CanonicalAcceptanceBytes must not mutate the caller's offer") + } + if requester.GetId() != "agent-1" || requester.GetDomain() != "agent.example.com" { + t.Error("CanonicalAcceptanceBytes must not mutate the caller's requester") + } + after, err := helpers.CanonicalAcceptanceBytes(offer, requester, idem) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(before, after) { + t.Error("CanonicalAcceptanceBytes must be deterministic for identical inputs") + } +} diff --git a/sdk/go/helpers/gen_vectors_test.go b/sdk/go/helpers/gen_vectors_test.go index 2ca8d8b..d15e794 100644 --- a/sdk/go/helpers/gen_vectors_test.go +++ b/sdk/go/helpers/gen_vectors_test.go @@ -1023,7 +1023,7 @@ func buildAcceptanceVectors(t *testing.T) []acceptanceVector { for _, s := range specs { offer := &rampv1.Offer{Signature: s.offerSig} requester := &rampv1.Requester{Id: s.requesterID, Domain: s.requesterDomain} - canon, err := canonicalAcceptancePayload(offer, requester, s.idempotencyKey) + canon, err := CanonicalAcceptanceBytes(offer, requester, s.idempotencyKey) if err != nil { t.Fatalf("%s: canonical: %v", s.name, err) } diff --git a/sdk/parity/symbol-map.json b/sdk/parity/symbol-map.json index 7f2849b..b07443d 100644 --- a/sdk/parity/symbol-map.json +++ b/sdk/parity/symbol-map.json @@ -210,6 +210,11 @@ "python": "apply_scopes", "ts": "applyScopes" }, + "helpers.CanonicalAcceptanceBytes": { + "allowlist_reason": null, + "python": "jcs_acceptance_payload", + "ts": "acceptancePayload" + }, "helpers.CanonicalOfferBytes": { "allowlist_reason": null, "python": "canonical_offer_payload", diff --git a/sdk/python/ramp_sdk/core.py b/sdk/python/ramp_sdk/core.py index 07255ad..ac6acdd 100644 --- a/sdk/python/ramp_sdk/core.py +++ b/sdk/python/ramp_sdk/core.py @@ -274,7 +274,7 @@ def jcs_acceptance_payload( The same JCS(protojson(...)) canonicalization the offer signature uses. proto-JSON OMITS unpopulated fields, so an empty ``requester_domain`` is absent from the object before JCS (matching the Go oracle's empty-domain vector). Fail-closed on - an empty ``offer_sig`` (mirror Go canonicalAcceptancePayload): an empty anchor + an empty ``offer_sig`` (mirror Go CanonicalAcceptanceBytes): an empty anchor would let the acceptance float free of any concrete offer. """ if offer_sig == "": diff --git a/sdk/ts/src/acceptance.ts b/sdk/ts/src/acceptance.ts index 7c133dc..ef45fcb 100644 --- a/sdk/ts/src/acceptance.ts +++ b/sdk/ts/src/acceptance.ts @@ -54,7 +54,7 @@ function bytesToHex(bytes: Uint8Array): string { * acceptancePayload reproduces the canonical signed bytes: * JCS(protojson(AgentAcceptancePayload)) with an empty requester_domain omitted. * Throws on an empty offer signature (fail-closed, mirror Go - * canonicalAcceptancePayload / Python jcs_acceptance_payload). + * CanonicalAcceptanceBytes / Python jcs_acceptance_payload). */ export function acceptancePayload(input: AcceptanceInput): Uint8Array { if (input.offerSig === "") { diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index 3acb4a1..8343a06 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -41,6 +41,18 @@ caller can persist the signed offer as verbatim, independently re-verifiable evidence. Python (`canonical_offer_payload`) and TS (`canonicalOfferPayload`) already expose the equivalent public accessor; this brings the Go surface to parity. +**Go SDK: `helpers.CanonicalAcceptanceBytes` exported (additive, no wire change).** +The acceptance-canonical-bytes accessor — RFC 8785 JCS over canonical proto-JSON of +`AgentAcceptancePayload{offer_sig, requester_id, requester_domain, idempotency_key}`, +byte-identical to what `SignOfferAcceptance` signs and `VerifyOfferAcceptance` +verifies — is now a public Go symbol, completing the pair with +`CanonicalOfferBytes`. A caller can persist an agent's acceptance as verbatim, +independently re-verifiable evidence rather than re-deriving the bytes at +verification time, which would pin an already-signed acceptance to whatever +canonicalization the SDK implements later. Python (`jcs_acceptance_payload`) and TS +(`acceptancePayload`) already expose the equivalent public accessor; this brings the +Go surface to parity. + **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written billing label on `Requester` is gone; the field is deleted outright with no `reserved` statement — pre-v1 the number returns to the free pool, and From d4c509e8a34d815d921f86d8cd2b6525174e5b30 Mon Sep 17 00:00:00 2001 From: legendko Date: Thu, 23 Jul 2026 14:38:44 +0200 Subject: [PATCH 04/21] docs(proto): correct the acceptance canonical-form text to JCS over proto-JSON AgentAcceptance and AgentAcceptancePayload still described the RETIRED signing form -- "the deterministic protobuf serialization of AgentAcceptancePayload", "derive BYTE-IDENTICAL signed bytes ... via proto.Marshal(Deterministic: true)" -- while the canonical-signing block on Offer.signature in the same file already states that RFC 8785 JCS over canonical proto-JSON "applies to the agent offer-acceptance signature". The normative spec asserted both, and an implementation that followed the acceptance text would have produced non-verifying signatures. Point the acceptance text at that single definition instead of restating a superseded recipe: AgentAcceptancePayload fixes the FIELD SET, Offer.signature fixes the BYTE LAYOUT. Four sites, comments only -- no field, message, or wire change; gen/ regenerated (buf 1.66.1) and the hand-maintained website mirror updated, both of which carried the same stale claim. Also record the reversal in design-history: why deterministic protobuf binary was rejected as a cross-language signing primitive (protobuf disclaims byte stability across languages and versions, and it forces every verifier to link a binary codec), and why the canonical bytes are consequently a public accessor in all three SDKs -- a form that changed once can change again, so stored evidence must re-verify against the bytes as signed, not as later re-derived. --- docs/design-history.md | 35 ++++++++++++++++++ gen/descriptor.binpb | Bin 539291 -> 539577 bytes gen/go/ramp/v1/ramp.pb.go | 21 ++++++----- gen/python/wire/models.py | 4 +- gen/ts/wire/schemas.ts | 6 +-- proto/CHANGELOG.md | 12 ++++++ proto/ramp/v1/ramp.proto | 21 ++++++----- .../src/content/docs/reference/changelog.mdx | 12 ++++++ .../src/content/docs/reference/proto-ramp.mdx | 4 +- 9 files changed, 90 insertions(+), 25 deletions(-) diff --git a/docs/design-history.md b/docs/design-history.md index 37c0b98..a35268e 100644 --- a/docs/design-history.md +++ b/docs/design-history.md @@ -37,6 +37,41 @@ Responses do not retrace the chain: the terminal Exchange returns directly to th originating agent, because the signed `retrieval_endpoint` is already bound to the 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 outlive an HTTP exchange — `Offer.signature` and +the agent's `AgentAcceptance.signature` — 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 +fields omitted). + +Deterministic protobuf marshaling is not actually canonical. Protobuf's own +documentation disclaims byte stability across languages, across library versions, +and in the presence of unknown fields — it is a best-effort local property, not a +wire-format guarantee. That makes it unusable as the basis of a signature two +independent implementations must agree on, which is precisely the RAMP case: an +agent SDK signs, a broker relays, an Exchange verifies, and none of them need share +a language. It also forces every verifier — including an edge worker or a browser +client — to link a protobuf binary codec just to check a signature. JCS over +proto-JSON has neither problem: it is a published canonicalization of JSON, and any +language can reproduce the signed bytes from the JSON rendering alone. The +Go-emitted golden vector is the arbiter, and the TS and Python SDKs replay it +byte-for-byte. + +The reversal 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 canonical form +that changed once can change again, and a verifier that 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` 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. + ## "Marketplace" → "Exchange" The selling intermediary was originally called a *marketplace*. We renamed it to diff --git a/gen/descriptor.binpb b/gen/descriptor.binpb index e3c9ee67bb462832a19029284dca65ab2e5798e0..73b70a07dd878a1cb979446d2a57d0da54bfdac5 100644 GIT binary patch delta 23697 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 GcK%;{P5Z3? delta 23367 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±)6R=YW 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 diff --git a/gen/go/ramp/v1/ramp.pb.go b/gen/go/ramp/v1/ramp.pb.go index f5ad886..c9c9178 100644 --- a/gen/go/ramp/v1/ramp.pb.go +++ b/gen/go/ramp/v1/ramp.pb.go @@ -4364,13 +4364,14 @@ func (x *Delegation) GetExtCritical() []string { // agent's key (RFC 7638 thumbprint of the acceptance key). // // `signature` is a hex-encoded detached Ed25519 signature (NOT a JWS) over the -// deterministic protobuf serialization of `AgentAcceptancePayload` — the same -// hex/Ed25519 convention the SDK uses for Offer.signature. `signature_algorithm` -// is "EdDSA". +// CANONICAL SIGNING form of `AgentAcceptancePayload` — RFC 8785 JCS over canonical +// proto-JSON, defined once on Offer.signature above and applying verbatim here — +// the same hex/Ed25519 convention the SDK uses for Offer.signature. +// `signature_algorithm` is "EdDSA". type AgentAcceptance struct { state protoimpl.MessageState `protogen:"open.v1"` - // Hex-encoded detached Ed25519 signature over the deterministic-marshaled - // AgentAcceptancePayload bytes. + // Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload + // bytes (see the canonical-signing definition on Offer.signature). Signature string `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` // Signature algorithm; "EdDSA" for Ed25519. SignatureAlgorithm string `protobuf:"bytes,2,opt,name=signature_algorithm,json=signatureAlgorithm,proto3" json:"signature_algorithm,omitempty"` @@ -4425,8 +4426,10 @@ func (x *AgentAcceptance) GetSignatureAlgorithm() string { // 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 -// schema via `proto.Marshal(Deterministic: true)`. Underspecifying these bytes -// is the top cross-implementation drift risk, so the field set is fixed here. +// schema. This message fixes the FIELD SET; the byte layout is the canonical +// signing form defined on Offer.signature — RFC 8785 JCS over canonical +// proto-JSON with the pinned option set. Underspecifying either half is the top +// cross-implementation drift risk, so both are pinned normatively. // // Field provenance when building the payload for an execute request: // - offer_sig = the accepted Offer.signature (the Exchange's hex @@ -4632,8 +4635,8 @@ type TransactionItem struct { Offer *Offer `protobuf:"bytes,3,opt,name=offer,proto3" json:"offer,omitempty"` // The agent's detached acceptance signature over this item's `offer`. // Optional on the wire; the Exchange enforces presence per - // item at the service layer for relayed batches. Signed bytes = - // deterministic AgentAcceptancePayload, with requester_* and idempotency_key + // item at the service layer for relayed batches. Signed bytes = the canonical + // AgentAcceptancePayload form, with requester_* and idempotency_key // taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature. AgentAcceptance *AgentAcceptance `protobuf:"bytes,4,opt,name=agent_acceptance,json=agentAcceptance,proto3,oneof" json:"agent_acceptance,omitempty"` unknownFields protoimpl.UnknownFields diff --git a/gen/python/wire/models.py b/gen/python/wire/models.py index 86d41c9..b743b11 100644 --- a/gen/python/wire/models.py +++ b/gen/python/wire/models.py @@ -13,7 +13,7 @@ class AgentAcceptance(WireModel): signature: constr(min_length=1) = Field( ..., - description='Hex-encoded detached Ed25519 signature over the deterministic-marshaled\n AgentAcceptancePayload bytes.', + description='Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload\n bytes (see the canonical-signing definition on Offer.signature).', ) signature_algorithm: str | None = Field( '', description='Signature algorithm; "EdDSA" for Ed25519.' @@ -1740,7 +1740,7 @@ class ResourceResponse(WireModel): class TransactionItem(WireModel): agent_acceptance: AgentAcceptance | None = Field( None, - description="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 =\n deterministic AgentAcceptancePayload, with requester_* and idempotency_key\n taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.", + description="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.", ) offer: Offer = Field( ..., diff --git a/gen/ts/wire/schemas.ts b/gen/ts/wire/schemas.ts index 5c07348..c9253e6 100644 --- a/gen/ts/wire/schemas.ts +++ b/gen/ts/wire/schemas.ts @@ -8,7 +8,7 @@ import { wire } from "./base.ts"; export const AcceptableRestrictionSchema = wire(z.object({ "axis": z.union([z.string().regex(new RegExp("^RESTRICTION_KIND_UNSPECIFIED$")), z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("Which axis (same enum as Restriction.kind): FUNCTION / GEOGRAPHY /\n USER_TYPE / OTHER.").default(0), "values": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("The values the query operates within on this axis — same token vocabulary\n as the terms (e.g. FUNCTION [\"ai-train\"], GEOGRAPHY [\"US\", \"EU\"]).").optional() }).describe("AcceptableRestriction — the limits a query operates within on one restriction\n axis, expressed in the same RestrictionKind vocabulary that terms use. The\n Exchange/Broker MAY pre-select offers whose term restrictions fall within\n these as a convenience (see Restriction); it is NOT enforcement — the agent\n self-selects and bears compliance.")); -export const AgentAcceptanceSchema = wire(z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the deterministic-marshaled\n AgentAcceptancePayload bytes."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("`signature` is a hex-encoded detached Ed25519 signature (NOT a JWS) over the\n deterministic protobuf serialization of `AgentAcceptancePayload` — the same\n hex/Ed25519 convention the SDK uses for Offer.signature. `signature_algorithm`\n is \"EdDSA\".")); +export const AgentAcceptanceSchema = wire(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("`signature` is a hex-encoded detached Ed25519 signature (NOT a JWS) over the\n CANONICAL SIGNING form of `AgentAcceptancePayload` — RFC 8785 JCS over canonical\n proto-JSON, defined once on Offer.signature above and applying verbatim here —\n the same hex/Ed25519 convention the SDK uses for Offer.signature.\n `signature_algorithm` is \"EdDSA\".")); 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("Field 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.")); @@ -180,9 +180,9 @@ export const TermSemanticsSchema = wire(z.enum(["TERM_SEMANTICS_ENUMERATED","TER export const TransactionDenialSchema = wire(z.object({ "offer_id": z.string().describe("Batch mode: the offer this denial pertains to.").optional(), "reason": z.enum(["DENIAL_REASON_BILLING_REF_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"]).describe("The denial reason (defined-only, non-zero)"), "restriction_mismatches": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When reason = RESTRICTION_NOT_SATISFIED, the failed axes (same\n RestrictionKind vocabulary the terms use).").optional() }).describe("TransactionDenial — ExecuteTransaction could not complete. Carries the denial\n reason the response body no longer holds (denial_reason / restriction_mismatches\n move here in the response-shape normalization). Reuses the DenialReason vocab.")); -export const TransactionItemSchema = wire(z.object({ "agent_acceptance": z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the deterministic-marshaled\n AgentAcceptancePayload bytes."), "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 =\n deterministic AgentAcceptancePayload, 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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).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 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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).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 deterministic-marshaled\n AgentAcceptancePayload bytes."), "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 =\n deterministic AgentAcceptancePayload, 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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).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().describe("Domain the requester belongs to — used for public key lookup.\n Keys published at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT).").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(), "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("The 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("Protocol version").default("") }).describe("After 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({ "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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).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().describe("Domain the requester belongs to — used for public key lookup.\n Keys published at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT).").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(), "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("The 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("Protocol version").default("") }).describe("After 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_BILLING_REF_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"]).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("Analogous 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("Protocol version").default("") }).describe("Items-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 3455984..e9af66a 100644 --- a/proto/CHANGELOG.md +++ b/proto/CHANGELOG.md @@ -70,6 +70,18 @@ canonicalization the SDK implements later. Python (`jcs_acceptance_payload`) and (`acceptancePayload`) already expose the equivalent public accessor; this brings the Go surface to parity. +**Acceptance canonical-form text corrected (documentation only, no wire change).** +`AgentAcceptance` and `AgentAcceptancePayload` still described the RETIRED signing +form — "the deterministic protobuf serialization", "`proto.Marshal(Deterministic: +true)`" — contradicting the canonical-signing block on `Offer.signature` in the same +file, which already states that RFC 8785 JCS over canonical proto-JSON "applies to +the agent offer-acceptance signature". The acceptance text now points at that single +normative definition instead of restating a superseded recipe: +`AgentAcceptancePayload` fixes the field set, `Offer.signature` fixes the byte +layout. Implementations that followed the stale text would have produced +non-verifying signatures. No field, message, or wire change — comments only, with +`gen/` and the website mirror regenerated. + **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written billing label on `Requester` is gone; the field is deleted outright with no `reserved` statement — pre-v1 the number returns to the free pool, and diff --git a/proto/ramp/v1/ramp.proto b/proto/ramp/v1/ramp.proto index 43f6362..cb7c912 100644 --- a/proto/ramp/v1/ramp.proto +++ b/proto/ramp/v1/ramp.proto @@ -1651,12 +1651,13 @@ enum ResourceMutability { // agent's key (RFC 7638 thumbprint of the acceptance key). // // `signature` is a hex-encoded detached Ed25519 signature (NOT a JWS) over the -// deterministic protobuf serialization of `AgentAcceptancePayload` — the same -// hex/Ed25519 convention the SDK uses for Offer.signature. `signature_algorithm` -// is "EdDSA". +// CANONICAL SIGNING form of `AgentAcceptancePayload` — RFC 8785 JCS over canonical +// proto-JSON, defined once on Offer.signature above and applying verbatim here — +// the same hex/Ed25519 convention the SDK uses for Offer.signature. +// `signature_algorithm` is "EdDSA". message AgentAcceptance { - // Hex-encoded detached Ed25519 signature over the deterministic-marshaled - // AgentAcceptancePayload bytes. + // Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload + // bytes (see the canonical-signing definition on Offer.signature). string signature = 1 [(buf.validate.field).string.min_len = 1]; // Signature algorithm; "EdDSA" for Ed25519. @@ -1666,8 +1667,10 @@ message AgentAcceptance { // 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 -// schema via `proto.Marshal(Deterministic: true)`. Underspecifying these bytes -// is the top cross-implementation drift risk, so the field set is fixed here. +// schema. This message fixes the FIELD SET; the byte layout is the canonical +// signing form defined on Offer.signature — RFC 8785 JCS over canonical +// proto-JSON with the pinned option set. Underspecifying either half is the top +// cross-implementation drift risk, so both are pinned normatively. // // Field provenance when building the payload for an execute request: // - offer_sig = the accepted Offer.signature (the Exchange's hex @@ -1750,8 +1753,8 @@ message TransactionItem { // The agent's detached acceptance signature over this item's `offer`. // Optional on the wire; the Exchange enforces presence per - // item at the service layer for relayed batches. Signed bytes = - // deterministic AgentAcceptancePayload, with requester_* and idempotency_key + // item at the service layer for relayed batches. Signed bytes = the canonical + // AgentAcceptancePayload form, with requester_* and idempotency_key // taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature. optional AgentAcceptance agent_acceptance = 4; } diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index 8343a06..39f5946 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -53,6 +53,18 @@ canonicalization the SDK implements later. Python (`jcs_acceptance_payload`) and (`acceptancePayload`) already expose the equivalent public accessor; this brings the Go surface to parity. +**Acceptance canonical-form text corrected (documentation only, no wire change).** +`AgentAcceptance` and `AgentAcceptancePayload` still described the RETIRED signing +form — "the deterministic protobuf serialization", "`proto.Marshal(Deterministic: +true)`" — contradicting the canonical-signing block on `Offer.signature` in the same +file, which already states that RFC 8785 JCS over canonical proto-JSON "applies to +the agent offer-acceptance signature". The acceptance text now points at that single +normative definition instead of restating a superseded recipe: +`AgentAcceptancePayload` fixes the field set, `Offer.signature` fixes the byte +layout. Implementations that followed the stale text would have produced +non-verifying signatures. No field, message, or wire change — comments only, with +`gen/` and the website mirror regenerated. + **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written billing label on `Requester` is gone; the field is deleted outright with no `reserved` statement — pre-v1 the number returns to the free pool, and diff --git a/website/src/content/docs/reference/proto-ramp.mdx b/website/src/content/docs/reference/proto-ramp.mdx index d03dfe8..e388531 100644 --- a/website/src/content/docs/reference/proto-ramp.mdx +++ b/website/src/content/docs/reference/proto-ramp.mdx @@ -158,13 +158,13 @@ A single offer commitment within a batch transaction. ### AgentAcceptance -The agent's detached acceptance signature over an accepted Offer. It travels in the execute body alongside the reflected Offer and is independent of the transport (RFC 9421) request signature, so it survives any number of broker relays. The Exchange verifies it and binds the delivery URL to the agent's key (RFC 7638 thumbprint). `signature` is a hex-encoded detached Ed25519 signature over the deterministically marshaled [`AgentAcceptancePayload`](#agentacceptancepayload); `signature_algorithm` is `EdDSA`. +The agent's detached acceptance signature over an accepted Offer. It travels in the execute body alongside the reflected Offer and is independent of the transport (RFC 9421) request signature, so it survives any number of broker relays. The Exchange verifies it and binds the delivery URL to the agent's key (RFC 7638 thumbprint). `signature` is a hex-encoded detached Ed25519 signature over the JCS-canonicalized (RFC 8785) proto-JSON of [`AgentAcceptancePayload`](#agentacceptancepayload) — the same canonical signing form `Offer.signature` uses; `signature_algorithm` is `EdDSA`. ::proto-message{name=AgentAcceptance} ### AgentAcceptancePayload -The canonical signing structure for [`AgentAcceptance`](#agentacceptance). It is **never sent on the wire** — both the signer and the verifier marshal it deterministically (`proto.Marshal` with deterministic field order) to derive byte-identical signed bytes, so 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`. +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`. ::proto-message{name=AgentAcceptancePayload} From 1fefc44f3bb1fe0b4be24ef319283c33518770e5 Mon Sep 17 00:00:00 2001 From: legendko Date: Thu, 23 Jul 2026 16:54:56 +0200 Subject: [PATCH 05/21] docs(sdk,conformance): retire the deterministic-marshal and camelCase claims Two comment sites still described the acceptance signed bytes as deterministic protobuf, and one test executed that form -- inside the same package as canonicalsign.go, which states the opposite. A third set of comments still described the JCS rendering as camelCase, retired when the signed form was re-pinned to snake_case proto names. - acceptance_payload_test.go: drop TestAgentAcceptancePayload_deterministicMarshal. It marshalled the same message twice with Deterministic:true and asserted the results matched -- vacuous, and it advertised the superseded recipe. Replace it with a field-set guard: AgentAcceptancePayload fixes the FIELD SET the signature covers, and canonical proto-JSON omits unpopulated fields, so a field added without a matching assignment in CanonicalAcceptanceBytes drops out of the signed bytes with the golden vectors byte-identical and every gate green. - reachability_test.go: the AgentAcceptancePayload out-of-band rationale said the signer and verifier "deterministically marshal" it; it is the field set they canonicalize under RFC 8785 JCS over proto-JSON. - gen_vectors_test.go: three comments claimed camelCase. One described the pinned canonical-SIGNING option set, which is UseProtoNames:true; one claimed camelCase json_names on the wire form three lines above its own option set stating snake_case; one credited JCS with preserving a camel shape. Both sides are snake_case, so what a from-wire canonicalizer undoes is the zero-inflation and the signature fields, not the naming. Comments and one test swap only -- no behavior change. --- conformance/reachability_test.go | 2 +- sdk/go/helpers/acceptance_payload_test.go | 57 +++++++++++------------ sdk/go/helpers/gen_vectors_test.go | 12 +++-- 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/conformance/reachability_test.go b/conformance/reachability_test.go index d1ac9b7..a3f2c53 100644 --- a/conformance/reachability_test.go +++ b/conformance/reachability_test.go @@ -32,7 +32,7 @@ var outOfBandRoots = map[string]string{ "WBAFile": "served at /.well-known/http-message-signatures-directory — the pure WBA JWK Set (identity keys + revocation_url)", "ErrorDetail": "the transport-error envelope; carries the seven typed failure reasons", "KeyRevocationList": "served key-revocation document (thumbprint list), fetched out of band from WBAFile.revocation_url", - "AgentAcceptancePayload": "canonical signing structure for AgentAcceptance; never sent on the wire — the signer and verifier deterministically marshal it to derive byte-identical signed bytes", + "AgentAcceptancePayload": "canonical signing structure for AgentAcceptance; never sent on the wire — it fixes the field set the signer and verifier canonicalize (RFC 8785 JCS over canonical proto-JSON) to derive byte-identical signed bytes", } // refFields visits the message/enum each of md's fields references (following the diff --git a/sdk/go/helpers/acceptance_payload_test.go b/sdk/go/helpers/acceptance_payload_test.go index 476bccc..e4c6c28 100644 --- a/sdk/go/helpers/acceptance_payload_test.go +++ b/sdk/go/helpers/acceptance_payload_test.go @@ -4,14 +4,13 @@ import ( "testing" rampv1 "github.com/RAMP-Protocol/protocol/gen/go/ramp/v1" - "google.golang.org/protobuf/proto" ) -// The agent offer-acceptance wire envelope and its canonical -// signing payload exist on the modern execute shape. The acceptance must travel -// WITH the reflected Offer on both single-mode (TransactionRequest) and batch -// (TransactionItem), and the canonical payload must marshal deterministically -// (byte-identical across the SDK signer and the Exchange verifier). +// The agent offer-acceptance wire envelope and its canonical signing payload +// exist on the modern execute shape. The acceptance must travel WITH the +// reflected Offer on both single-mode (TransactionRequest) and batch +// (TransactionItem), so the Exchange can bind the agent to the exact offer it +// is executing no matter how many brokers relayed the request. func TestAgentAcceptance_wireFieldsExist(t *testing.T) { acc := &rampv1.AgentAcceptance{ Signature: "deadbeef", @@ -32,28 +31,28 @@ func TestAgentAcceptance_wireFieldsExist(t *testing.T) { } } -// The canonical signing structure is a dedicated message so the signer and -// verifier derive byte-identical signed bytes from the same proto. It MUST -// marshal deterministically. -func TestAgentAcceptancePayload_deterministicMarshal(t *testing.T) { - payload := &rampv1.AgentAcceptancePayload{ - OfferSig: "ex-offer-sig-hex", - RequesterId: "req-1", - RequesterDomain: "agent.example.com", - IdempotencyKey: "idem-1", - } - a, err := proto.MarshalOptions{Deterministic: true}.Marshal(payload) - if err != nil { - t.Fatalf("marshal: %v", err) - } - b, err := proto.MarshalOptions{Deterministic: true}.Marshal(payload) - if err != nil { - t.Fatalf("marshal (2): %v", err) - } - if len(a) == 0 { - t.Fatal("canonical payload marshalled to zero bytes") - } - if string(a) != string(b) { - t.Fatal("deterministic marshal is not stable across calls") +// AgentAcceptancePayload fixes the FIELD SET the acceptance signature covers; +// the byte layout is the canonical signing form defined on Offer.signature (RFC +// 8785 JCS over canonical proto-JSON). The two halves are pinned separately, and +// only this one is pinned by the message. +// +// Canonical proto-JSON omits unpopulated fields, so a field added to this message +// without a matching assignment in CanonicalAcceptanceBytes drops out of the +// signed bytes entirely: the golden vectors stay byte-identical and every gate +// stays green while the agent silently commits to less than the message claims. +// The set is therefore asserted here rather than left to review. +func TestAgentAcceptancePayload_fieldSetIsPinned(t *testing.T) { + want := []string{"offer_sig", "requester_id", "requester_domain", "idempotency_key"} + + fields := (&rampv1.AgentAcceptancePayload{}).ProtoReflect().Descriptor().Fields() + if fields.Len() != len(want) { + t.Fatalf("AgentAcceptancePayload has %d fields, want %d %v; a field the "+ + "canonicalizer does not assign is omitted from the signed bytes", + fields.Len(), len(want), 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/gen_vectors_test.go b/sdk/go/helpers/gen_vectors_test.go index d15e794..c16a060 100644 --- a/sdk/go/helpers/gen_vectors_test.go +++ b/sdk/go/helpers/gen_vectors_test.go @@ -331,7 +331,7 @@ type offerVerifyDoc struct { } // offerCanonicalProtoJSON renders offer to the SAME pinned proto-JSON the signer -// canonicalizes over (camelCase, enums-as-names, omit-unpopulated). Emitting the +// canonicalizes over (snake_case, enums-as-names, omit-unpopulated). Emitting the // vector's offer_json through the identical option set is what lets the port // reproduce JCS(protojson(offer)) byte-for-byte. func offerCanonicalProtoJSON(t *testing.T, offer *rampv1.Offer) json.RawMessage { @@ -480,11 +480,12 @@ func buildOfferVerifyVectors(t *testing.T) []offerVerifyVector { } // wireCanonicalVector pins the wire-to-canonical conversion: wire_json is the -// offer exactly as the Connect codec emits it (camelCase json_names, enums as +// offer exactly as the Connect codec emits it (snake_case proto names, enums as // names, EmitUnpopulated zero-inflation; JCS-stabilized so the committed file is // deterministic), canonical_json is the byte sequence the offer signature covers -// (CanonicalOfferBytes: signature/signature_algorithm cleared, snake_case, -// omit-unpopulated, JCS). A from-wire canonicalizer in any language must map +// (CanonicalOfferBytes: signature/signature_algorithm cleared, omit-unpopulated, +// JCS). Both sides share the snake_case naming, so what a from-wire canonicalizer +// must actually undo is the zero-inflation and the signature fields — it must map // wire_json to canonical_json exactly. type wireCanonicalVector struct { Name string `json:"name"` @@ -511,7 +512,8 @@ func buildWireCanonicalVectors(t *testing.T) []wireCanonicalVector { t.Fatalf("%s: wire proto-JSON marshal: %v", name, err) } // JCS-stabilize the committed wire form (protojson whitespace/order is not - // deterministic); key CASE is untouched, so the camel wire shape survives. + // deterministic); JCS sorts and re-encodes but never RENAMES a key, so the + // codec's snake_case wire shape survives intact. wireCanon, err := jcs.Transform(wirePJ) if err != nil { t.Fatalf("%s: wire JCS: %v", name, err) From aa95a25b19b7671997de1ae0edcb425cad1945ec Mon Sep 17 00:00:00 2001 From: legendko Date: Thu, 23 Jul 2026 16:55:08 +0200 Subject: [PATCH 06/21] docs(sdk): align the acceptance docs with the field-set/byte-layout split The proto now pins the acceptance signed bytes in two halves -- the message fixes the FIELD SET, Offer.signature fixes the BYTE LAYOUT -- but the SDK package header still carried the conflation the proto shed, and design-history recorded only the first of the two canonical-form reversals. - acceptance.go: the package header said the signed bytes are pinned by the AgentAcceptancePayload message; state both halves. CanonicalAcceptanceBytes' godoc said "the transaction's idempotency key" without saying which -- name the enclosing execute request, since a TransactionItem carries neither the requester nor the key. - acceptance_test.go: the non-mutation test spot-checked four getters and spent half its body asserting determinism. CanonicalAcceptanceBytes clones nothing, so compare whole messages with proto.Clone + proto.Equal; determinism is already pinned by the corpus test's exact-bytes assertion. - test_acceptance_jcs_repin.py: the historical re-pin narrative named a Go symbol that no longer exists. Keep the history, note the export. - design-history.md: the canonical form changed TWICE, not once. The second change re-pinned the JCS rendering from camelCase json_names to snake_case proto names and re-signed the golden vectors -- signatures over the camelCase form no longer verify. A document that exists to record reversals was missing one, and it is the second data point the "a form that changed can change again" argument rests on. Also scope the opening claim: the two payloads that moved off deterministic protobuf are the two that cover a proto message; Attestation.signature outlives an HTTP exchange too but signs a hand-built JSON object under its own rule. --- docs/design-history.md | 35 ++++++++++++------- sdk/go/helpers/acceptance.go | 12 ++++--- sdk/go/helpers/acceptance_test.go | 28 +++++++-------- sdk/python/tests/test_acceptance_jcs_repin.py | 7 ++-- 4 files changed, 48 insertions(+), 34 deletions(-) diff --git a/docs/design-history.md b/docs/design-history.md index a35268e..1d96493 100644 --- a/docs/design-history.md +++ b/docs/design-history.md @@ -39,13 +39,15 @@ 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 outlive an HTTP exchange — `Offer.signature` and +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 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 -fields omitted). +fields omitted). (`Attestation.signature` also outlives an HTTP exchange but is out +of scope here: it signs a hand-built JSON object — `{verifier, keyid, attested_at, +uri, claims}` — under its own rule, never a rendering of its proto message.) Deterministic protobuf marshaling is not actually canonical. Protobuf's own documentation disclaims byte stability across languages, across library versions, @@ -60,16 +62,25 @@ language can reproduce the signed bytes from the JSON rendering alone. The Go-emitted golden vector is the arbiter, and the TS and Python SDKs replay it byte-for-byte. -The reversal 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 canonical form -that changed once can change again, and a verifier that 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` in -TS). A party keeping evidence stores those bytes and re-verifies against them +The form then changed a **second** time, within JCS. The first cut rendered +proto-JSON with its default camelCase `json_name` keys; we re-pinned it to +snake_case proto field names (`UseProtoNames=true`) so the wire, the conformance +corpus, the generated Pydantic/Zod clients, and the signed form all share one +naming — protojson accepts both spellings on input, so the duality was silently +hiding client divergence rather than absorbing it. That re-pin **re-signed the +golden vectors**: signatures produced over the camelCase-JCS form no longer verify, +and all three SDKs had to move in the same commit. + +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 +canonical form that has already changed twice can change again, and a verifier that +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` +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. ## "Marketplace" → "Exchange" diff --git a/sdk/go/helpers/acceptance.go b/sdk/go/helpers/acceptance.go index 5b4a3be..988f2ca 100644 --- a/sdk/go/helpers/acceptance.go +++ b/sdk/go/helpers/acceptance.go @@ -17,9 +17,11 @@ import ( // it stays valid no matter how many brokers relay the request, because it covers // the offer + requester + idempotency, not the HTTP envelope. // -// The signed bytes are pinned by the AgentAcceptancePayload proto message so the -// signer (agent) and the verifier (Exchange) derive them identically; both go -// through CanonicalAcceptanceBytes, the single source of the byte layout. +// The signed bytes are pinned in two halves, both normative: the +// AgentAcceptancePayload proto message fixes the FIELD SET, and the canonical +// signing form defined on Offer.signature fixes the BYTE LAYOUT. That is what +// lets the signer (agent) and the verifier (Exchange) derive them identically; +// both go through CanonicalAcceptanceBytes, the single implementation of the pair. // AcceptanceSignatureAlgorithm is the alg advertised on AgentAcceptance. // Always EdDSA for Ed25519. @@ -33,7 +35,9 @@ var ErrAcceptanceSignatureInvalid = errors.New("helpers: offer-acceptance signat // CanonicalAcceptanceBytes returns the exact canonical byte sequence an agent's // offer acceptance covers: the accepted Offer.signature (which transitively binds // the offer's pricing, terms, expiry, and issuing Exchange), plus the requester -// identity and the transaction's idempotency key. The returned bytes are +// identity and idempotency key of the ENCLOSING execute request — in batch mode +// both come from the TransactionRequest, never from the per-item TransactionItem, +// which carries neither. The returned bytes are // byte-identical to what SignOfferAcceptance signs and VerifyOfferAcceptance // verifies over — persist them to re-verify an acceptance verbatim, independent of // how the canonical form later evolves. Re-verification also needs the persisted diff --git a/sdk/go/helpers/acceptance_test.go b/sdk/go/helpers/acceptance_test.go index f7266c5..c1f93f4 100644 --- a/sdk/go/helpers/acceptance_test.go +++ b/sdk/go/helpers/acceptance_test.go @@ -1,7 +1,6 @@ package helpers_test import ( - "bytes" "crypto/ed25519" "encoding/hex" "encoding/json" @@ -12,6 +11,7 @@ import ( rampv1 "github.com/RAMP-Protocol/protocol/gen/go/ramp/v1" "github.com/RAMP-Protocol/protocol/sdk/go/helpers" + "google.golang.org/protobuf/proto" ) func acceptanceFixture() (*rampv1.Offer, *rampv1.Requester, string) { @@ -212,23 +212,21 @@ func TestCanonicalAcceptanceBytes_failsClosed(t *testing.T) { func TestCanonicalAcceptanceBytes_doesNotMutateInputs(t *testing.T) { // The payload is built from getters onto a fresh message, so a caller can hand - // in the live Offer/Requester it is about to persist and get them back untouched. + // in the live Offer/Requester it is about to persist and get them back + // untouched. Nothing is cloned on the way in, so the whole message is compared: + // a future refactor could reach any field, not only the four the canonical form + // reads. offer, requester, idem := acceptanceFixture() - before, err := helpers.CanonicalAcceptanceBytes(offer, requester, idem) - if err != nil { + offerBefore := proto.Clone(offer) + requesterBefore := proto.Clone(requester) + + if _, err := helpers.CanonicalAcceptanceBytes(offer, requester, idem); err != nil { t.Fatal(err) } - if offer.GetSignature() != "ex-offer-sig-hex" || offer.GetOfferId() != "of_1" { - t.Error("CanonicalAcceptanceBytes must not mutate the caller's offer") - } - if requester.GetId() != "agent-1" || requester.GetDomain() != "agent.example.com" { - t.Error("CanonicalAcceptanceBytes must not mutate the caller's requester") - } - after, err := helpers.CanonicalAcceptanceBytes(offer, requester, idem) - if err != nil { - t.Fatal(err) + if !proto.Equal(offer, offerBefore) { + t.Errorf("CanonicalAcceptanceBytes mutated the caller's offer\n got %v\n want %v", offer, offerBefore) } - if !bytes.Equal(before, after) { - t.Error("CanonicalAcceptanceBytes must be deterministic for identical inputs") + if !proto.Equal(requester, requesterBefore) { + t.Errorf("CanonicalAcceptanceBytes mutated the caller's requester\n got %v\n want %v", requester, requesterBefore) } } diff --git a/sdk/python/tests/test_acceptance_jcs_repin.py b/sdk/python/tests/test_acceptance_jcs_repin.py index 83f1f50..6434328 100644 --- a/sdk/python/tests/test_acceptance_jcs_repin.py +++ b/sdk/python/tests/test_acceptance_jcs_repin.py @@ -6,9 +6,10 @@ signed_payload = JCS(protojson(msg with sig/sig_algorithm cleared)) -For acceptance this changes Go canonicalAcceptancePayload (acceptance.go) from the -hand-rolled proto3 field-tag marshal (today's acceptance-vectors.json carries -`canonical_bytes_hex` beginning `0a10…` — proto field tags) to JCS UTF-8 bytes. +For acceptance this changes Go canonicalAcceptancePayload (acceptance.go, since +exported as CanonicalAcceptanceBytes) from the hand-rolled proto3 field-tag marshal +(today's acceptance-vectors.json carries `canonical_bytes_hex` beginning `0a10…` — +proto field tags) to JCS UTF-8 bytes. The Go golden emitter therefore REGENERATES acceptance-vectors.json to the JCS form (new canonical bytes AND new signatures — the canonicalization itself changed, so this is the intended vector change, NOT a behavior-preservation violation). From a456dcd417de14a2d8f1fd7983aa601176d64ea1 Mon Sep 17 00:00:00 2001 From: legendko Date: Thu, 23 Jul 2026 16:55:17 +0200 Subject: [PATCH 07/21] docs(proto): state the acceptance canonical form without a positional pointer The AgentAcceptance block pointed at the canonical-signing definition as "defined once on Offer.signature above and applying verbatim here". Both halves were wrong in effect. "above" is a position in the .proto, and the comment does not stay there: it is compiled into gen/ts/wire/schemas.ts, which is ordered alphabetically, so AgentAcceptanceSchema lands near the top of the file and OfferSchema far below it. "verbatim" overstates. The Offer.signature formula is JCS(protojson(msg with signature + signature_algorithm cleared)); AgentAcceptancePayload has neither field, so the clear step is a no-op there. Say so explicitly instead -- the definition reduces to JCS(protojson(AgentAcceptancePayload)) -- which is more useful to an implementer than an instruction to apply a formula whose first step does nothing. Comment only -- no field, message, or wire change. gen/ regenerated (buf 1.66.1; gen/python/wire is unaffected, it carries field descriptions only) and the hand-maintained website mirror updated with the same reduction note. --- gen/descriptor.binpb | Bin 539577 -> 539813 bytes gen/go/ramp/v1/ramp.pb.go | 9 ++++++--- gen/ts/wire/schemas.ts | 2 +- proto/ramp/v1/ramp.proto | 9 ++++++--- .../src/content/docs/reference/proto-ramp.mdx | 2 +- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/gen/descriptor.binpb b/gen/descriptor.binpb index 73b70a07dd878a1cb979446d2a57d0da54bfdac5..199e2cbdadce8706828f4e4d48e5247ad7ff5b1d 100644 GIT binary patch delta 23376 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 M2XF9y)#mp2e Date: Fri, 24 Jul 2026 10:08:15 +0200 Subject: [PATCH 08/21] docs(sdk/python): state the Connect wire as snake_case in wire_canon The module docstring described the RAMP Connect wire as camelCase protojson and cited sdk/go/connectserver/codec_test.go as pinning "camelCase, NOT UseProtoNames". That test's own header pins the opposite, and codec.go marshals with UseProtoNames=true. The claim was written hours before the codec moved to snake_case proto names and was never swept; the Go side of the same claim was corrected, the Python face was not. Both the wire and the signed form are snake_case, so what the inversion undoes is the zero-inflation and the signature fields, not the naming. The retired lowerCamel json_name form stays tolerated on input and is now stated as such -- accepted when it arrives, never emitted -- which is why _snake stays and is documented as idempotent on snake keys, and why the offer_camel parameter name still reads correctly. The live-captured camelCase fixture is reframed as the pre-flip capture it is, kept as the tolerance case; the synthetic camelCase dicts in the enum-pruning and set-empty tests are noted as the same. Parity for the current wire is the drift-gated wire-canonical-vectors corpus, which the test file already replays but the docstring did not list. Docstrings only -- no behavior, no identifiers, no fixtures, no generated files. --- sdk/python/ramp_sdk/wire_canon.py | 35 +++++++++++++------- sdk/python/tests/test_wire_canon.py | 50 ++++++++++++++++++++--------- 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/sdk/python/ramp_sdk/wire_canon.py b/sdk/python/ramp_sdk/wire_canon.py index 62c563d..beb3899 100644 --- a/sdk/python/ramp_sdk/wire_canon.py +++ b/sdk/python/ramp_sdk/wire_canon.py @@ -1,13 +1,19 @@ """Wire-to-canonical offer conversion (the from-wire canonicalizer). -The RAMP Connect wire is camelCase protojson with ``EmitUnpopulated`` -(zero-valued scalars present) — pinned by the Go SDK codec test -(``sdk/go/connectserver/codec_test.go``: camelCase, NOT ``UseProtoNames``). The -offer SIGNATURE however covers the CANONICAL form: snake_case proto names, -omit-unpopulated, enums-as-names (sdk/go helpers ``canonicalSignJSONOptions``), -then RFC 8785 JCS. Every Python client verifying a wire offer must therefore -invert the wire emission before calling :func:`ramp_sdk.core.canonical_offer_payload` -— never call it on raw camelCase wire input. +The RAMP Connect wire is snake_case proto-JSON with ``EmitUnpopulated``: zero-valued +scalars, empty repeateds, null messages and ``*_UNSPECIFIED`` enums are all present. +That is what ``sdk/go/connectserver/codec.go`` marshals (``UseProtoNames: true``), and +what ``codec_test.go`` guards — a stray ``UseProtoNames=false`` is the regression it +catches. The offer SIGNATURE covers the CANONICAL form: the same snake_case proto +names, but omit-unpopulated, enums-as-names (sdk/go helpers +``canonicalSignJSONOptions``), then RFC 8785 JCS. Both sides share the naming, so what +this inversion actually undoes is the zero-inflation and the signature fields, NOT the +naming. Every Python client verifying a wire offer must still perform it before calling +:func:`ramp_sdk.core.canonical_offer_payload` — never call that on raw wire input. + +The retired proto-JSON lowerCamel ``json_name`` form is still TOLERATED on input (hence +:func:`_snake`, the ``offer_camel`` parameter name, and the pre-flip fixture the tests +keep). It is out of contract: accepted when it arrives, never emitted. :func:`from_wire_offer` performs that inversion SCHEMA-AWARE, driven by the generated ``wire.models`` classes (gen/python), whose field DEFAULTS encode @@ -21,8 +27,9 @@ data, never case-converted). ``*_UNSPECIFIED`` enum values are zero values and drop. -Byte-parity with the Go oracle is pinned by ``tests/test_wire_canon.py`` over a -live-captured wire/canonical fixture pair. +Byte-parity with the Go oracle is pinned by ``tests/test_wire_canon.py``: over the +drift-gated ``sdk/go/helpers/testdata/wire-canonical-vectors.json`` corpus for the +current snake_case wire, and over a live-captured fixture pair for the retired form. KNOWN LIMITS (fail-closed, never fail-open): a wire offer carrying fields newer than the pinned gen models is kept verbatim and will verify FALSE (rejected, @@ -46,7 +53,11 @@ def _snake(key: str) -> str: - """Invert protojson's default lowerCamel json_name back to the proto name.""" + """Invert protojson's retired lowerCamel json_name back to the proto name. + + Idempotent on already-snake keys, which is what the current wire and the shared + corpus emit — so the inversion costs nothing and keeps the retired form working. + """ return _CAMEL_BOUNDARY.sub(r"_\1", key).lower() @@ -102,7 +113,7 @@ def _canon_field(field: FieldInfo, value: Any) -> Any: def _canon_message(model_cls: type[Any], wire: dict[str, Any]) -> dict[str, Any]: - """Reconstruct one message's canonical dict from its wire (camel) dict.""" + """Reconstruct one message's canonical dict from its wire dict.""" fields = model_cls.model_fields out: dict[str, Any] = {} for key, value in wire.items(): diff --git a/sdk/python/tests/test_wire_canon.py b/sdk/python/tests/test_wire_canon.py index 3cde9de..a88887f 100644 --- a/sdk/python/tests/test_wire_canon.py +++ b/sdk/python/tests/test_wire_canon.py @@ -1,19 +1,23 @@ """sdk/python wire-to-canonical offer canonicalizer. -The RAMP Connect wire emitted by the Broker is camelCase protojson with -EmitUnpopulated (zero-valued scalars present). The offer SIGNATURE covers the -CANONICAL form: snake_case proto names, omit-unpopulated, enums-as-names, then -RFC 8785 JCS. ``from_wire_offer`` bridges the two forms; it belongs in sdk/python -so every RAMP client (MCP shim, future TS MCP, future Python broker) shares one +The RAMP Connect wire emitted by the Broker is snake_case proto-JSON with +EmitUnpopulated (zero-valued scalars, empty repeateds, null messages and +``*_UNSPECIFIED`` enums all present). The offer SIGNATURE covers the CANONICAL form: +the same snake_case proto names, but omit-unpopulated, enums-as-names, then RFC 8785 +JCS. Both sides share the naming, so what ``from_wire_offer`` undoes is the +zero-inflation and the signature fields, not the naming. It belongs in sdk/python so +every RAMP client (MCP shim, future TS MCP, future Python broker) shares one wire-normalization function proven byte-identical to the Go oracle. -Three behaviors pinned here: +Four behaviors pinned here: -(a) GO-ORACLE PARITY — ``from_wire_offer(offer_wire_camel)`` produces a canonical - dict whose JCS (with ``signature``/``signature_algorithm`` stripped) equals the - committed ``offer_canonical_go.json`` fixture byte-for-byte. The fixture pair - was captured from the live e2e stack (offer_wire_camel.json = real broker wire - output; offer_canonical_go.json = Go canonical form). +(a) LEGACY camelCase TOLERANCE — ``offer_wire_camel.json`` is a PRE-FLIP capture, + taken from the live e2e stack before the Connect wire moved to snake_case proto + names (``UseProtoNames=true``); ``offer_canonical_go.json`` is the Go canonical + form of the same offer. It is kept because the inversion must go on tolerating + the retired lowerCamel form. ``from_wire_offer`` on it produces a canonical dict + whose JCS (with ``signature``/``signature_algorithm`` stripped) equals the + committed fixture byte-for-byte. The CURRENT wire is covered by (d). (b) UNSPECIFIED ENUM PRUNING — a wire offer carrying ``deliveryMethod='DELIVERY_METHOD_UNSPECIFIED'`` must emit a canonical dict @@ -28,6 +32,15 @@ this dedicated test makes the invariant explicit and survives future fixture rotation. +(d) DRIFT-GATED GO-ORACLE PARITY — the committed + ``sdk/go/helpers/testdata/wire-canonical-vectors.json`` corpus, emitted by the Go + golden emitter and replayed by Go, Python and TS. This is the authoritative + parity for the CURRENT snake_case wire; unlike (a) it cannot go silently stale. + +(b) and (c) feed camelCase synthetic dicts too, so they exercise the same tolerance +as (a). The rules they pin are naming-independent, and their snake_case twins are the +``unspecified_enum_pruned`` and ``set_empty_optional_unit`` vectors in (d). + RED now: ``ramp_sdk.wire_canon`` does not exist yet. The module-level import below causes a collection error, which is the established red style in this suite (mirrors test_client_binding_smoke.py and test_core_offer_verify_parity.py: @@ -54,20 +67,25 @@ # --------------------------------------------------------------------------- -# (a) Go-oracle parity +# (a) Legacy camelCase tolerance — Go-oracle parity on the pre-flip fixture # --------------------------------------------------------------------------- def test_from_wire_offer_go_oracle_parity() -> None: """from_wire_offer(wire_camel) JCS-equals the committed Go canonical output. - The fixture pair was captured from the LIVE e2e stack: offer_wire_camel.json - is a real demo offer exactly as the Broker's Connect codec emitted it - (camelCase, EmitUnpopulated zero-inflation, ``unit:""`` set-empty optional), - and offer_canonical_go.json is the canonical form computed by the Go side + The fixture pair was captured from the LIVE e2e stack BEFORE the Connect wire + moved to snake_case proto names: offer_wire_camel.json is a real demo offer + exactly as the Broker's Connect codec emitted it back then (camelCase, + EmitUnpopulated zero-inflation, ``unit:""`` set-empty optional), and + offer_canonical_go.json is the canonical form computed by the Go side (protojson UseProtoNames + omit-unpopulated, then RFC 8785 JCS) — the byte sequence the exchange's offer signature covers. + The wire has since flipped to snake_case, so this pair no longer describes what + a Broker emits; it is retained as the tolerance case, pinning that a lowerCamel + wire offer still canonicalizes correctly. Parity for the CURRENT wire is (d). + A divergence here means from_wire_offer diverges from the Go oracle and every genuine offer signature would fail to verify in the Python shim. """ From 7d7c75217c12b5c6ccd1381fea2801f8100c7676 Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 24 Jul 2026 15:53:39 +0200 Subject: [PATCH 09/21] fix(sdk): omit every unpopulated field from the hand-built acceptance payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python jcs_acceptance_payload and TS acceptancePayload assemble the AgentAcceptancePayload JSON object key by key and dropped only an empty requester_domain, always emitting requester_id and idempotency_key. Go renders the same object through protojson with EmitUnpopulated=false, which omits EVERY unpopulated field, so the three SDKs signed different bytes whenever requester_id was empty — a wire-valid input, since Requester.id carries no min_len. The mismatch failed closed (a byte difference, never a bypass), but it falsified the byte-equivalence the canonical-bytes accessors are registered as parity symbols to promise. Both hand-built faces now drop each empty string field. The corpus gains an empty_requester_id vector so the agreement is pinned rather than asserted. The change is additive only: the existing vectors and their signatures are byte-identical, so no already-issued signature is affected. --- proto/CHANGELOG.md | 14 ++++++++++++ sdk/go/helpers/gen_vectors_test.go | 5 +++++ .../helpers/testdata/acceptance-vectors.json | 11 ++++++++++ sdk/python/ramp_sdk/core.py | 22 +++++++++++-------- sdk/ts/src/acceptance.ts | 16 ++++++++------ .../src/content/docs/reference/changelog.mdx | 14 ++++++++++++ 6 files changed, 66 insertions(+), 16 deletions(-) diff --git a/proto/CHANGELOG.md b/proto/CHANGELOG.md index e9af66a..23c6743 100644 --- a/proto/CHANGELOG.md +++ b/proto/CHANGELOG.md @@ -82,6 +82,20 @@ layout. Implementations that followed the stale text would have produced non-verifying signatures. No field, message, or wire change — comments only, with `gen/` and the website mirror regenerated. +**Python + TS SDK: the hand-built acceptance payload omits every unpopulated field +(bug fix, no wire change).** `jcs_acceptance_payload` (Python) and `acceptancePayload` +(TS) assemble the `AgentAcceptancePayload` JSON object key by key, and omitted only an +empty `requester_domain` — `requester_id` and `idempotency_key` were always emitted. +Go renders the same object through `protojson` with `EmitUnpopulated=false`, which omits +EVERY unpopulated field, so the three SDKs signed different bytes whenever `requester_id` +was empty. That input is wire-valid: `Requester.id` carries no `min_len`. Verification +failed closed on it (a byte mismatch, never a bypass), but the byte-equivalence the +canonical-bytes accessors promise did not hold. Both hand-built faces now drop each empty +string field, and a new `empty_requester_id` vector in +`sdk/go/helpers/testdata/acceptance-vectors.json` pins the agreement across Go, Python and +TS. The corpus change is purely additive — the pre-existing vectors and their signatures +are byte-identical, so no already-issued signature is affected. + **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written billing label on `Requester` is gone; the field is deleted outright with no `reserved` statement — pre-v1 the number returns to the free pool, and diff --git a/sdk/go/helpers/gen_vectors_test.go b/sdk/go/helpers/gen_vectors_test.go index c16a060..f987400 100644 --- a/sdk/go/helpers/gen_vectors_test.go +++ b/sdk/go/helpers/gen_vectors_test.go @@ -1019,6 +1019,11 @@ func buildAcceptanceVectors(t *testing.T) []acceptanceVector { specs := []spec{ {"all_present", "ex-offer-sig-hex", "agent-1", "agent.example.com", "idem-1"}, {"empty_domain", "sig2deadbeef", "agent-2", "", "idem-2"}, + // Requester.id carries no min_len, so an empty id is wire-valid. The + // canonical form omits EVERY unpopulated field, not only the domain — this + // vector is what holds the hand-built Python/TS payloads to that, since + // they enumerate the keys instead of inheriting EmitUnpopulated=false. + {"empty_requester_id", "sig3deadbeef", "", "agent.example.com", "idem-3"}, } out := make([]acceptanceVector, 0, len(specs)) diff --git a/sdk/go/helpers/testdata/acceptance-vectors.json b/sdk/go/helpers/testdata/acceptance-vectors.json index 7ed6b20..d74ab04 100644 --- a/sdk/go/helpers/testdata/acceptance-vectors.json +++ b/sdk/go/helpers/testdata/acceptance-vectors.json @@ -22,6 +22,17 @@ "signature_hex": "a067c7d83dcf801f10389a5b2e2936756aae37006478b1eae5dbf70805fdf2feb0292e3ae3b5511a3921f4fe331c52bfc19894f25332f0b2ba2d741db0769506", "pubkey_b64": "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=", "seed_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + }, + { + "name": "empty_requester_id", + "offer_sig": "sig3deadbeef", + "requester_id": "", + "requester_domain": "agent.example.com", + "idempotency_key": "idem-3", + "canonical_jcs": "{\"idempotency_key\":\"idem-3\",\"offer_sig\":\"sig3deadbeef\",\"requester_domain\":\"agent.example.com\"}", + "signature_hex": "5e9baefd90faae7edc44c8bcd3c6aec68fc3d9c253a6791ab83d6dcb6d638310053b5cb3607f19618cff96e5fc002a7d93d600ce16ba77019c73a2b7c50d1c01", + "pubkey_b64": "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=", + "seed_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" } ] } diff --git a/sdk/python/ramp_sdk/core.py b/sdk/python/ramp_sdk/core.py index ac6acdd..d29f737 100644 --- a/sdk/python/ramp_sdk/core.py +++ b/sdk/python/ramp_sdk/core.py @@ -272,20 +272,24 @@ def jcs_acceptance_payload( """Canonical acceptance bytes = JCS(protojson(AgentAcceptancePayload)). The same JCS(protojson(...)) canonicalization the offer signature uses. proto-JSON - OMITS unpopulated fields, so an empty ``requester_domain`` is absent from the - object before JCS (matching the Go oracle's empty-domain vector). Fail-closed on - an empty ``offer_sig`` (mirror Go CanonicalAcceptanceBytes): an empty anchor - would let the acceptance float free of any concrete offer. + OMITS unpopulated fields, so EVERY empty string field is absent from the object + before JCS — not just ``requester_domain``. The Go oracle gets that from + ``EmitUnpopulated=false``; this hand-built object has to do it per field, or an + empty ``requester_id`` would sign bytes Go never produces and cross-language + verification would fail on a wire-valid input (``Requester.id`` carries no + ``min_len``). Fail-closed on an empty ``offer_sig`` (mirror Go + CanonicalAcceptanceBytes): an empty anchor would let the acceptance float free of + any concrete offer. """ if offer_sig == "": raise ValueError("cannot accept an unsigned offer (empty offer signature)") - obj: dict[str, str] = { - "offer_sig": offer_sig, - "requester_id": requester_id, - "idempotency_key": idempotency_key, - } + obj: dict[str, str] = {"offer_sig": offer_sig} + if requester_id != "": + obj["requester_id"] = requester_id if requester_domain != "": obj["requester_domain"] = requester_domain + if idempotency_key != "": + obj["idempotency_key"] = idempotency_key return rfc8785.dumps(obj) diff --git a/sdk/ts/src/acceptance.ts b/sdk/ts/src/acceptance.ts index ef45fcb..aeb2045 100644 --- a/sdk/ts/src/acceptance.ts +++ b/sdk/ts/src/acceptance.ts @@ -52,7 +52,7 @@ function bytesToHex(bytes: Uint8Array): string { /** * acceptancePayload reproduces the canonical signed bytes: - * JCS(protojson(AgentAcceptancePayload)) with an empty requester_domain omitted. + * JCS(protojson(AgentAcceptancePayload)) with EVERY empty string field omitted. * Throws on an empty offer signature (fail-closed, mirror Go * CanonicalAcceptanceBytes / Python jcs_acceptance_payload). */ @@ -62,13 +62,15 @@ export function acceptancePayload(input: AcceptanceInput): Uint8Array = { - offer_sig: input.offerSig, - requester_id: input.requesterId, - idempotency_key: input.idempotencyKey, - }; - // proto omit-unpopulated: an empty requester_domain is absent before JCS. + // proto omit-unpopulated: every empty string field is absent before JCS. The Go + // oracle gets that from EmitUnpopulated=false; this hand-built object has to do it + // per field, or an empty requester_id would sign bytes Go never produces and + // cross-language verification would fail on a wire-valid input (Requester.id + // carries no min_len). + const obj: Record = { offer_sig: input.offerSig }; + if (input.requesterId !== "") obj.requester_id = input.requesterId; if (input.requesterDomain !== "") obj.requester_domain = input.requesterDomain; + if (input.idempotencyKey !== "") obj.idempotency_key = input.idempotencyKey; const jcs = canonicalize(obj); if (jcs === undefined) { throw new Error("ramp/acceptance: payload is not JSON-serializable"); diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index 39f5946..d6fe90c 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -65,6 +65,20 @@ layout. Implementations that followed the stale text would have produced non-verifying signatures. No field, message, or wire change — comments only, with `gen/` and the website mirror regenerated. +**Python + TS SDK: the hand-built acceptance payload omits every unpopulated field +(bug fix, no wire change).** `jcs_acceptance_payload` (Python) and `acceptancePayload` +(TS) assemble the `AgentAcceptancePayload` JSON object key by key, and omitted only an +empty `requester_domain` — `requester_id` and `idempotency_key` were always emitted. +Go renders the same object through `protojson` with `EmitUnpopulated=false`, which omits +EVERY unpopulated field, so the three SDKs signed different bytes whenever `requester_id` +was empty. That input is wire-valid: `Requester.id` carries no `min_len`. Verification +failed closed on it (a byte mismatch, never a bypass), but the byte-equivalence the +canonical-bytes accessors promise did not hold. Both hand-built faces now drop each empty +string field, and a new `empty_requester_id` vector in +`sdk/go/helpers/testdata/acceptance-vectors.json` pins the agreement across Go, Python and +TS. The corpus change is purely additive — the pre-existing vectors and their signatures +are byte-identical, so no already-issued signature is affected. + **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written billing label on `Requester` is gone; the field is deleted outright with no `reserved` statement — pre-v1 the number returns to the free pool, and From 3f64226baa142dd13a1bb5a55c7edcdbec720254 Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 24 Jul 2026 15:53:51 +0200 Subject: [PATCH 10/21] docs(sdk/python): attribute signature-field clearing to canonical_offer_payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wire_canon module and test docstrings claimed the wire-to-canonical inversion undoes "the zero-inflation and the signature fields". It does not: from_wire_offer keeps signature/signature_algorithm verbatim (a non-empty value survives the presence check), and canonical_offer_payload strips them on the way into JCS — which is why the tests here strip the two keys by hand before comparing. Misattributing a security-relevant step invites a reader to treat from_wire_offer output as the pre-JCS signed object. Also shift the re-pin suite's two present-tense claims about the acceptance corpus to past tense. Both described the file as still carrying the retired protobuf-binary form; it has carried canonical_jcs since the re-pin landed, so the preserved red-step narrative now reads as history rather than current state. --- sdk/python/ramp_sdk/wire_canon.py | 9 +++++--- sdk/python/tests/test_acceptance_jcs_repin.py | 21 ++++++++++--------- sdk/python/tests/test_wire_canon.py | 8 ++++--- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/sdk/python/ramp_sdk/wire_canon.py b/sdk/python/ramp_sdk/wire_canon.py index beb3899..2e02ac5 100644 --- a/sdk/python/ramp_sdk/wire_canon.py +++ b/sdk/python/ramp_sdk/wire_canon.py @@ -7,9 +7,12 @@ catches. The offer SIGNATURE covers the CANONICAL form: the same snake_case proto names, but omit-unpopulated, enums-as-names (sdk/go helpers ``canonicalSignJSONOptions``), then RFC 8785 JCS. Both sides share the naming, so what -this inversion actually undoes is the zero-inflation and the signature fields, NOT the -naming. Every Python client verifying a wire offer must still perform it before calling -:func:`ramp_sdk.core.canonical_offer_payload` — never call that on raw wire input. +this inversion undoes is the zero-inflation, NOT the naming. It does NOT clear the +signature fields — :func:`from_wire_offer` keeps ``signature`` / +``signature_algorithm`` verbatim, and +:func:`ramp_sdk.core.canonical_offer_payload` strips them on the way into JCS. Every +Python client verifying a wire offer must still perform the inversion before calling +that function — never call it on raw wire input. The retired proto-JSON lowerCamel ``json_name`` form is still TOLERATED on input (hence :func:`_snake`, the ``offer_camel`` parameter name, and the pre-flip fixture the tests diff --git a/sdk/python/tests/test_acceptance_jcs_repin.py b/sdk/python/tests/test_acceptance_jcs_repin.py index 6434328..7eafba8 100644 --- a/sdk/python/tests/test_acceptance_jcs_repin.py +++ b/sdk/python/tests/test_acceptance_jcs_repin.py @@ -6,24 +6,25 @@ signed_payload = JCS(protojson(msg with sig/sig_algorithm cleared)) -For acceptance this changes Go canonicalAcceptancePayload (acceptance.go, since +For acceptance this changed Go canonicalAcceptancePayload (acceptance.go, since exported as CanonicalAcceptanceBytes) from the hand-rolled proto3 field-tag marshal -(today's acceptance-vectors.json carries `canonical_bytes_hex` beginning `0a10…` — -proto field tags) to JCS UTF-8 bytes. +(acceptance-vectors.json then carried `canonical_bytes_hex` beginning `0a10…` — proto +field tags) to JCS UTF-8 bytes. The Go golden emitter therefore REGENERATES acceptance-vectors.json to the JCS form (new canonical bytes AND new signatures — the canonicalization itself changed, so this is the intended vector change, NOT a behavior-preservation violation). This re-pin suite asserts the Python core canonicalizes acceptance via JCS and produces byte-identical canonical bytes + signature to the REGENERATED Go oracle. -It is RED now for TWO expected reasons: - 1. sdk/python/ramp_sdk/core (the JCS acceptance canonicalizer + sign/verify) does +It was authored RED, for TWO expected reasons — both since resolved, so the suite is +green and the narrative below is history, not current state: + 1. sdk/python/ramp_sdk/core (the JCS acceptance canonicalizer + sign/verify) did not exist yet. - 2. The regenerated acceptance vectors do not exist yet — today's - acceptance-vectors.json is still the protobuf-binary form (no `canonical_jcs` - field, no `canonicalization: "jcs"` marker). The assertions below require the - JCS-regenerated shape, so they FAIL against the current file (and the import - fails outright), a clean TDD-red signal. + 2. The regenerated acceptance vectors did not exist yet — acceptance-vectors.json + was still the protobuf-binary form (no `canonical_jcs` field, no + `canonicalization: "jcs"` marker). The assertions below require the + JCS-regenerated shape, so they FAILED against the file as it then stood (and the + import failed outright), a clean TDD-red signal. Note: this is the acceptance sibling of test_core_offer_verify_parity.py; the two prove Go/TS/Python agree on the SAME JCS canonicalization for both signed payloads. diff --git a/sdk/python/tests/test_wire_canon.py b/sdk/python/tests/test_wire_canon.py index a88887f..d35a64b 100644 --- a/sdk/python/tests/test_wire_canon.py +++ b/sdk/python/tests/test_wire_canon.py @@ -5,9 +5,11 @@ ``*_UNSPECIFIED`` enums all present). The offer SIGNATURE covers the CANONICAL form: the same snake_case proto names, but omit-unpopulated, enums-as-names, then RFC 8785 JCS. Both sides share the naming, so what ``from_wire_offer`` undoes is the -zero-inflation and the signature fields, not the naming. It belongs in sdk/python so -every RAMP client (MCP shim, future TS MCP, future Python broker) shares one -wire-normalization function proven byte-identical to the Go oracle. +zero-inflation, not the naming; it keeps the signature fields, which +``ramp_sdk.core.canonical_offer_payload`` strips on the way into JCS (hence the manual +strip in the assertions below). It belongs in sdk/python so every RAMP client (MCP +shim, future TS MCP, future Python broker) shares one wire-normalization function +proven byte-identical to the Go oracle. Four behaviors pinned here: From 188b1d3a09bdd3b49fbaec372f098c05966e8cb4 Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 24 Jul 2026 15:53:51 +0200 Subject: [PATCH 11/21] test(sdk-go): drop the duplicate acceptance-corpus replay TestCanonicalAcceptanceBytes_matchesCorpus re-declared the committed corpus schema field for field and asserted an equality the golden emitter already gates: TestGenerateVectors rebuilds acceptance-vectors.json through CanonicalAcceptanceBytes and byte-compares the whole committed file on every default run. Two structs describing one file drift apart on the next corpus-shape change, which is exactly what the canonicalization re-pin just did. The exported symbol keeps its external-package coverage through the byte-parity, fail-closed and non-mutation tests. A comment records why corpus byte-identity is not asserted a second time here. --- sdk/go/helpers/acceptance_test.go | 57 +++---------------------------- 1 file changed, 5 insertions(+), 52 deletions(-) diff --git a/sdk/go/helpers/acceptance_test.go b/sdk/go/helpers/acceptance_test.go index c1f93f4..0ee4371 100644 --- a/sdk/go/helpers/acceptance_test.go +++ b/sdk/go/helpers/acceptance_test.go @@ -3,10 +3,7 @@ package helpers_test import ( "crypto/ed25519" "encoding/hex" - "encoding/json" "errors" - "os" - "path/filepath" "testing" rampv1 "github.com/RAMP-Protocol/protocol/gen/go/ramp/v1" @@ -144,55 +141,11 @@ func TestCanonicalAcceptanceBytes_matchesSignOfferAcceptance(t *testing.T) { } } -// acceptanceCorpus is the cross-language acceptance fixture: the Go-emitted -// golden the TS and Python acceptance faces replay byte-for-byte. Asserting the -// exported accessor against canonical_jcs pins Go/TS/Python to one byte layout -// through the corpus that already gates the other two. -const acceptanceCorpus = "testdata/acceptance-vectors.json" - -type acceptanceCorpusFile struct { - Canonicalization string `json:"canonicalization"` - Vectors []struct { - Name string `json:"name"` - OfferSig string `json:"offer_sig"` - RequesterID string `json:"requester_id"` - RequesterDomain string `json:"requester_domain"` - IdempotencyKey string `json:"idempotency_key"` - CanonicalJCS string `json:"canonical_jcs"` - } `json:"vectors"` -} - -func TestCanonicalAcceptanceBytes_matchesCorpus(t *testing.T) { - t.Parallel() - raw, err := os.ReadFile(filepath.Clean(acceptanceCorpus)) - if err != nil { - t.Fatalf("read acceptance corpus: %v", err) - } - var corpus acceptanceCorpusFile - if err := json.Unmarshal(raw, &corpus); err != nil { - t.Fatalf("unmarshal acceptance corpus: %v", err) - } - if corpus.Canonicalization != "jcs" { - t.Fatalf("acceptance corpus canonicalization = %q, want jcs", corpus.Canonicalization) - } - if len(corpus.Vectors) == 0 { - t.Fatal("acceptance corpus has no vectors") - } - for _, v := range corpus.Vectors { - t.Run(v.Name, func(t *testing.T) { - t.Parallel() - offer := &rampv1.Offer{Signature: v.OfferSig} - requester := &rampv1.Requester{Id: v.RequesterID, Domain: v.RequesterDomain} - got, err := helpers.CanonicalAcceptanceBytes(offer, requester, v.IdempotencyKey) - if err != nil { - t.Fatalf("CanonicalAcceptanceBytes: %v", err) - } - if string(got) != v.CanonicalJCS { - t.Errorf("canonical bytes\n got %s\n want %s", got, v.CanonicalJCS) - } - }) - } -} +// Byte-identity against the committed cross-language corpus is not asserted here: +// the golden emitter rebuilds testdata/acceptance-vectors.json through this same +// function and byte-compares the whole committed file on every default run, so a +// second harness over the same equality would only add a second schema to keep in +// step with the file. func TestCanonicalAcceptanceBytes_failsClosed(t *testing.T) { offer, requester, idem := acceptanceFixture() From 45985f4e6476af3b0e49fc8c38e8f86544ce6a9c Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 24 Jul 2026 15:54:05 +0200 Subject: [PATCH 12/21] docs(sdk-go): state the canonical-bytes contract against its normative definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both accessors deferred the pinned proto-JSON option set to the unexported canonicalSignPayload and to canonicalsign.go by filename — neither of which a godoc reader can resolve. They now name the decisive options inline and point at the single normative statement of the form, the Offer.signature comment in ramp.proto, rather than restating the recipe a third time. The two godocs also stated the persistence guarantee along different axes ("how the Offer message later evolves" against "how the canonical form later evolves"). The canonical-form axis is the one the design history argues, so it is primary in both; the offer keeps its message-evolution clause. Both now close the evidence contract: a passing signature proves only that the key holder signed those exact bytes, so a caller must also match the parsed content against the transaction under dispute — otherwise any valid triple minted under the same key passes as evidence for any transaction. The design history says the same, and additionally records why omit-unpopulated obliges a hand-built payload to enumerate the omission per field. Also corrects the attestation message name in the design history: the message is ResourceAttestation; no Attestation message exists. --- docs/design-history.md | 27 ++++++++++++++++++++++++--- sdk/go/helpers/acceptance.go | 27 +++++++++++++++++---------- sdk/go/helpers/offer.go | 22 +++++++++++++++++----- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/docs/design-history.md b/docs/design-history.md index 1d96493..a1df6e1 100644 --- a/docs/design-history.md +++ b/docs/design-history.md @@ -45,9 +45,10 @@ the agent's `AgentAcceptance.signature` — originally covered deterministic pro 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 -fields omitted). (`Attestation.signature` also outlives an HTTP exchange but is out -of scope here: it signs a hand-built JSON object — `{verifier, keyid, attested_at, -uri, claims}` — under its own rule, never a rendering of its proto message.) +fields omitted). (`ResourceAttestation.signature` also outlives an HTTP exchange but +is out of scope here: it signs a hand-built JSON object — `{verifier, keyid, +attested_at, uri, claims}` — under its own rule, never a rendering of its proto +message.) Deterministic protobuf marshaling is not actually canonical. Protobuf's own documentation disclaims byte stability across languages, across library versions, @@ -71,6 +72,19 @@ hiding client divergence rather than absorbing it. That re-pin **re-signed the golden vectors**: signatures produced over the camelCase-JCS form no longer verify, and all three SDKs had to move in the same commit. +One clause of that pinned option set carries more weight than it looks. +`EmitUnpopulated=false` means an empty field is *absent* from the JSON, not present +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 all three SDKs hand-build the object, and it 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 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 canonical form that has already changed twice can change again, and a verifier that @@ -83,6 +97,13 @@ signed". So all three SDKs expose the exact signed bytes as a public accessor 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. +Storing them is necessary, not sufficient. A signature that verifies over stored bytes +proves only that the holder of that key signed *those bytes*; it says nothing about +which transaction they belong to. Whoever reads the evidence must also parse the stored +bytes and match their content — offer id, requester, idempotency key — against the +transaction under dispute. Skip that and any valid triple minted under the same key +passes as evidence for any transaction. + ## "Marketplace" → "Exchange" The selling intermediary was originally called a *marketplace*. We renamed it to diff --git a/sdk/go/helpers/acceptance.go b/sdk/go/helpers/acceptance.go index 988f2ca..a8a7b1e 100644 --- a/sdk/go/helpers/acceptance.go +++ b/sdk/go/helpers/acceptance.go @@ -40,19 +40,26 @@ var ErrAcceptanceSignatureInvalid = errors.New("helpers: offer-acceptance signat // which carries neither. The returned bytes are // byte-identical to what SignOfferAcceptance signs and VerifyOfferAcceptance // verifies over — persist them to re-verify an acceptance verbatim, independent of -// how the canonical form later evolves. Re-verification also needs the persisted -// AgentAcceptance.signature and the signer's trusted public key: these bytes are the -// signed message, necessary but not by themselves sufficient. +// how the canonical form later evolves. +// +// Re-verification also needs the persisted AgentAcceptance.signature and the signer's +// trusted public key: these bytes are the signed message, necessary but not by +// themselves sufficient. A passing signature proves only that the key holder signed +// THESE bytes, so a caller weighing them as evidence must also parse them and match +// their content against the transaction in question. // // The canonical form is RFC 8785 JCS over canonical proto-JSON — -// JCS(protojson(AgentAcceptancePayload)) — via canonicalSignPayload, the same -// primitive the offer signature uses, so any language (Go/TS/Python) reproduces the -// exact signed bytes without a protobuf binary codec. See canonicalsign.go for the -// pinned proto-JSON option set. +// JCS(protojson(AgentAcceptancePayload)) — the same definition the offer signature +// uses, stated normatively on Offer.signature in ramp.proto: snake_case proto field +// names, enums as name strings, unpopulated fields omitted. AgentAcceptancePayload +// carries no signature fields, so the clear-then-render step reduces to a plain +// render. Any language (Go/TS/Python) reproduces the exact bytes from that +// definition, without a protobuf binary codec. // -// Unpopulated fields are OMITTED before JCS, so an empty requester domain is absent -// from the object entirely rather than emitted as "": the bytes for an empty domain -// are not the bytes for any populated one. +// Unpopulated fields are OMITTED before JCS — EVERY empty string field, not just the +// domain: the bytes for an empty requester id are not the bytes for any populated +// one. A port that assembles this object by hand instead of rendering the proto must +// reproduce that omission per field, or it signs bytes this function never produces. // // Fails closed on a nil offer, a nil requester, or an unsigned offer (empty // Offer.signature) — an empty anchor would let the acceptance float free of any diff --git a/sdk/go/helpers/offer.go b/sdk/go/helpers/offer.go index e9350d5..a303694 100644 --- a/sdk/go/helpers/offer.go +++ b/sdk/go/helpers/offer.go @@ -68,19 +68,31 @@ func VerifyOffer(offer *rampv1.Offer, signatureHex string, pub ed25519.PublicKey // with ONLY the signature and signature_algorithm fields cleared, per // ramp.proto Offer.signature. The returned bytes are byte-identical to what // SignOffer signs and VerifyOffer verifies over — persist them to re-verify an -// Offer signature verbatim, independent of how the Offer message later evolves. +// Offer signature verbatim, independent of how the canonical form, or the Offer +// message, later evolves. +// // Re-verification also needs the persisted Offer.signature and the signer's trusted // public key: these bytes are the signed message, necessary but not by themselves -// sufficient. +// sufficient. A passing signature proves only that the key holder signed THESE bytes, +// so a caller weighing them as evidence must also parse them and match their content +// against the transaction in question. // // The canonical form is RFC 8785 JCS over canonical proto-JSON — -// JCS(protojson(offer with sig cleared)) — via canonicalSignPayload, so any -// language (Go/TS/Python) reproduces the exact signed bytes without a protobuf -// binary codec. See canonicalsign.go for the pinned proto-JSON option set. +// JCS(protojson(offer with sig cleared)) — rendered under the option set the +// Offer.signature comment in ramp.proto defines normatively: snake_case proto field +// names, enums as name strings, unpopulated fields omitted, 64-bit ints as decimal +// strings. That definition, not this implementation, is what lets any language +// (Go/TS/Python) reproduce the exact signed bytes without a protobuf binary codec. // // expires_at is covered (only signature/signature_algorithm are cleared), so a // relaying Broker cannot extend or shorten a signed offer's TTL under an // otherwise-valid signature. +// +// KNOWN LIMIT (fail-closed, never fail-open): a field a newer peer set that this +// build's generated types do not know arrives as an unknown field, and proto-JSON +// does not render unknown fields — it is absent from the returned bytes. A signature +// that covered it therefore fails to verify, rather than verifying over a message +// silently missing part of what was signed. func CanonicalOfferBytes(offer *rampv1.Offer) ([]byte, error) { if offer == nil { return nil, errors.New("helpers: offer is nil") From 1dc97b62f3b4f0044d87e8dae2aefb4e357a12fb Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 24 Jul 2026 15:54:05 +0200 Subject: [PATCH 13/21] test(sdk-go): pin unknown-field canonicalization as fail-closed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An Offer from a peer built against a newer schema keeps its unrecognized fields as protobuf unknown fields, but proto-JSON does not render them — so they never reach the canonical bytes. That is the safe direction (the peer's signature covered more than we can reconstruct, so it is rejected rather than verified over a truncated message), but nothing pinned it, unlike the equivalent limit the Python canonicalizer documents and tests. The test builds the newer-peer message by appending an unknown field to the encoded form, proves it survives unmarshal, proves it does not reach the canonical bytes, and proves a signature over the peer's own rendering is rejected. A signature over the reconstructable bytes still verifies, so the rejection is attributable to the extra signed member. --- sdk/go/helpers/offer_test.go | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/sdk/go/helpers/offer_test.go b/sdk/go/helpers/offer_test.go index a1c7420..e1ee612 100644 --- a/sdk/go/helpers/offer_test.go +++ b/sdk/go/helpers/offer_test.go @@ -10,6 +10,8 @@ import ( rampv1 "github.com/RAMP-Protocol/protocol/gen/go/ramp/v1" "github.com/RAMP-Protocol/protocol/sdk/go/helpers" + "google.golang.org/protobuf/encoding/protowire" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -173,3 +175,51 @@ func TestCanonicalOfferBytes_nil(t *testing.T) { t.Error("nil offer should error") } } + +func TestCanonicalOfferBytes_unknownFieldsFailClosed(t *testing.T) { + // An Offer from a peer built against a newer schema carries fields this build's + // generated types do not know. proto.Unmarshal keeps them as unknown fields, but + // proto-JSON does not render them — so they are absent from the canonical bytes. + // The direction that matters is which way the gap fails: the peer's signature + // covered MORE than we can reconstruct, so it must be rejected, never accepted + // over the truncated message. + pub, priv, _ := ed25519.GenerateKey(nil) + base := sampleOffer() + + wire, err := proto.Marshal(base) + if err != nil { + t.Fatal(err) + } + unknown := protowire.AppendTag(nil, 500, protowire.VarintType) + unknown = protowire.AppendVarint(unknown, 7) + var newer rampv1.Offer + if err := proto.Unmarshal(append(wire, unknown...), &newer); err != nil { + t.Fatal(err) + } + if len(newer.ProtoReflect().GetUnknown()) == 0 { + t.Fatal("fixture is inert: the unknown field did not survive unmarshal") + } + + baseCanon, err := helpers.CanonicalOfferBytes(base) + if err != nil { + t.Fatal(err) + } + newerCanon, err := helpers.CanonicalOfferBytes(&newer) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(baseCanon, newerCanon) { + t.Fatalf("unknown fields must not reach the canonical bytes\n got %s\n want %s", + newerCanon, baseCanon) + } + + // What the newer peer signed: its own canonical rendering, which includes the + // field it knows about. JCS sorts keys, so a member sorting last is appended + // before the closing brace. + peerSigned := append(baseCanon[:len(baseCanon)-1:len(baseCanon)-1], []byte(`,"zz_new_field":"7"}`)...) + peerSig := hex.EncodeToString(ed25519.Sign(priv, peerSigned)) + + if err := helpers.VerifyOffer(&newer, peerSig, pub); !errors.Is(err, helpers.ErrOfferSignatureInvalid) { + t.Errorf("signature over a newer peer's canonical form must be rejected, got %v", err) + } +} From 54987c5073a72ade0143dfb7aeefe5bfded2b05a Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 24 Jul 2026 17:19:31 +0200 Subject: [PATCH 14/21] feat(sdk-go): reject messages carrying unknown fields before canonical signing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit proto-JSON renders only what the schema defines, so a message carrying unknown fields canonicalizes to bytes that silently drop them. Both directions of that gap were wrong, and only one of them was ever acknowledged: a signer built against a newer schema covered more than this build can reconstruct (rejected, but for a misleading reason), and an on-path party could APPEND unknown fields to an already-signed Offer without disturbing its signature — unauthenticated bytes riding on a message the recipient treats as verified, surviving a proto.Marshal round-trip onward. canonicalSignPayload now refuses such a message. The walk is written out rather than delegated to a traversal helper: this is a signature-coverage guard, so its reachability rules belong where they can be audited. Unknown-field sets are per message, so it visits nested messages and every element of a repeated or map field — a top-level check alone passes a payload whose tampering sits one level down. VerifyOffer surfaces the refusal as ErrOfferSignatureInvalid rather than propagating it raw: callers branch on that sentinel to map a rejection to its denial reason, and a message that arrived carrying extra bytes is a tampered offer, not an internal fault. SignOffer and CanonicalOfferBytes return it directly — signing or persisting such a message is a caller bug. No legitimate traffic changes behaviour. An offer signed WITH a field this build cannot render already failed to verify, because the renderer dropped it and the bytes differed; the refusal only makes the reason explicit. Python and TypeScript already rejected the same input, so this closes a Go-only divergence. The godoc that called this area "fail-closed, never fail-open" was accurate for the newer-signer direction only, and the test it pointed at pinned the byte equality that made the injection direction fail OPEN. Both are replaced: the tests now build a clean and a tampered offer that render identically, at three depths, so a rejection can only come from the unknown field itself. The paired accessors also stated the pinned option set with different clause lists; they now read identically. --- sdk/go/helpers/acceptance.go | 4 + sdk/go/helpers/canonicalsign.go | 74 ++++++++++++++ sdk/go/helpers/offer.go | 26 +++-- sdk/go/helpers/offer_test.go | 172 ++++++++++++++++++++++++++------ 4 files changed, 237 insertions(+), 39 deletions(-) diff --git a/sdk/go/helpers/acceptance.go b/sdk/go/helpers/acceptance.go index a8a7b1e..25de9db 100644 --- a/sdk/go/helpers/acceptance.go +++ b/sdk/go/helpers/acceptance.go @@ -56,6 +56,10 @@ var ErrAcceptanceSignatureInvalid = errors.New("helpers: offer-acceptance signat // render. Any language (Go/TS/Python) reproduces the exact bytes from that // definition, without a protobuf binary codec. // +// The payload is built here from the four scalars rather than taken from the caller, +// so it cannot carry the unknown fields CanonicalOfferBytes has to refuse; only the +// values read off the offer and requester reach the signed bytes. +// // Unpopulated fields are OMITTED before JCS — EVERY empty string field, not just the // domain: the bytes for an empty requester id are not the bytes for any populated // one. A port that assembles this object by hand instead of rendering the proto must diff --git a/sdk/go/helpers/canonicalsign.go b/sdk/go/helpers/canonicalsign.go index 157cd1a..b42fc01 100644 --- a/sdk/go/helpers/canonicalsign.go +++ b/sdk/go/helpers/canonicalsign.go @@ -1,11 +1,13 @@ package helpers import ( + "errors" "fmt" "github.com/gowebpki/jcs" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" ) // Canonical signing payload (ADR-020 §4, ramp.proto "canonical signing" note). @@ -47,10 +49,82 @@ var canonicalSignJSONOptions = protojson.MarshalOptions{ EmitUnpopulated: false, // omit unpopulated fields } +// errUnknownFields refuses a message the canonical form cannot faithfully +// represent. proto-JSON renders only fields the schema defines, so a message +// carrying unknown ones canonicalizes to bytes that silently omit them. Both +// directions of that gap are wrong: a signer built against a newer schema +// covered more than this build can reconstruct, and an on-path party that +// appends unknown fields to an already-signed message would otherwise leave the +// signature verifying over bytes it never saw. Refusing the message is what +// makes the canonical form's coverage total rather than schema-relative. +var errUnknownFields = errors.New("helpers: message carries unknown fields; canonical bytes cannot represent them") + +// hasUnknownFields reports whether msg, or anything reachable from it, carries +// fields this build's schema does not define. +// +// The walk is explicit rather than delegated to a traversal helper: this is a +// signature-coverage guard, so its reachability rules are stated here where they +// can be audited against the message tree. Unknown-field sets are PER MESSAGE — +// a nested message and every element of a repeated or map field owns its own — +// so a top-level check alone would pass a payload whose tampering sits one level +// down. +func hasUnknownFields(msg proto.Message) bool { + if msg == nil { + return false + } + return messageHasUnknown(msg.ProtoReflect()) +} + +func messageHasUnknown(m protoreflect.Message) bool { + if !m.IsValid() { + return false // a typed nil carries nothing + } + if len(m.GetUnknown()) > 0 { + return true + } + found := false + m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + switch { + case fd.IsMap(): + // Map KEYS are scalars; only a message-typed value can nest one. + if fd.MapValue().Kind() == protoreflect.MessageKind || + fd.MapValue().Kind() == protoreflect.GroupKind { + v.Map().Range(func(_ protoreflect.MapKey, mv protoreflect.Value) bool { + if messageHasUnknown(mv.Message()) { + found = true + } + return !found + }) + } + case fd.IsList(): + if fd.Kind() == protoreflect.MessageKind || fd.Kind() == protoreflect.GroupKind { + list := v.List() + for i := 0; i < list.Len() && !found; i++ { + if messageHasUnknown(list.Get(i).Message()) { + found = true + } + } + } + case fd.Kind() == protoreflect.MessageKind, fd.Kind() == protoreflect.GroupKind: + if messageHasUnknown(v.Message()) { + found = true + } + } + return !found // stop the range as soon as one is found + }) + return found +} + // canonicalSignPayload renders msg to canonical proto-JSON (pinned options) and // applies RFC 8785 JCS, returning the exact UTF-8 bytes an Ed25519 signature // covers. Callers clear signature/signature_algorithm on a clone BEFORE calling. +// +// A message carrying unknown fields is refused rather than rendered — see +// errUnknownFields. func canonicalSignPayload(msg proto.Message) ([]byte, error) { + if hasUnknownFields(msg) { + return nil, errUnknownFields + } pj, err := canonicalSignJSONOptions.Marshal(msg) if err != nil { return nil, fmt.Errorf("helpers: canonical proto-JSON marshal: %w", err) diff --git a/sdk/go/helpers/offer.go b/sdk/go/helpers/offer.go index a303694..67c689f 100644 --- a/sdk/go/helpers/offer.go +++ b/sdk/go/helpers/offer.go @@ -55,6 +55,13 @@ func VerifyOffer(offer *rampv1.Offer, signatureHex string, pub ed25519.PublicKey } payload, err := CanonicalOfferBytes(offer) if err != nil { + if errors.Is(err, errUnknownFields) { + // An offer carrying fields this build cannot render is refused on the + // SAME path as a forged one. Callers branch on the sentinel to map a + // rejection to its denial reason, and "someone appended bytes to a + // signed offer" is a signature failure, not an internal fault. + return fmt.Errorf("%w: %v", ErrOfferSignatureInvalid, err) + } return err } if !ed25519.Verify(pub, payload, sig) { @@ -80,19 +87,22 @@ func VerifyOffer(offer *rampv1.Offer, signatureHex string, pub ed25519.PublicKey // The canonical form is RFC 8785 JCS over canonical proto-JSON — // JCS(protojson(offer with sig cleared)) — rendered under the option set the // Offer.signature comment in ramp.proto defines normatively: snake_case proto field -// names, enums as name strings, unpopulated fields omitted, 64-bit ints as decimal -// strings. That definition, not this implementation, is what lets any language -// (Go/TS/Python) reproduce the exact signed bytes without a protobuf binary codec. +// names, enums as name strings, unpopulated fields omitted. That definition, not +// this implementation, is what lets any language (Go/TS/Python) reproduce the exact +// signed bytes without a protobuf binary codec. // // expires_at is covered (only signature/signature_algorithm are cleared), so a // relaying Broker cannot extend or shorten a signed offer's TTL under an // otherwise-valid signature. // -// KNOWN LIMIT (fail-closed, never fail-open): a field a newer peer set that this -// build's generated types do not know arrives as an unknown field, and proto-JSON -// does not render unknown fields — it is absent from the returned bytes. A signature -// that covered it therefore fails to verify, rather than verifying over a message -// silently missing part of what was signed. +// An Offer carrying UNKNOWN fields — at any depth, including inside a nested message +// or a repeated element — is REFUSED rather than rendered. proto-JSON emits only +// what the schema defines, so those bytes would silently drop the unknown content: +// a peer built against a newer schema would have signed more than this build can +// reconstruct, and an intermediary could otherwise append fields to a signed Offer +// without disturbing its signature. VerifyOffer surfaces the refusal as +// ErrOfferSignatureInvalid, since a message that arrived carrying extra bytes is a +// tampered Offer, not an internal fault. func CanonicalOfferBytes(offer *rampv1.Offer) ([]byte, error) { if offer == nil { return nil, errors.New("helpers: offer is nil") diff --git a/sdk/go/helpers/offer_test.go b/sdk/go/helpers/offer_test.go index e1ee612..9bc9a7c 100644 --- a/sdk/go/helpers/offer_test.go +++ b/sdk/go/helpers/offer_test.go @@ -176,50 +176,160 @@ func TestCanonicalOfferBytes_nil(t *testing.T) { } } -func TestCanonicalOfferBytes_unknownFieldsFailClosed(t *testing.T) { - // An Offer from a peer built against a newer schema carries fields this build's - // generated types do not know. proto.Unmarshal keeps them as unknown fields, but - // proto-JSON does not render them — so they are absent from the canonical bytes. - // The direction that matters is which way the gap fails: the peer's signature - // covered MORE than we can reconstruct, so it must be rejected, never accepted - // over the truncated message. - pub, priv, _ := ed25519.GenerateKey(nil) - base := sampleOffer() - - wire, err := proto.Marshal(base) +// encodeWithUnknownField encodes msg and appends a field number no RAMP message +// defines — what a peer built against a newer schema emits, and equally what an +// on-path party appends to a message it did not author. +func encodeWithUnknownField(t *testing.T, msg proto.Message) []byte { + t.Helper() + raw, err := proto.Marshal(msg) if err != nil { t.Fatal(err) } - unknown := protowire.AppendTag(nil, 500, protowire.VarintType) - unknown = protowire.AppendVarint(unknown, 7) - var newer rampv1.Offer - if err := proto.Unmarshal(append(wire, unknown...), &newer); err != nil { - t.Fatal(err) + raw = protowire.AppendTag(raw, 500, protowire.VarintType) + return protowire.AppendVarint(raw, 7) +} + +// embed appends msg's encoding to host as a length-delimited field, so an +// unknown field can be planted at a chosen depth of the Offer tree. +func embed(t *testing.T, host []byte, field protowire.Number, encoded []byte) []byte { + t.Helper() + host = protowire.AppendTag(host, field, protowire.BytesType) + return protowire.AppendBytes(host, encoded) +} + +// unknownFieldCase pairs an Offer with the SAME Offer carrying one unknown field. +// The two differ by nothing a renderer can see, which is the whole point: any +// rejection has to come from the unknown field itself, not from a visible +// difference that would break the signature anyway. +type unknownFieldCase struct { + clean *rampv1.Offer + tampered *rampv1.Offer +} + +// unknownFieldCases plants an unknown field at three depths: on the Offer itself, +// inside the nested Pricing message, and inside one element of the repeated +// attestations list. Unknown-field sets are per-message, so a guard that only +// checks the top level passes the first and misses the other two. +func unknownFieldCases(t *testing.T) map[string]unknownFieldCase { + t.Helper() + + decode := func(raw []byte) *rampv1.Offer { + var o rampv1.Offer + if err := proto.Unmarshal(raw, &o); err != nil { + t.Fatal(err) + } + return &o } - if len(newer.ProtoReflect().GetUnknown()) == 0 { - t.Fatal("fixture is inert: the unknown field did not survive unmarshal") + encode := func(m proto.Message) []byte { + raw, err := proto.Marshal(m) + if err != nil { + t.Fatal(err) + } + return raw } - baseCanon, err := helpers.CanonicalOfferBytes(base) - if err != nil { - t.Fatal(err) + out := map[string]unknownFieldCase{} + + out["top level"] = unknownFieldCase{ + clean: sampleOffer(), + tampered: decode(encodeWithUnknownField(t, sampleOffer())), } - newerCanon, err := helpers.CanonicalOfferBytes(&newer) - if err != nil { - t.Fatal(err) + + // Rebuild the offer from a shell plus a hand-encoded Pricing so the unknown + // field lands inside the nested message. The shell carries every other field, + // so clean and tampered render identically. + base := sampleOffer() + shell := encode(&rampv1.Offer{OfferId: base.OfferId, ExpiresAt: base.ExpiresAt}) + out["nested message"] = unknownFieldCase{ + clean: decode(embed(t, shell, offerPricingField, encode(base.Pricing))), + tampered: decode(embed(t, shell, offerPricingField, encodeWithUnknownField(t, base.Pricing))), } - if !bytes.Equal(baseCanon, newerCanon) { - t.Fatalf("unknown fields must not reach the canonical bytes\n got %s\n want %s", - newerCanon, baseCanon) + + // The attestation must be present in BOTH, or the tampered offer would differ + // visibly and be rejected on a byte mismatch — passing the test for a reason + // that has nothing to do with the unknown field. + full := encode(sampleOffer()) + att := &rampv1.ResourceAttestation{Verifier: "verifier.example.com", Uri: "https://example.com/a"} + out["repeated element"] = unknownFieldCase{ + clean: decode(embed(t, full, offerAttestationsField, encode(att))), + tampered: decode(embed(t, full, offerAttestationsField, encodeWithUnknownField(t, att))), } - // What the newer peer signed: its own canonical rendering, which includes the - // field it knows about. JCS sorts keys, so a member sorting last is appended - // before the closing brace. + return out +} + +// Offer field numbers used to plant an unknown field at depth. Kept as named +// constants so a proto renumbering breaks the fixture loudly instead of quietly +// planting the field somewhere else. +const ( + offerPricingField protowire.Number = 3 + offerAttestationsField protowire.Number = 14 +) + +func TestCanonicalOfferBytes_refusesUnknownFields(t *testing.T) { + // proto-JSON renders only what the schema defines, so a message carrying + // unknown fields would canonicalize to bytes that silently drop them. Producing + // those bytes is refused outright: they are not the bytes anyone signed, and + // handing them back as "the canonical form" is what would let an intermediary + // append content to a signed offer without disturbing its signature. + for name, c := range unknownFieldCases(t) { + t.Run(name, func(t *testing.T) { + if _, err := helpers.CanonicalOfferBytes(c.clean); err != nil { + t.Fatalf("control: the same offer without the unknown field must canonicalize: %v", err) + } + if _, err := helpers.CanonicalOfferBytes(c.tampered); err == nil { + t.Error("an offer carrying unknown fields must not yield canonical bytes") + } + }) + } +} + +func TestVerifyOffer_rejectsInjectedUnknownFields(t *testing.T) { + // The tamper case: a legitimately signed offer picks up unknown fields in + // transit. The signature still matches the fields this build can render, so + // only the refusal above stops it from verifying. It must land on the signature + // sentinel — callers map that to a signature-invalid denial, and an appended + // payload is a tampered offer, not an internal fault. + pub, priv, _ := ed25519.GenerateKey(nil) + for name, c := range unknownFieldCases(t) { + t.Run(name, func(t *testing.T) { + // Sign the CLEAN offer, then verify the tampered one. The signature is + // genuine and the two render identically, so the unknown field is the + // only thing that can cause a rejection. + sigHex, err := helpers.SignOffer(priv, c.clean) + if err != nil { + t.Fatal(err) + } + if err := helpers.VerifyOffer(c.clean, sigHex, pub); err != nil { + t.Fatalf("control: the clean offer must verify: %v", err) + } + if err := helpers.VerifyOffer(c.tampered, sigHex, pub); !errors.Is(err, helpers.ErrOfferSignatureInvalid) { + t.Errorf("want ErrOfferSignatureInvalid, got %v", err) + } + }) + } +} + +func TestVerifyOffer_rejectsSignatureOverANewerCanonicalForm(t *testing.T) { + // The other direction: the peer signed its OWN richer rendering, which includes + // the field it knows about and this build does not. The signed bytes cannot be + // reconstructed here at all, so the offer is rejected rather than verified over + // a reduced form. + pub, priv, _ := ed25519.GenerateKey(nil) + base := sampleOffer() + baseCanon, err := helpers.CanonicalOfferBytes(base) + if err != nil { + t.Fatal(err) + } + // JCS sorts keys, so a member sorting last is appended before the closing brace. peerSigned := append(baseCanon[:len(baseCanon)-1:len(baseCanon)-1], []byte(`,"zz_new_field":"7"}`)...) peerSig := hex.EncodeToString(ed25519.Sign(priv, peerSigned)) + var newer rampv1.Offer + if err := proto.Unmarshal(encodeWithUnknownField(t, base), &newer); err != nil { + t.Fatal(err) + } if err := helpers.VerifyOffer(&newer, peerSig, pub); !errors.Is(err, helpers.ErrOfferSignatureInvalid) { - t.Errorf("signature over a newer peer's canonical form must be rejected, got %v", err) + t.Errorf("want ErrOfferSignatureInvalid, got %v", err) } } From f846ec36115e1a37e125fd61043e946388aab854 Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 24 Jul 2026 17:19:40 +0200 Subject: [PATCH 15/21] docs(proto): make unknown-field rejection part of the canonical signing definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK guard is worth little if only this SDK applies it: a conforming verifier written from the spec would still verify a signature over a message an intermediary had appended unknown fields to. The rule now sits where the canonical form is defined, on Offer.signature, so it binds every implementation and both signed payloads rather than one language's helpers. It states the requirement at every depth — a nested message and each element of a repeated or map field carries its own unknown-field set — and names the reason: proto-JSON emits only what the schema defines, so bytes reconstructed from such a message omit part of what the signer covered. It also draws the line against the extension mechanism, which is unaffected: ext and ext_critical are declared fields inside the signed bytes, never undeclared field numbers. Comments only — no field, message, or wire change. gen/ regenerated with the pinned buf 1.66.1 (descriptor, Go types, and the Pydantic/Zod descriptions the proto comments feed), and the changelog mirrored to the website. The reference page renders the Offer table from the committed descriptor, so it picks the text up without a hand edit. --- gen/descriptor.binpb | Bin 539813 -> 540739 bytes gen/go/ramp/v1/ramp.pb.go | 13 ++++++++++ gen/python/wire/models.py | 2 +- gen/ts/wire/schemas.ts | 12 ++++----- proto/CHANGELOG.md | 23 ++++++++++++++++++ proto/ramp/v1/ramp.proto | 13 ++++++++++ .../src/content/docs/reference/changelog.mdx | 23 ++++++++++++++++++ 7 files changed, 79 insertions(+), 7 deletions(-) diff --git a/gen/descriptor.binpb b/gen/descriptor.binpb index 199e2cbdadce8706828f4e4d48e5247ad7ff5b1d..7b390007425296f46919ad235546b5146f366789 100644 GIT binary patch delta 34896 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 diff --git a/gen/go/ramp/v1/ramp.pb.go b/gen/go/ramp/v1/ramp.pb.go index 1e95e68..0d32269 100644 --- a/gen/go/ramp/v1/ramp.pb.go +++ b/gen/go/ramp/v1/ramp.pb.go @@ -2556,6 +2556,19 @@ type Offer struct { // - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its // keys recursively, so the Struct case needs no special handling. // + // UNKNOWN FIELDS: a message carrying fields the renderer's schema does not + // define MUST NOT be canonicalized, and a verifier MUST reject it rather than + // verify over the reduced bytes. proto-JSON emits only what the schema defines, + // so the bytes reconstructed from such a message silently omit part of what the + // signer covered. The rule binds at EVERY depth — a nested message and each + // element of a repeated or map field carries its own unknown-field set. Without + // it the omission cuts both ways: a signer built against a newer schema would be + // rejected for the wrong reason, and an intermediary could APPEND unknown fields + // to an already-signed message without invalidating its signature, smuggling + // unauthenticated content through a message the recipient treats as verified. + // Extensions therefore ride in `ext` / `ext_critical`, which are defined fields + // and inside the signed bytes — never as undeclared field numbers. + // // Because the signature covers `terms`, `pricing`, `expires_at`, and // `exchange`, an intermediary (Broker) cannot tamper with price, restrictions, // quotas, obligations, the expiry, the execute-routing target, or any diff --git a/gen/python/wire/models.py b/gen/python/wire/models.py index b743b11..cf8776e 100644 --- a/gen/python/wire/models.py +++ b/gen/python/wire/models.py @@ -1630,7 +1630,7 @@ class Offer(WireModel): ) signature: str | None = Field( '', - description="CANONICAL 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 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.", + description="CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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.", ) signature_algorithm: str | None = Field( '', diff --git a/gen/ts/wire/schemas.ts b/gen/ts/wire/schemas.ts index 7fa5d85..19d5cab 100644 --- a/gen/ts/wire/schemas.ts +++ b/gen/ts/wire/schemas.ts @@ -40,7 +40,7 @@ export const DiscoveryMethodSchema = wire(z.enum(["DISCOVERY_METHOD_EXCHANGE","D export const DiscoveryRequestSchema = wire(z.object({ "acceptable_restrictions": z.array(z.object({ "axis": z.union([z.string().regex(new RegExp("^RESTRICTION_KIND_UNSPECIFIED$")), z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("Which axis (same enum as Restriction.kind): FUNCTION / GEOGRAPHY /\n USER_TYPE / OTHER.").default(0), "values": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("The values the query operates within on this axis — same token vocabulary\n as the terms (e.g. FUNCTION [\"ai-train\"], GEOGRAPHY [\"US\", \"EU\"]).").optional() }).describe("AcceptableRestriction — the limits a query operates within on one restriction\n axis, expressed in the same RestrictionKind vocabulary that terms use. The\n Exchange/Broker MAY pre-select offers whose term restrictions fall within\n these as a convenience (see Restriction); it is NOT enforcement — the agent\n self-selects and bears compliance.")).describe("The limits the agent will operate within, per restriction axis — see\n AcceptableRestriction. The Broker forwards these to Exchanges in\n ResourceQuery.acceptable_restrictions. Advisory selection inputs, not\n enforcement.").optional(), "constraints": z.object({ "budget_period": z.string().describe("Budget period (e.g. \"2592000s\" = 30 days; proto-JSON encodes Duration\n as seconds). Resets at period boundary.").optional(), "budget_scope": z.string().describe("Budget scope identifier for per-period tracking.\n E.g. \"user:u-12345\" for per-user budgets, \"team:eng\" for per-team.\n The Broker tracks cumulative spend per scope across sessions.").optional(), "delivery_preference": z.array(z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"])).describe("Preferred delivery methods, in order of preference.").optional(), "exchanges": z.array(z.string()).describe("Authorized Exchange domains. Broker queries only these.").optional(), "max_data_age": z.string().describe("Only relevant for DYNAMIC resources. Ignored for STATIC (content is\n immutable) and LIVE (content doesn't exist yet).\n\n Examples:\n 7 days — \"credit report updated within the last week\"\n 1 hour — \"stock snapshot from the last hour\"\n 30 days — \"drug interaction database updated this month\"").optional(), "max_hops": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum forwarding hops the agent will allow (Agent → Broker → … →\n Exchange), counted as the number of RFC 9421 HTTP Message Signatures on the\n request. Caps chain depth so a request is not relayed through more brokers\n than the agent is willing to trust or pay. A Broker MUST NOT forward a\n request whose signature count would exceed this. Absent = agent imposes no\n cap (the Exchange's max_intermediary_hops still applies).").optional(), "max_price": 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("Maximum price the agent is willing to pay.").optional(), "max_unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Maximum effective cost per unit, as an exact decimal string (not a float).").optional(), "period_budget": 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("Per-period budget limit. The Broker tracks spend against this\n for the budget_scope. Transactions that would exceed are denied.").optional(), "preferred_exchanges": z.array(z.string()).describe("Exchanges the agent has existing relationships with (subscriptions,\n contracts). The Broker SHOULD prefer these when resource is\n available — subscription resource has zero marginal cost.").optional(), "reporting_capable": z.boolean().describe("Whether the agent supports post-usage reporting.").optional() }).describe("Constraints for exchange filtering and offer selection.").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(), "query": z.string().describe("Search query for Broker-side resource discovery.\n Used when the agent doesn't know specific URIs but wants the Broker\n to find matching resources across Exchanges.\n When present, the Broker interprets the query and discovers resources\n across Exchanges on the agent's behalf. Results returned as Offers\n in DiscoveryResponse, same as for specific URI requests.\n Can be used alongside uris (specific URIs + search in one request).").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().describe("Domain the requester belongs to — used for public key lookup.\n Keys published at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT).").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(), "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("The 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 — who is making this request, what scopes they have.\n The Broker forwards this to Exchanges in ResourceQuery.requester.").optional(), "search_filters": z.record(z.string(), z.any()).describe("Structured search filters (optional, alongside or instead of query).\n Keys are profile-specific: \"academic.topic\", \"news.category\",\n \"legal.jurisdiction\", etc. The Broker maps these to Exchange-specific\n query parameters.").optional(), "supported_profiles": z.array(z.string()).describe("The Broker uses this to:\n 1. Route queries to Exchanges that support these profiles\n 2. Forward the profiles in ResourceQuery.supported_profiles\n 3. Include profile-specific ext fields when returning results\n\n Examples: [\"ramp-academic-v1\"] — agent working on literature review").optional(), "uris": z.array(z.string()).max(256).describe("Resource URIs the agent wants. The Broker forwards these to Exchanges in\n ResourceQuery.uris. Optional when `query` / `search_filters` drive\n Broker-side discovery instead.").optional(), "ver": z.string().describe("RAMP protocol version").default("") }).describe("DiscoveryRequest — Agent sends to Broker (Step 1).")); -export const DiscoveryResponseSchema = wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Existence-oracle note: an authorization-flavored reason (SCOPE_INSUFFICIENT,\n NOT_AUTHORIZED, NOT_IN_CATALOG, CONTENT_BLOCKED) confirms a resource exists\n and why access was refused. Resolve surfaces the same oracle at the broker\n that OfferGroup.absence_reason does at the Exchange, so the same mitigation\n applies: where existence itself must stay hidden, the Broker MAY omit the\n reason (leave this unset) rather than reveal it. See the threat model.").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(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI — the sole offer representation in this\n response. One OfferGroup per URI the agent asked for (echoed in\n OfferGroup.uri); a group with no offers carries OfferGroup.absence_reason\n explaining why. Each contained Offer is the full signed Offer the Exchange\n issued (including Offer.exchange, the execute-routing target), forwarded by\n the Broker unchanged so the agent can verify the signature end to end.").optional(), "ver": z.string().default("") }).describe("Carries discovery results only: the offers the Broker gathered across\n Exchanges, grouped by the URI they were requested for. Committing to an offer\n is a separate exchange on the execute path; that per-transaction result\n (transaction_id, billing_id, cost, delivery_method, retrieval endpoint, …)\n is returned by TransactionResponse, not here.")); +export const DiscoveryResponseSchema = wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Existence-oracle note: an authorization-flavored reason (SCOPE_INSUFFICIENT,\n NOT_AUTHORIZED, NOT_IN_CATALOG, CONTENT_BLOCKED) confirms a resource exists\n and why access was refused. Resolve surfaces the same oracle at the broker\n that OfferGroup.absence_reason does at the Exchange, so the same mitigation\n applies: where existence itself must stay hidden, the Broker MAY omit the\n reason (leave this unset) rather than reveal it. See the threat model.").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(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI — the sole offer representation in this\n response. One OfferGroup per URI the agent asked for (echoed in\n OfferGroup.uri); a group with no offers carries OfferGroup.absence_reason\n explaining why. Each contained Offer is the full signed Offer the Exchange\n issued (including Offer.exchange, the execute-routing target), forwarded by\n the Broker unchanged so the agent can verify the signature end to end.").optional(), "ver": z.string().default("") }).describe("Carries discovery results only: the offers the Broker gathered across\n Exchanges, grouped by the URI they were requested for. Committing to an offer\n is a separate exchange on the execute path; that per-transaction result\n (transaction_id, billing_id, cost, delivery_method, retrieval endpoint, …)\n is returned by TransactionResponse, not here.")); export const DisputeFailureSchema = wire(z.object({ "reason": z.enum(["DISPUTE_FAILURE_REASON_TRANSACTION_NOT_FOUND","DISPUTE_FAILURE_REASON_REPORT_NOT_FILED","DISPUTE_FAILURE_REASON_WINDOW_EXPIRED","DISPUTE_FAILURE_REASON_DUPLICATE","DISPUTE_FAILURE_REASON_INELIGIBLE"]).describe("The failure reason (defined-only, non-zero)") }).describe("DisputeFailure — a dispute could not be filed.")); @@ -88,11 +88,11 @@ export const ObligationKindSchema = wire(z.enum(["OBLIGATION_KIND_ATTRIBUTION"," export const ObligationTriggerSchema = wire(z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"])); -export const OfferSchema = wire(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")); +export const OfferSchema = wire(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")); export const OfferAbsenceReasonSchema = wire(z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"])); -export const OfferGroupSchema = wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")); +export const OfferGroupSchema = wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")); export const PreviewSchema = wire(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("The 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.")); @@ -152,7 +152,7 @@ export const ResourceMutabilitySchema = wire(z.enum(["RESOURCE_MUTABILITY_STATIC export const ResourceQuerySchema = wire(z.object({ "acceptable_restrictions": z.array(z.object({ "axis": z.union([z.string().regex(new RegExp("^RESTRICTION_KIND_UNSPECIFIED$")), z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("Which axis (same enum as Restriction.kind): FUNCTION / GEOGRAPHY /\n USER_TYPE / OTHER.").default(0), "values": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("The values the query operates within on this axis — same token vocabulary\n as the terms (e.g. FUNCTION [\"ai-train\"], GEOGRAPHY [\"US\", \"EU\"]).").optional() }).describe("AcceptableRestriction — the limits a query operates within on one restriction\n axis, expressed in the same RestrictionKind vocabulary that terms use. The\n Exchange/Broker MAY pre-select offers whose term restrictions fall within\n these as a convenience (see Restriction); it is NOT enforcement — the agent\n self-selects and bears compliance.")).describe("The limits this query operates within, per restriction axis (function,\n geography, user-type, …) — see AcceptableRestriction. Advisory selection\n inputs the Exchange/Broker MAY pre-select offers against (convenience, not\n enforcement); the agent self-selects and bears compliance.").optional(), "deadline": z.string().describe("Maximum time the caller will wait for a response.\n Exchange SHOULD prioritize speed over completeness when tight.\n Absent = \"0.5s\" default (proto-JSON encodes Duration as seconds).").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(), "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().describe("Domain the requester belongs to — used for public key lookup.\n Keys published at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT).").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(), "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("The 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 — who is making this request, what scopes they have,\n and optional delegation chain.").optional(), "supported_profiles": z.array(z.string()).describe("Declares which ext field vocabularies the caller can parse and act on.\n The Exchange SHOULD include profile-specific ext fields in Offers\n when the caller declares support. The Exchange MAY skip expensive\n metadata computation (e.g., retraction checking, consolidation\n verification) when the caller does not declare the relevant profile.\n\n Absence means \"send all available metadata\" — Exchange MUST NOT\n withhold ext fields solely because the caller omitted this field.\n\n Values match the Exchange's WellKnownManifest.supported_profiles entries.\n Examples: [\"ramp-news-v1\", \"ramp-academic-v1\", \"ramp-legal-v1\"]").optional(), "uris": z.array(z.string()).max(256).describe("Resource URIs being queried.").optional(), "ver": z.string().describe("RAMP protocol version.").default("") }).describe("Sent by a Broker or directly by an AI agent.\n The Exchange evaluates its access policies, available inventory,\n and reporting requirements before responding.")); -export const ResourceResponseSchema = wire(z.object({ "exchange": z.string().describe("Canonical domain of the responding Exchange.").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(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI (for multi-URI batch queries).\n When populated, `offers` SHOULD be empty to avoid ambiguity.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Flat list of offers (for single-URI queries).").optional(), "rate_limit": z.object({ "limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum requests allowed in the current window.").optional(), "remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Requests remaining in the current window.").optional(), "reset_at": z.string().datetime({ offset: true }).describe("When the current window resets (UTC). After this time, `remaining` resets to `limit`.").optional(), "window": z.string().describe("Duration of the rate limit window (e.g. 60s = per-minute limit).").optional() }).describe("Rate limit status for this caller.\n Present when the Exchange enforces per-caller rate limits on discovery.\n Enables agents/Brokers to throttle proactively rather than hitting\n hard limits. Particularly important when a Broker fans out the\n same batch query to multiple Exchanges — mid-batch rate limiting\n can cause partial results if not signaled early.").optional(), "ver": z.string().describe("Protocol version").default("") }).describe("When the ResourceQuery contains multiple URIs, offers are grouped by URI\n via OfferGroup. When a single URI is queried, the Exchange MAY use\n either the flat `offers` field or a single OfferGroup.")); +export const ResourceResponseSchema = wire(z.object({ "exchange": z.string().describe("Canonical domain of the responding Exchange.").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(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI (for multi-URI batch queries).\n When populated, `offers` SHOULD be empty to avoid ambiguity.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Flat list of offers (for single-URI queries).").optional(), "rate_limit": z.object({ "limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum requests allowed in the current window.").optional(), "remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Requests remaining in the current window.").optional(), "reset_at": z.string().datetime({ offset: true }).describe("When the current window resets (UTC). After this time, `remaining` resets to `limit`.").optional(), "window": z.string().describe("Duration of the rate limit window (e.g. 60s = per-minute limit).").optional() }).describe("Rate limit status for this caller.\n Present when the Exchange enforces per-caller rate limits on discovery.\n Enables agents/Brokers to throttle proactively rather than hitting\n hard limits. Particularly important when a Broker fans out the\n same batch query to multiple Exchanges — mid-batch rate limiting\n can cause partial results if not signaled early.").optional(), "ver": z.string().describe("Protocol version").default("") }).describe("When the ResourceQuery contains multiple URIs, offers are grouped by URI\n via OfferGroup. When a single URI is queried, the Exchange MAY use\n either the flat `offers` field or a single OfferGroup.")); export const RestrictionSchema = wire(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."), "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("Restrictions 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")); @@ -180,9 +180,9 @@ export const TermSemanticsSchema = wire(z.enum(["TERM_SEMANTICS_ENUMERATED","TER export const TransactionDenialSchema = wire(z.object({ "offer_id": z.string().describe("Batch mode: the offer this denial pertains to.").optional(), "reason": z.enum(["DENIAL_REASON_BILLING_REF_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"]).describe("The denial reason (defined-only, non-zero)"), "restriction_mismatches": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When reason = RESTRICTION_NOT_SATISFIED, the failed axes (same\n RestrictionKind vocabulary the terms use).").optional() }).describe("TransactionDenial — ExecuteTransaction could not complete. Carries the denial\n reason the response body no longer holds (denial_reason / restriction_mismatches\n move here in the response-shape normalization). Reuses the DenialReason vocab.")); -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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).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 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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).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().describe("Domain the requester belongs to — used for public key lookup.\n Keys published at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT).").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(), "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("The 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("Protocol version").default("") }).describe("After 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({ "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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).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().describe("Domain the requester belongs to — used for public key lookup.\n Keys published at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT).").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(), "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("The 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("Protocol version").default("") }).describe("After 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_BILLING_REF_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"]).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("Analogous 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("Protocol version").default("") }).describe("Items-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 23c6743..ae1ccfb 100644 --- a/proto/CHANGELOG.md +++ b/proto/CHANGELOG.md @@ -96,6 +96,29 @@ string field, and a new `empty_requester_id` vector in TS. The corpus change is purely additive — the pre-existing vectors and their signatures are byte-identical, so no already-issued signature is affected. +**Canonical signing rejects messages carrying unknown fields (normative; Go SDK +behaviour change, no wire change).** A message carrying fields the renderer's schema +does not define MUST NOT be canonicalized, and a verifier MUST reject it rather than +verify over the reduced bytes. The rule is stated on `Offer.signature`, the single +normative definition of the canonical form, and binds at EVERY depth — a nested message +and each element of a repeated or map field carries its own unknown-field set. proto-JSON +emits only what the schema defines, so bytes reconstructed from such a message silently +omit part of what the signer covered. Left unchecked the omission cuts both ways: a signer +built against a newer schema is rejected for the wrong reason, and an intermediary could +APPEND unknown fields to an already-signed `Offer` **without invalidating its signature**, +smuggling unauthenticated content through a message the recipient treats as verified. +`helpers.VerifyOffer` surfaces the refusal as `ErrOfferSignatureInvalid` — a message that +arrived carrying extra bytes is a tampered offer, not an internal fault — while +`helpers.CanonicalOfferBytes` and `helpers.SignOffer` return it directly. No legitimate +traffic regresses: an offer signed WITH a field this build cannot render already failed to +verify, because the renderer dropped it and the bytes differed; the refusal only makes the +reason explicit. Python (`from_wire_offer`) and TypeScript (`canonicalOfferPayload`) +already rejected such input — both keep unrecognized members, so their bytes differ — so +this brings Go into line rather than moving the cross-language contract. Extensions are +unaffected: they ride in `ext` / `ext_critical`, defined fields that sit inside the signed +bytes, never undeclared field numbers. No new exported symbol; no field, message, or wire +change — proto comments only, with `gen/` and the website mirror regenerated. + **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written billing label on `Requester` is gone; the field is deleted outright with no `reserved` statement — pre-v1 the number returns to the free pool, and diff --git a/proto/ramp/v1/ramp.proto b/proto/ramp/v1/ramp.proto index 5eaca58..7c3fa3c 100644 --- a/proto/ramp/v1/ramp.proto +++ b/proto/ramp/v1/ramp.proto @@ -475,6 +475,19 @@ message Offer { // - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its // keys recursively, so the Struct case needs no special handling. // + // UNKNOWN FIELDS: a message carrying fields the renderer's schema does not + // define MUST NOT be canonicalized, and a verifier MUST reject it rather than + // verify over the reduced bytes. proto-JSON emits only what the schema defines, + // so the bytes reconstructed from such a message silently omit part of what the + // signer covered. The rule binds at EVERY depth — a nested message and each + // element of a repeated or map field carries its own unknown-field set. Without + // it the omission cuts both ways: a signer built against a newer schema would be + // rejected for the wrong reason, and an intermediary could APPEND unknown fields + // to an already-signed message without invalidating its signature, smuggling + // unauthenticated content through a message the recipient treats as verified. + // Extensions therefore ride in `ext` / `ext_critical`, which are defined fields + // and inside the signed bytes — never as undeclared field numbers. + // // Because the signature covers `terms`, `pricing`, `expires_at`, and // `exchange`, an intermediary (Broker) cannot tamper with price, restrictions, // quotas, obligations, the expiry, the execute-routing target, or any diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index d6fe90c..c4d7dc6 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -79,6 +79,29 @@ string field, and a new `empty_requester_id` vector in TS. The corpus change is purely additive — the pre-existing vectors and their signatures are byte-identical, so no already-issued signature is affected. +**Canonical signing rejects messages carrying unknown fields (normative; Go SDK +behaviour change, no wire change).** A message carrying fields the renderer's schema +does not define MUST NOT be canonicalized, and a verifier MUST reject it rather than +verify over the reduced bytes. The rule is stated on `Offer.signature`, the single +normative definition of the canonical form, and binds at EVERY depth — a nested message +and each element of a repeated or map field carries its own unknown-field set. proto-JSON +emits only what the schema defines, so bytes reconstructed from such a message silently +omit part of what the signer covered. Left unchecked the omission cuts both ways: a signer +built against a newer schema is rejected for the wrong reason, and an intermediary could +APPEND unknown fields to an already-signed `Offer` **without invalidating its signature**, +smuggling unauthenticated content through a message the recipient treats as verified. +`helpers.VerifyOffer` surfaces the refusal as `ErrOfferSignatureInvalid` — a message that +arrived carrying extra bytes is a tampered offer, not an internal fault — while +`helpers.CanonicalOfferBytes` and `helpers.SignOffer` return it directly. No legitimate +traffic regresses: an offer signed WITH a field this build cannot render already failed to +verify, because the renderer dropped it and the bytes differed; the refusal only makes the +reason explicit. Python (`from_wire_offer`) and TypeScript (`canonicalOfferPayload`) +already rejected such input — both keep unrecognized members, so their bytes differ — so +this brings Go into line rather than moving the cross-language contract. Extensions are +unaffected: they ride in `ext` / `ext_critical`, defined fields that sit inside the signed +bytes, never undeclared field numbers. No new exported symbol; no field, message, or wire +change — proto comments only, with `gen/` and the website mirror regenerated. + **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written billing label on `Requester` is gone; the field is deleted outright with no `reserved` statement — pre-v1 the number returns to the free pool, and From fc6e7b5f90d0b71f0942500d79392b18d8661a35 Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 24 Jul 2026 17:19:56 +0200 Subject: [PATCH 16/21] test(sdk-go): pin the empty idempotency-key omission in the shared corpus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverting the empty-idempotency-key guard in the Python and TypeScript acceptance payloads left every gate green in all three languages — it was the last omittable field the corpus did not cover, so the omission rule could be dropped there silently, re-opening the cross-language byte divergence the requester_id vector was added to close. TransactionRequest.idempotency_key carries min_len:1, so an empty key never reaches the wire; these accessors sit below that check and must still agree on the bytes, which is exactly why the gap was invisible. The corpus gains one vector and nothing else: the existing entries and their signatures are byte-identical, so no already-issued signature is affected. The Python and TypeScript replay suites iterate the vector list, so they pick it up with no per-vector code. --- sdk/go/helpers/gen_vectors_test.go | 16 +++++++++++++--- sdk/go/helpers/testdata/acceptance-vectors.json | 11 +++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/sdk/go/helpers/gen_vectors_test.go b/sdk/go/helpers/gen_vectors_test.go index f987400..0cb43a2 100644 --- a/sdk/go/helpers/gen_vectors_test.go +++ b/sdk/go/helpers/gen_vectors_test.go @@ -996,9 +996,13 @@ func verifySignRequestVector(t *testing.T, v signRequestVector) { } // buildAcceptanceVectors signs a fixed set of offer acceptances with the REAL Go -// SignOfferAcceptance, seeded 0102..1f20 (shared with the app fixture). Includes -// the empty-domain case (proto3 field-3 default-skip). Records canonical bytes -// hex + signature hex + std-base64 pubkey. +// SignOfferAcceptance, seeded 0102..1f20 (shared with the app fixture). Records +// canonical bytes + signature hex + std-base64 pubkey. +// +// The specs cover each field the canonical form may omit, one per vector. Go omits +// them structurally (EmitUnpopulated=false); the Python and TS faces hand-build the +// object and have to drop empty members themselves, so an omission they miss shows +// up here as a byte mismatch and nowhere else. func buildAcceptanceVectors(t *testing.T) []acceptanceVector { t.Helper() seed, err := hex.DecodeString(acceptanceSeedHex) @@ -1024,6 +1028,12 @@ func buildAcceptanceVectors(t *testing.T) []acceptanceVector { // vector is what holds the hand-built Python/TS payloads to that, since // they enumerate the keys instead of inheriting EmitUnpopulated=false. {"empty_requester_id", "sig3deadbeef", "", "agent.example.com", "idem-3"}, + // The last omittable field. TransactionRequest.idempotency_key carries + // min_len:1 so the wire rejects an empty one, but these accessors are the + // layer below that check and must still agree on the bytes — without this + // vector the omission guard can be dropped in any language with every gate + // still green. + {"empty_idempotency_key", "sig4deadbeef", "agent-4", "agent.example.com", ""}, } out := make([]acceptanceVector, 0, len(specs)) diff --git a/sdk/go/helpers/testdata/acceptance-vectors.json b/sdk/go/helpers/testdata/acceptance-vectors.json index d74ab04..f4b5655 100644 --- a/sdk/go/helpers/testdata/acceptance-vectors.json +++ b/sdk/go/helpers/testdata/acceptance-vectors.json @@ -33,6 +33,17 @@ "signature_hex": "5e9baefd90faae7edc44c8bcd3c6aec68fc3d9c253a6791ab83d6dcb6d638310053b5cb3607f19618cff96e5fc002a7d93d600ce16ba77019c73a2b7c50d1c01", "pubkey_b64": "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=", "seed_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + }, + { + "name": "empty_idempotency_key", + "offer_sig": "sig4deadbeef", + "requester_id": "agent-4", + "requester_domain": "agent.example.com", + "idempotency_key": "", + "canonical_jcs": "{\"offer_sig\":\"sig4deadbeef\",\"requester_domain\":\"agent.example.com\",\"requester_id\":\"agent-4\"}", + "signature_hex": "8230b1c9de146f70b9448adb2846a8e4db8a41172cb6b8e8da52c25be4d62d3d4b209eb77f85503eda3b2bd99f27fbd148bcfc7775a19a7f6830e0aa08dde606", + "pubkey_b64": "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=", + "seed_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" } ] } From c9199232c79772b21181f3b005f9ce8acb792a13 Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 24 Jul 2026 17:19:56 +0200 Subject: [PATCH 17/21] refactor(sdk): drop empty acceptance members in one step per port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Go omits unpopulated fields structurally, through the pinned protojson option set. The Python and TypeScript faces hand-build the payload, so they re-encoded that one layer-wide rule as a conditional per key — and a missing conditional is precisely how requester_id came to sign bytes Go never produces. Assembling the whole record and filtering empty members once means a field added to AgentAcceptancePayload cannot arrive without its omission. Byte-neutral: the committed acceptance corpus replays unchanged in both languages, which is the check that the refactor preserved every rendering. Also corrects the TypeScript AcceptanceInput doc, which still described the omission as applying to requesterDomain alone. That is the doc an editor surfaces at the call site, so it outranks the function comment already fixed. --- sdk/python/ramp_sdk/core.py | 30 +++++++++++++++--------------- sdk/ts/src/acceptance.ts | 30 ++++++++++++++++++------------ 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/sdk/python/ramp_sdk/core.py b/sdk/python/ramp_sdk/core.py index d29f737..0513571 100644 --- a/sdk/python/ramp_sdk/core.py +++ b/sdk/python/ramp_sdk/core.py @@ -273,24 +273,24 @@ def jcs_acceptance_payload( The same JCS(protojson(...)) canonicalization the offer signature uses. proto-JSON OMITS unpopulated fields, so EVERY empty string field is absent from the object - before JCS — not just ``requester_domain``. The Go oracle gets that from - ``EmitUnpopulated=false``; this hand-built object has to do it per field, or an - empty ``requester_id`` would sign bytes Go never produces and cross-language - verification would fail on a wire-valid input (``Requester.id`` carries no - ``min_len``). Fail-closed on an empty ``offer_sig`` (mirror Go - CanonicalAcceptanceBytes): an empty anchor would let the acceptance float free of - any concrete offer. + before JCS — not just ``requester_domain``. The Go oracle gets that structurally + from ``EmitUnpopulated=false``; this object is hand-built, so the omission is + applied once over the whole record rather than per key. A per-key guard is how the + rule went missing for ``requester_id`` — wire-valid, since ``Requester.id`` carries + no ``min_len`` — which signed bytes Go never produces; filtering the assembled + record means a field added to ``AgentAcceptancePayload`` cannot arrive without it. + Fail-closed on an empty ``offer_sig`` (mirror Go CanonicalAcceptanceBytes): an + empty anchor would let the acceptance float free of any concrete offer. """ if offer_sig == "": raise ValueError("cannot accept an unsigned offer (empty offer signature)") - obj: dict[str, str] = {"offer_sig": offer_sig} - if requester_id != "": - obj["requester_id"] = requester_id - if requester_domain != "": - obj["requester_domain"] = requester_domain - if idempotency_key != "": - obj["idempotency_key"] = idempotency_key - return rfc8785.dumps(obj) + payload: dict[str, str] = { + "offer_sig": offer_sig, + "requester_id": requester_id, + "requester_domain": requester_domain, + "idempotency_key": idempotency_key, + } + return rfc8785.dumps({k: v for k, v in payload.items() if v != ""}) def sign_offer_acceptance_jcs( diff --git a/sdk/ts/src/acceptance.ts b/sdk/ts/src/acceptance.ts index aeb2045..ef9c8fd 100644 --- a/sdk/ts/src/acceptance.ts +++ b/sdk/ts/src/acceptance.ts @@ -25,10 +25,11 @@ import { utf8Bytes } from "./base64url.ts"; * (mirror helpers.AcceptanceSignatureAlgorithm). */ export const ACCEPTANCE_SIGNATURE_ALGORITHM = "EdDSA"; -/** The acceptance binding fields. requesterDomain is omitted from the canonical - * payload when empty (proto omit-unpopulated); an empty offerSig is rejected - * fail-closed — an empty anchor would let the acceptance float free of any - * concrete offer. */ +/** The acceptance binding fields. EVERY empty field is omitted from the canonical + * payload, not only requesterDomain (proto omit-unpopulated) — so an empty value and + * an absent one sign the same bytes, and neither signs the bytes of a populated one. + * An empty offerSig is rejected fail-closed — an empty anchor would let the + * acceptance float free of any concrete offer. */ export interface AcceptanceInput { offerSig: string; requesterId: string; @@ -63,14 +64,19 @@ export function acceptancePayload(input: AcceptanceInput): Uint8Array = { offer_sig: input.offerSig }; - if (input.requesterId !== "") obj.requester_id = input.requesterId; - if (input.requesterDomain !== "") obj.requester_domain = input.requesterDomain; - if (input.idempotencyKey !== "") obj.idempotency_key = input.idempotencyKey; + // oracle gets that structurally from EmitUnpopulated=false; this record is + // hand-built, so the omission is applied once over the whole record rather than + // per key. A per-key guard is how the rule went missing for requester_id -- + // wire-valid, since Requester.id carries no min_len -- which signed bytes Go never + // produces; filtering the assembled record means a field added to + // AgentAcceptancePayload cannot arrive without it. + const payload: Record = { + offer_sig: input.offerSig, + requester_id: input.requesterId, + requester_domain: input.requesterDomain, + idempotency_key: input.idempotencyKey, + }; + const obj = Object.fromEntries(Object.entries(payload).filter(([, v]) => v !== "")); const jcs = canonicalize(obj); if (jcs === undefined) { throw new Error("ramp/acceptance: payload is not JSON-serializable"); From 178672b384fe261be1361d9e25a0acf7f298f135 Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 24 Jul 2026 17:19:56 +0200 Subject: [PATCH 18/21] docs(sdk/python): keep the wire contract in one place MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test module restated the wire-versus-canonical contract in full, so the correction to the retired camelCase description had to be made twice, in lockstep, in the same change — the drift this kind of duplication causes, demonstrated on itself. The description now lives only in ramp_sdk.wire_canon; the test docstring keeps what the suite actually pins, plus the one consequence its assertions depend on (from_wire_offer keeps the signature fields, so the comparisons strip them by hand). The same docstring also still announced the module as not yet existing, a red-step narrative left standing after the module landed. Stated as history now, matching the acceptance suite. --- sdk/python/tests/test_wire_canon.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/sdk/python/tests/test_wire_canon.py b/sdk/python/tests/test_wire_canon.py index d35a64b..5279bba 100644 --- a/sdk/python/tests/test_wire_canon.py +++ b/sdk/python/tests/test_wire_canon.py @@ -1,15 +1,12 @@ """sdk/python wire-to-canonical offer canonicalizer. -The RAMP Connect wire emitted by the Broker is snake_case proto-JSON with -EmitUnpopulated (zero-valued scalars, empty repeateds, null messages and -``*_UNSPECIFIED`` enums all present). The offer SIGNATURE covers the CANONICAL form: -the same snake_case proto names, but omit-unpopulated, enums-as-names, then RFC 8785 -JCS. Both sides share the naming, so what ``from_wire_offer`` undoes is the -zero-inflation, not the naming; it keeps the signature fields, which -``ramp_sdk.core.canonical_offer_payload`` strips on the way into JCS (hence the manual -strip in the assertions below). It belongs in sdk/python so every RAMP client (MCP -shim, future TS MCP, future Python broker) shares one wire-normalization function -proven byte-identical to the Go oracle. +The wire form, the canonical form, and what the inversion does and does not undo are +described once, in :mod:`ramp_sdk.wire_canon`. That module docstring is the contract; +this one says only what the suite pins, so the two cannot drift apart the way they +already did once. The one consequence worth restating, because the assertions below +depend on it: ``from_wire_offer`` KEEPS the signature fields, so every comparison here +strips ``signature``/``signature_algorithm`` by hand, exactly as +``ramp_sdk.core.canonical_offer_payload`` does on the way into JCS. Four behaviors pinned here: @@ -43,13 +40,11 @@ as (a). The rules they pin are naming-independent, and their snake_case twins are the ``unspecified_enum_pruned`` and ``set_empty_optional_unit`` vectors in (d). -RED now: ``ramp_sdk.wire_canon`` does not exist yet. The module-level import -below causes a collection error, which is the established red style in this suite -(mirrors test_client_binding_smoke.py and test_core_offer_verify_parity.py: -module-level import with ``type: ignore[import-not-found]`` → ImportError on -collection → pytest ERRORS = confirmed RED). The implement step adds -``sdk/python/ramp_sdk/wire_canon.py`` (exporting ``from_wire_offer``); these -tests go green with no change to the assertions. +The suite was authored RED, before ``ramp_sdk.wire_canon`` existed: the module-level +import below raised on collection, the established red style here (mirrors +test_client_binding_smoke.py and test_core_offer_verify_parity.py). The module has +since landed and the assertions went green unchanged — the ``type: ignore`` on the +import is what remains of that step. """ from __future__ import annotations From 7308d2eb5acdfd95becb61720b96059460562286 Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 24 Jul 2026 18:34:48 +0200 Subject: [PATCH 19/21] feat(sdk-go): export the unknown-field refusal and scope its normative rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rule as first written said a message carrying unknown fields MUST NOT be canonicalized and a verifier MUST reject it. That binds implementations it should not. Whether a canonicalizer can reproduce the signed bytes depends on what it does with content it has no schema for: - OMITTING (proto-JSON emits only schema-defined fields): it cannot reproduce those bytes at all, so it must refuse the message rather than emit a reduced rendering, at every depth. - PRESERVING (carries unrecognized members through): it reproduces them faithfully and has nothing to refuse. Either way an appended field cannot pass — the omitting canonicalizer refuses the message, the preserving one renders the appended member into bytes the signer never covered. Only the omitting case ever failed open, and that is the case the Go guard closes. The Python and TS ports are preserving canonicalizers: they already reject the tamper case on a byte mismatch, and they verify a message whose signer covered the unknown member, which is the forward-compatible outcome rather than a parity break. The changelog said they "already rejected such input", true only of the tamper direction; it now states the distinction, as does the Python module docstring that made the same claim. The refusal is also now classifiable. CanonicalOfferBytes was exported for out-of-package callers persisting evidence, and its doc promises the refusal, so those callers need something to match on: errUnknownFields becomes ErrUnknownFields, registered as a Go-idiomatic exclusion beside the other errors.Is sentinels. VerifyOffer wraps BOTH it and ErrOfferSignatureInvalid, so the denial mapping downstream keeps resolving through the signature sentinel while a caller wanting the specific reason can ask for it. The accessor doc no longer re-enumerates the depths the rule reaches — one copy had already lost maps. That list lives in the normative comment and in the walk, which is the audit point. No verification outcome moves: proto comments only, regenerated with the pinned buf, and the guard is untouched. --- docs/sdk-parity-matrix.md | 3 +- gen/descriptor.binpb | Bin 540739 -> 541225 bytes gen/go/ramp/v1/ramp.pb.go | 28 +++++--- gen/python/wire/models.py | 2 +- gen/ts/wire/schemas.ts | 12 ++-- proto/CHANGELOG.md | 67 +++++++++++------- proto/ramp/v1/ramp.proto | 28 +++++--- sdk/go/helpers/canonicalsign.go | 19 +++-- sdk/go/helpers/offer.go | 22 +++--- sdk/parity/symbol-map.json | 1 + sdk/python/ramp_sdk/wire_canon.py | 19 +++-- .../src/content/docs/reference/changelog.mdx | 67 +++++++++++------- 12 files changed, 170 insertions(+), 98 deletions(-) diff --git a/docs/sdk-parity-matrix.md b/docs/sdk-parity-matrix.md index 787608d..d2084eb 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:** 74 symbols at cross-language parity · 14 documented divergences · 98 Go-idiomatic exclusions · 23 conformance corpora, each tri-replayed. +**At a glance:** 74 symbols at cross-language parity · 14 documented divergences · 99 Go-idiomatic exclusions · 23 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). @@ -228,6 +228,7 @@ Go constructs (functional-option builders, `errors.Is` sentinels, value types, c | `helpers.ErrURLMissingSignature` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | | `helpers.ErrURLNotAgentBound` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | | `helpers.ErrURLSignatureInvalid` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | +| `helpers.ErrUnknownFields` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | | `helpers.ErrUnsupportedAlgorithm` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | | `helpers.FromContext` | Go context.Context accessor; py/ts thread verified-request state explicitly. | | `helpers.NewContext` | Go context.Context accessor; py/ts thread verified-request state explicitly. | diff --git a/gen/descriptor.binpb b/gen/descriptor.binpb index 7b390007425296f46919ad235546b5146f366789..3163f4c4537d060b2d4247547a05e280c7bca030 100644 GIT binary patch delta 35102 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 diff --git a/gen/go/ramp/v1/ramp.pb.go b/gen/go/ramp/v1/ramp.pb.go index 0d32269..93dacd7 100644 --- a/gen/go/ramp/v1/ramp.pb.go +++ b/gen/go/ramp/v1/ramp.pb.go @@ -2556,16 +2556,26 @@ type Offer struct { // - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its // keys recursively, so the Struct case needs no special handling. // - // UNKNOWN FIELDS: a message carrying fields the renderer's schema does not - // define MUST NOT be canonicalized, and a verifier MUST reject it rather than - // verify over the reduced bytes. proto-JSON emits only what the schema defines, - // so the bytes reconstructed from such a message silently omit part of what the - // signer covered. The rule binds at EVERY depth — a nested message and each - // element of a repeated or map field carries its own unknown-field set. Without - // it the omission cuts both ways: a signer built against a newer schema would be - // rejected for the wrong reason, and an intermediary could APPEND unknown fields - // to an already-signed message without invalidating its signature, smuggling + // UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or + // PRESERVES it, and the rule follows from which: + // + // - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a + // canonicalizer CANNOT reproduce the signed bytes of a message carrying + // unknown fields — what it renders silently drops part of what the signer + // covered. It MUST refuse the message rather than emit the reduced bytes, + // and a verifier built on it MUST reject rather than verify over them. The + // refusal binds at EVERY depth: a nested message and each element of a + // repeated or map field carries its own unknown-field set. + // - PRESERVING (a canonicalizer that carries unrecognized members through): + // it reproduces the signed bytes faithfully, so there is nothing to refuse. + // + // Either way an APPENDED field cannot pass: an omitting canonicalizer refuses + // the message, and a preserving one renders the appended member into bytes the + // signer never covered, so the signature fails. Without the refusal the omitting + // case would fail OPEN — an intermediary could add unknown fields to an + // already-signed message and leave its signature verifying, smuggling // unauthenticated content through a message the recipient treats as verified. + // // Extensions therefore ride in `ext` / `ext_critical`, which are defined fields // and inside the signed bytes — never as undeclared field numbers. // diff --git a/gen/python/wire/models.py b/gen/python/wire/models.py index cf8776e..f5e3934 100644 --- a/gen/python/wire/models.py +++ b/gen/python/wire/models.py @@ -1630,7 +1630,7 @@ class Offer(WireModel): ) signature: str | None = Field( '', - description="CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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.", + description="CANONICAL 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.", ) signature_algorithm: str | None = Field( '', diff --git a/gen/ts/wire/schemas.ts b/gen/ts/wire/schemas.ts index 19d5cab..ab56239 100644 --- a/gen/ts/wire/schemas.ts +++ b/gen/ts/wire/schemas.ts @@ -40,7 +40,7 @@ export const DiscoveryMethodSchema = wire(z.enum(["DISCOVERY_METHOD_EXCHANGE","D export const DiscoveryRequestSchema = wire(z.object({ "acceptable_restrictions": z.array(z.object({ "axis": z.union([z.string().regex(new RegExp("^RESTRICTION_KIND_UNSPECIFIED$")), z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("Which axis (same enum as Restriction.kind): FUNCTION / GEOGRAPHY /\n USER_TYPE / OTHER.").default(0), "values": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("The values the query operates within on this axis — same token vocabulary\n as the terms (e.g. FUNCTION [\"ai-train\"], GEOGRAPHY [\"US\", \"EU\"]).").optional() }).describe("AcceptableRestriction — the limits a query operates within on one restriction\n axis, expressed in the same RestrictionKind vocabulary that terms use. The\n Exchange/Broker MAY pre-select offers whose term restrictions fall within\n these as a convenience (see Restriction); it is NOT enforcement — the agent\n self-selects and bears compliance.")).describe("The limits the agent will operate within, per restriction axis — see\n AcceptableRestriction. The Broker forwards these to Exchanges in\n ResourceQuery.acceptable_restrictions. Advisory selection inputs, not\n enforcement.").optional(), "constraints": z.object({ "budget_period": z.string().describe("Budget period (e.g. \"2592000s\" = 30 days; proto-JSON encodes Duration\n as seconds). Resets at period boundary.").optional(), "budget_scope": z.string().describe("Budget scope identifier for per-period tracking.\n E.g. \"user:u-12345\" for per-user budgets, \"team:eng\" for per-team.\n The Broker tracks cumulative spend per scope across sessions.").optional(), "delivery_preference": z.array(z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"])).describe("Preferred delivery methods, in order of preference.").optional(), "exchanges": z.array(z.string()).describe("Authorized Exchange domains. Broker queries only these.").optional(), "max_data_age": z.string().describe("Only relevant for DYNAMIC resources. Ignored for STATIC (content is\n immutable) and LIVE (content doesn't exist yet).\n\n Examples:\n 7 days — \"credit report updated within the last week\"\n 1 hour — \"stock snapshot from the last hour\"\n 30 days — \"drug interaction database updated this month\"").optional(), "max_hops": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum forwarding hops the agent will allow (Agent → Broker → … →\n Exchange), counted as the number of RFC 9421 HTTP Message Signatures on the\n request. Caps chain depth so a request is not relayed through more brokers\n than the agent is willing to trust or pay. A Broker MUST NOT forward a\n request whose signature count would exceed this. Absent = agent imposes no\n cap (the Exchange's max_intermediary_hops still applies).").optional(), "max_price": 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("Maximum price the agent is willing to pay.").optional(), "max_unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Maximum effective cost per unit, as an exact decimal string (not a float).").optional(), "period_budget": 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("Per-period budget limit. The Broker tracks spend against this\n for the budget_scope. Transactions that would exceed are denied.").optional(), "preferred_exchanges": z.array(z.string()).describe("Exchanges the agent has existing relationships with (subscriptions,\n contracts). The Broker SHOULD prefer these when resource is\n available — subscription resource has zero marginal cost.").optional(), "reporting_capable": z.boolean().describe("Whether the agent supports post-usage reporting.").optional() }).describe("Constraints for exchange filtering and offer selection.").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(), "query": z.string().describe("Search query for Broker-side resource discovery.\n Used when the agent doesn't know specific URIs but wants the Broker\n to find matching resources across Exchanges.\n When present, the Broker interprets the query and discovers resources\n across Exchanges on the agent's behalf. Results returned as Offers\n in DiscoveryResponse, same as for specific URI requests.\n Can be used alongside uris (specific URIs + search in one request).").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().describe("Domain the requester belongs to — used for public key lookup.\n Keys published at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT).").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(), "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("The 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 — who is making this request, what scopes they have.\n The Broker forwards this to Exchanges in ResourceQuery.requester.").optional(), "search_filters": z.record(z.string(), z.any()).describe("Structured search filters (optional, alongside or instead of query).\n Keys are profile-specific: \"academic.topic\", \"news.category\",\n \"legal.jurisdiction\", etc. The Broker maps these to Exchange-specific\n query parameters.").optional(), "supported_profiles": z.array(z.string()).describe("The Broker uses this to:\n 1. Route queries to Exchanges that support these profiles\n 2. Forward the profiles in ResourceQuery.supported_profiles\n 3. Include profile-specific ext fields when returning results\n\n Examples: [\"ramp-academic-v1\"] — agent working on literature review").optional(), "uris": z.array(z.string()).max(256).describe("Resource URIs the agent wants. The Broker forwards these to Exchanges in\n ResourceQuery.uris. Optional when `query` / `search_filters` drive\n Broker-side discovery instead.").optional(), "ver": z.string().describe("RAMP protocol version").default("") }).describe("DiscoveryRequest — Agent sends to Broker (Step 1).")); -export const DiscoveryResponseSchema = wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Existence-oracle note: an authorization-flavored reason (SCOPE_INSUFFICIENT,\n NOT_AUTHORIZED, NOT_IN_CATALOG, CONTENT_BLOCKED) confirms a resource exists\n and why access was refused. Resolve surfaces the same oracle at the broker\n that OfferGroup.absence_reason does at the Exchange, so the same mitigation\n applies: where existence itself must stay hidden, the Broker MAY omit the\n reason (leave this unset) rather than reveal it. See the threat model.").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(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI — the sole offer representation in this\n response. One OfferGroup per URI the agent asked for (echoed in\n OfferGroup.uri); a group with no offers carries OfferGroup.absence_reason\n explaining why. Each contained Offer is the full signed Offer the Exchange\n issued (including Offer.exchange, the execute-routing target), forwarded by\n the Broker unchanged so the agent can verify the signature end to end.").optional(), "ver": z.string().default("") }).describe("Carries discovery results only: the offers the Broker gathered across\n Exchanges, grouped by the URI they were requested for. Committing to an offer\n is a separate exchange on the execute path; that per-transaction result\n (transaction_id, billing_id, cost, delivery_method, retrieval endpoint, …)\n is returned by TransactionResponse, not here.")); +export const DiscoveryResponseSchema = wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Existence-oracle note: an authorization-flavored reason (SCOPE_INSUFFICIENT,\n NOT_AUTHORIZED, NOT_IN_CATALOG, CONTENT_BLOCKED) confirms a resource exists\n and why access was refused. Resolve surfaces the same oracle at the broker\n that OfferGroup.absence_reason does at the Exchange, so the same mitigation\n applies: where existence itself must stay hidden, the Broker MAY omit the\n reason (leave this unset) rather than reveal it. See the threat model.").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(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI — the sole offer representation in this\n response. One OfferGroup per URI the agent asked for (echoed in\n OfferGroup.uri); a group with no offers carries OfferGroup.absence_reason\n explaining why. Each contained Offer is the full signed Offer the Exchange\n issued (including Offer.exchange, the execute-routing target), forwarded by\n the Broker unchanged so the agent can verify the signature end to end.").optional(), "ver": z.string().default("") }).describe("Carries discovery results only: the offers the Broker gathered across\n Exchanges, grouped by the URI they were requested for. Committing to an offer\n is a separate exchange on the execute path; that per-transaction result\n (transaction_id, billing_id, cost, delivery_method, retrieval endpoint, …)\n is returned by TransactionResponse, not here.")); export const DisputeFailureSchema = wire(z.object({ "reason": z.enum(["DISPUTE_FAILURE_REASON_TRANSACTION_NOT_FOUND","DISPUTE_FAILURE_REASON_REPORT_NOT_FILED","DISPUTE_FAILURE_REASON_WINDOW_EXPIRED","DISPUTE_FAILURE_REASON_DUPLICATE","DISPUTE_FAILURE_REASON_INELIGIBLE"]).describe("The failure reason (defined-only, non-zero)") }).describe("DisputeFailure — a dispute could not be filed.")); @@ -88,11 +88,11 @@ export const ObligationKindSchema = wire(z.enum(["OBLIGATION_KIND_ATTRIBUTION"," export const ObligationTriggerSchema = wire(z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"])); -export const OfferSchema = wire(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")); +export const OfferSchema = wire(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")); export const OfferAbsenceReasonSchema = wire(z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"])); -export const OfferGroupSchema = wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")); +export const OfferGroupSchema = wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")); export const PreviewSchema = wire(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("The 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.")); @@ -152,7 +152,7 @@ export const ResourceMutabilitySchema = wire(z.enum(["RESOURCE_MUTABILITY_STATIC export const ResourceQuerySchema = wire(z.object({ "acceptable_restrictions": z.array(z.object({ "axis": z.union([z.string().regex(new RegExp("^RESTRICTION_KIND_UNSPECIFIED$")), z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("Which axis (same enum as Restriction.kind): FUNCTION / GEOGRAPHY /\n USER_TYPE / OTHER.").default(0), "values": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("The values the query operates within on this axis — same token vocabulary\n as the terms (e.g. FUNCTION [\"ai-train\"], GEOGRAPHY [\"US\", \"EU\"]).").optional() }).describe("AcceptableRestriction — the limits a query operates within on one restriction\n axis, expressed in the same RestrictionKind vocabulary that terms use. The\n Exchange/Broker MAY pre-select offers whose term restrictions fall within\n these as a convenience (see Restriction); it is NOT enforcement — the agent\n self-selects and bears compliance.")).describe("The limits this query operates within, per restriction axis (function,\n geography, user-type, …) — see AcceptableRestriction. Advisory selection\n inputs the Exchange/Broker MAY pre-select offers against (convenience, not\n enforcement); the agent self-selects and bears compliance.").optional(), "deadline": z.string().describe("Maximum time the caller will wait for a response.\n Exchange SHOULD prioritize speed over completeness when tight.\n Absent = \"0.5s\" default (proto-JSON encodes Duration as seconds).").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(), "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().describe("Domain the requester belongs to — used for public key lookup.\n Keys published at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT).").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(), "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("The 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 — who is making this request, what scopes they have,\n and optional delegation chain.").optional(), "supported_profiles": z.array(z.string()).describe("Declares which ext field vocabularies the caller can parse and act on.\n The Exchange SHOULD include profile-specific ext fields in Offers\n when the caller declares support. The Exchange MAY skip expensive\n metadata computation (e.g., retraction checking, consolidation\n verification) when the caller does not declare the relevant profile.\n\n Absence means \"send all available metadata\" — Exchange MUST NOT\n withhold ext fields solely because the caller omitted this field.\n\n Values match the Exchange's WellKnownManifest.supported_profiles entries.\n Examples: [\"ramp-news-v1\", \"ramp-academic-v1\", \"ramp-legal-v1\"]").optional(), "uris": z.array(z.string()).max(256).describe("Resource URIs being queried.").optional(), "ver": z.string().describe("RAMP protocol version.").default("") }).describe("Sent by a Broker or directly by an AI agent.\n The Exchange evaluates its access policies, available inventory,\n and reporting requirements before responding.")); -export const ResourceResponseSchema = wire(z.object({ "exchange": z.string().describe("Canonical domain of the responding Exchange.").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(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI (for multi-URI batch queries).\n When populated, `offers` SHOULD be empty to avoid ambiguity.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Flat list of offers (for single-URI queries).").optional(), "rate_limit": z.object({ "limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum requests allowed in the current window.").optional(), "remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Requests remaining in the current window.").optional(), "reset_at": z.string().datetime({ offset: true }).describe("When the current window resets (UTC). After this time, `remaining` resets to `limit`.").optional(), "window": z.string().describe("Duration of the rate limit window (e.g. 60s = per-minute limit).").optional() }).describe("Rate limit status for this caller.\n Present when the Exchange enforces per-caller rate limits on discovery.\n Enables agents/Brokers to throttle proactively rather than hitting\n hard limits. Particularly important when a Broker fans out the\n same batch query to multiple Exchanges — mid-batch rate limiting\n can cause partial results if not signaled early.").optional(), "ver": z.string().describe("Protocol version").default("") }).describe("When the ResourceQuery contains multiple URIs, offers are grouped by URI\n via OfferGroup. When a single URI is queried, the Exchange MAY use\n either the flat `offers` field or a single OfferGroup.")); +export const ResourceResponseSchema = wire(z.object({ "exchange": z.string().describe("Canonical domain of the responding Exchange.").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(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI (for multi-URI batch queries).\n When populated, `offers` SHOULD be empty to avoid ambiguity.").optional(), "offers": z.array(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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Flat list of offers (for single-URI queries).").optional(), "rate_limit": z.object({ "limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum requests allowed in the current window.").optional(), "remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Requests remaining in the current window.").optional(), "reset_at": z.string().datetime({ offset: true }).describe("When the current window resets (UTC). After this time, `remaining` resets to `limit`.").optional(), "window": z.string().describe("Duration of the rate limit window (e.g. 60s = per-minute limit).").optional() }).describe("Rate limit status for this caller.\n Present when the Exchange enforces per-caller rate limits on discovery.\n Enables agents/Brokers to throttle proactively rather than hitting\n hard limits. Particularly important when a Broker fans out the\n same batch query to multiple Exchanges — mid-batch rate limiting\n can cause partial results if not signaled early.").optional(), "ver": z.string().describe("Protocol version").default("") }).describe("When the ResourceQuery contains multiple URIs, offers are grouped by URI\n via OfferGroup. When a single URI is queried, the Exchange MAY use\n either the flat `offers` field or a single OfferGroup.")); export const RestrictionSchema = wire(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."), "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("Restrictions 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")); @@ -180,9 +180,9 @@ export const TermSemanticsSchema = wire(z.enum(["TERM_SEMANTICS_ENUMERATED","TER export const TransactionDenialSchema = wire(z.object({ "offer_id": z.string().describe("Batch mode: the offer this denial pertains to.").optional(), "reason": z.enum(["DENIAL_REASON_BILLING_REF_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"]).describe("The denial reason (defined-only, non-zero)"), "restriction_mismatches": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When reason = RESTRICTION_NOT_SATISFIED, the failed axes (same\n RestrictionKind vocabulary the terms use).").optional() }).describe("TransactionDenial — ExecuteTransaction could not complete. Carries the denial\n reason the response body no longer holds (denial_reason / restriction_mismatches\n move here in the response-shape normalization). Reuses the DenialReason vocab.")); -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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).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 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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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 message carrying fields the renderer's schema does not\n define MUST NOT be canonicalized, and a verifier MUST reject it rather than\n verify over the reduced bytes. proto-JSON emits only what the schema defines,\n so the bytes reconstructed from such a message silently omit part of what the\n signer covered. The rule binds at EVERY depth — a nested message and each\n element of a repeated or map field carries its own unknown-field set. Without\n it the omission cuts both ways: a signer built against a newer schema would be\n rejected for the wrong reason, and an intermediary could APPEND unknown fields\n to an already-signed message without invalidating its signature, smuggling\n unauthenticated content through a message the recipient treats as verified.\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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).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().describe("Domain the requester belongs to — used for public key lookup.\n Keys published at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT).").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(), "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("The 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("Protocol version").default("") }).describe("After 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({ "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("A 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("Three 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("Not 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().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "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("Formats:\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("Level 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("Algorithm 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.").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("The 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("Per 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("The (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("CANONICAL 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("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").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("Analogous 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("\"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("The 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("\"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("The 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("Examples:\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.")).describe("Post-use behavioral requirements.").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("The (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 (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("Quotas 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.")).describe("Usage caps. The agent must not exceed any individual Quota.").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."), "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("Restrictions 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")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage 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("One 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() }).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().describe("Domain the requester belongs to — used for public key lookup.\n Keys published at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT).").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(), "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("The 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("Protocol version").default("") }).describe("After 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_BILLING_REF_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"]).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("Analogous 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("Protocol version").default("") }).describe("Items-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 ae1ccfb..ad2c354 100644 --- a/proto/CHANGELOG.md +++ b/proto/CHANGELOG.md @@ -91,32 +91,47 @@ EVERY unpopulated field, so the three SDKs signed different bytes whenever `requ was empty. That input is wire-valid: `Requester.id` carries no `min_len`. Verification failed closed on it (a byte mismatch, never a bypass), but the byte-equivalence the canonical-bytes accessors promise did not hold. Both hand-built faces now drop each empty -string field, and a new `empty_requester_id` vector in -`sdk/go/helpers/testdata/acceptance-vectors.json` pins the agreement across Go, Python and -TS. The corpus change is purely additive — the pre-existing vectors and their signatures -are byte-identical, so no already-issued signature is affected. - -**Canonical signing rejects messages carrying unknown fields (normative; Go SDK -behaviour change, no wire change).** A message carrying fields the renderer's schema -does not define MUST NOT be canonicalized, and a verifier MUST reject it rather than -verify over the reduced bytes. The rule is stated on `Offer.signature`, the single -normative definition of the canonical form, and binds at EVERY depth — a nested message -and each element of a repeated or map field carries its own unknown-field set. proto-JSON -emits only what the schema defines, so bytes reconstructed from such a message silently -omit part of what the signer covered. Left unchecked the omission cuts both ways: a signer -built against a newer schema is rejected for the wrong reason, and an intermediary could -APPEND unknown fields to an already-signed `Offer` **without invalidating its signature**, -smuggling unauthenticated content through a message the recipient treats as verified. -`helpers.VerifyOffer` surfaces the refusal as `ErrOfferSignatureInvalid` — a message that -arrived carrying extra bytes is a tampered offer, not an internal fault — while -`helpers.CanonicalOfferBytes` and `helpers.SignOffer` return it directly. No legitimate -traffic regresses: an offer signed WITH a field this build cannot render already failed to -verify, because the renderer dropped it and the bytes differed; the refusal only makes the -reason explicit. Python (`from_wire_offer`) and TypeScript (`canonicalOfferPayload`) -already rejected such input — both keep unrecognized members, so their bytes differ — so -this brings Go into line rather than moving the cross-language contract. Extensions are -unaffected: they ride in `ext` / `ext_critical`, defined fields that sit inside the signed -bytes, never undeclared field numbers. No new exported symbol; no field, message, or wire +string field, and two new vectors in `sdk/go/helpers/testdata/acceptance-vectors.json` — +`empty_requester_id` and `empty_idempotency_key`, one per omittable field left uncovered — +pin the agreement across Go, Python and TS. Without them the omission can be dropped in any +one language with every gate still green. The corpus change is purely additive — the +pre-existing vectors and their signatures are byte-identical, so no already-issued signature +is affected. + +**Canonical signing refuses messages carrying unknown fields (normative; Go SDK +behavior change, no wire change).** `Offer.signature` — the single normative definition of +the canonical form — now states the rule, which turns on whether a canonicalizer omits or +preserves content it has no schema for. An OMITTING canonicalizer (proto-JSON emits only +schema-defined fields) cannot reproduce the signed bytes of a message carrying unknown +fields, so it MUST refuse the message rather than emit the reduced bytes, at EVERY depth — +a nested message and each element of a repeated or map field carries its own unknown-field +set. A PRESERVING canonicalizer carries unrecognized members through, reproduces the +signed bytes faithfully, and has nothing to refuse. + +Either way an APPENDED field cannot pass, which is the point: the omitting case refuses +the message, and the preserving case renders the appended member into bytes the signer +never covered. Without the refusal the omitting case failed OPEN — an intermediary could +add unknown fields to an already-signed `Offer` **without invalidating its signature**, +smuggling unauthenticated content through a message the recipient treats as verified. That +is what the Go SDK now closes: `helpers.VerifyOffer` surfaces the refusal as +`ErrOfferSignatureInvalid` (a message that arrived carrying extra bytes is a tampered +offer, not an internal fault) wrapping the new `helpers.ErrUnknownFields`, so a caller can +branch on either; `helpers.CanonicalOfferBytes` and `helpers.SignOffer` return +`ErrUnknownFields` directly. + +Python (`from_wire_offer`) and TypeScript (`canonicalOfferPayload`) are preserving +canonicalizers and need no change — they already reject the appended-field case on a byte +mismatch. They are NOT expected to reject a message whose signer covered the unknown +member: they reproduce those bytes exactly and verify, which is the forward-compatible +outcome. Go, being an omitting canonicalizer, cannot reconstruct such a message at all and +refuses it; that asymmetry is inherent to the renderer, not new here — before this change +Go rejected the same message on a byte mismatch instead. + +No legitimate traffic regresses: an offer signed WITH a field this build cannot render +already failed to verify; the refusal only makes the reason explicit. Extensions are +unaffected — they ride in `ext` / `ext_critical`, defined fields that sit inside the signed +bytes, never undeclared field numbers. One new exported Go symbol (`ErrUnknownFields`, +registered as a Go-idiomatic exclusion in the parity map); no field, message, or wire change — proto comments only, with `gen/` and the website mirror regenerated. **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written diff --git a/proto/ramp/v1/ramp.proto b/proto/ramp/v1/ramp.proto index 7c3fa3c..119c35a 100644 --- a/proto/ramp/v1/ramp.proto +++ b/proto/ramp/v1/ramp.proto @@ -475,16 +475,26 @@ message Offer { // - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its // keys recursively, so the Struct case needs no special handling. // - // UNKNOWN FIELDS: a message carrying fields the renderer's schema does not - // define MUST NOT be canonicalized, and a verifier MUST reject it rather than - // verify over the reduced bytes. proto-JSON emits only what the schema defines, - // so the bytes reconstructed from such a message silently omit part of what the - // signer covered. The rule binds at EVERY depth — a nested message and each - // element of a repeated or map field carries its own unknown-field set. Without - // it the omission cuts both ways: a signer built against a newer schema would be - // rejected for the wrong reason, and an intermediary could APPEND unknown fields - // to an already-signed message without invalidating its signature, smuggling + // UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or + // PRESERVES it, and the rule follows from which: + // + // - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a + // canonicalizer CANNOT reproduce the signed bytes of a message carrying + // unknown fields — what it renders silently drops part of what the signer + // covered. It MUST refuse the message rather than emit the reduced bytes, + // and a verifier built on it MUST reject rather than verify over them. The + // refusal binds at EVERY depth: a nested message and each element of a + // repeated or map field carries its own unknown-field set. + // - PRESERVING (a canonicalizer that carries unrecognized members through): + // it reproduces the signed bytes faithfully, so there is nothing to refuse. + // + // Either way an APPENDED field cannot pass: an omitting canonicalizer refuses + // the message, and a preserving one renders the appended member into bytes the + // signer never covered, so the signature fails. Without the refusal the omitting + // case would fail OPEN — an intermediary could add unknown fields to an + // already-signed message and leave its signature verifying, smuggling // unauthenticated content through a message the recipient treats as verified. + // // Extensions therefore ride in `ext` / `ext_critical`, which are defined fields // and inside the signed bytes — never as undeclared field numbers. // diff --git a/sdk/go/helpers/canonicalsign.go b/sdk/go/helpers/canonicalsign.go index b42fc01..30c8bb6 100644 --- a/sdk/go/helpers/canonicalsign.go +++ b/sdk/go/helpers/canonicalsign.go @@ -49,7 +49,7 @@ var canonicalSignJSONOptions = protojson.MarshalOptions{ EmitUnpopulated: false, // omit unpopulated fields } -// errUnknownFields refuses a message the canonical form cannot faithfully +// ErrUnknownFields refuses a message the canonical form cannot faithfully // represent. proto-JSON renders only fields the schema defines, so a message // carrying unknown ones canonicalizes to bytes that silently omit them. Both // directions of that gap are wrong: a signer built against a newer schema @@ -57,7 +57,12 @@ var canonicalSignJSONOptions = protojson.MarshalOptions{ // appends unknown fields to an already-signed message would otherwise leave the // signature verifying over bytes it never saw. Refusing the message is what // makes the canonical form's coverage total rather than schema-relative. -var errUnknownFields = errors.New("helpers: message carries unknown fields; canonical bytes cannot represent them") +// +// CanonicalOfferBytes and SignOffer return it directly, so a caller persisting +// evidence can tell this refusal apart from a marshal fault. VerifyOffer wraps it +// alongside ErrOfferSignatureInvalid — a message that arrived carrying extra bytes +// is a tampered offer — so errors.Is matches either sentinel there. +var ErrUnknownFields = errors.New("helpers: message carries unknown fields; canonical bytes cannot represent them") // hasUnknownFields reports whether msg, or anything reachable from it, carries // fields this build's schema does not define. @@ -86,7 +91,11 @@ func messageHasUnknown(m protoreflect.Message) bool { m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { switch { case fd.IsMap(): - // Map KEYS are scalars; only a message-typed value can nest one. + // Map KEYS are scalars; only a message-typed value can nest one. The + // synthetic ENTRY messages are not visited: protobuf-go discards unknown + // bytes planted on an entry at unmarshal, so they never reach here (nor + // the wire again). offer_test.go pins that, and fails loudly if a + // protobuf upgrade starts retaining them. if fd.MapValue().Kind() == protoreflect.MessageKind || fd.MapValue().Kind() == protoreflect.GroupKind { v.Map().Range(func(_ protoreflect.MapKey, mv protoreflect.Value) bool { @@ -120,10 +129,10 @@ func messageHasUnknown(m protoreflect.Message) bool { // covers. Callers clear signature/signature_algorithm on a clone BEFORE calling. // // A message carrying unknown fields is refused rather than rendered — see -// errUnknownFields. +// ErrUnknownFields. func canonicalSignPayload(msg proto.Message) ([]byte, error) { if hasUnknownFields(msg) { - return nil, errUnknownFields + return nil, ErrUnknownFields } pj, err := canonicalSignJSONOptions.Marshal(msg) if err != nil { diff --git a/sdk/go/helpers/offer.go b/sdk/go/helpers/offer.go index 67c689f..f3b81b8 100644 --- a/sdk/go/helpers/offer.go +++ b/sdk/go/helpers/offer.go @@ -55,12 +55,14 @@ func VerifyOffer(offer *rampv1.Offer, signatureHex string, pub ed25519.PublicKey } payload, err := CanonicalOfferBytes(offer) if err != nil { - if errors.Is(err, errUnknownFields) { + if errors.Is(err, ErrUnknownFields) { // An offer carrying fields this build cannot render is refused on the // SAME path as a forged one. Callers branch on the sentinel to map a // rejection to its denial reason, and "someone appended bytes to a - // signed offer" is a signature failure, not an internal fault. - return fmt.Errorf("%w: %v", ErrOfferSignatureInvalid, err) + // signed offer" is a signature failure, not an internal fault. BOTH + // sentinels are wrapped: the denial mapping resolves through the first, + // a caller wanting the specific reason gets the second. + return fmt.Errorf("%w: %w", ErrOfferSignatureInvalid, err) } return err } @@ -95,14 +97,12 @@ func VerifyOffer(offer *rampv1.Offer, signatureHex string, pub ed25519.PublicKey // relaying Broker cannot extend or shorten a signed offer's TTL under an // otherwise-valid signature. // -// An Offer carrying UNKNOWN fields — at any depth, including inside a nested message -// or a repeated element — is REFUSED rather than rendered. proto-JSON emits only -// what the schema defines, so those bytes would silently drop the unknown content: -// a peer built against a newer schema would have signed more than this build can -// reconstruct, and an intermediary could otherwise append fields to a signed Offer -// without disturbing its signature. VerifyOffer surfaces the refusal as -// ErrOfferSignatureInvalid, since a message that arrived carrying extra bytes is a -// tampered Offer, not an internal fault. +// An Offer carrying UNKNOWN fields at ANY depth is REFUSED (ErrUnknownFields) rather +// than rendered — proto-JSON emits only what the schema defines, so those bytes would +// silently drop the unknown content. The rule, and the depths it reaches, are stated +// once in the ramp.proto Offer.signature comment; it is not restated here. VerifyOffer +// surfaces the refusal wrapped in ErrOfferSignatureInvalid, since a message that +// arrived carrying extra bytes is a tampered Offer, not an internal fault. func CanonicalOfferBytes(offer *rampv1.Offer) ([]byte, error) { if offer == nil { return nil, errors.New("helpers: offer is nil") diff --git a/sdk/parity/symbol-map.json b/sdk/parity/symbol-map.json index b07443d..4392f1c 100644 --- a/sdk/parity/symbol-map.json +++ b/sdk/parity/symbol-map.json @@ -76,6 +76,7 @@ "helpers.ErrURLMissingSignature": "Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels.", "helpers.ErrURLNotAgentBound": "Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels.", "helpers.ErrURLSignatureInvalid": "Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels.", + "helpers.ErrUnknownFields": "Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels.", "helpers.ErrUnsupportedAlgorithm": "Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels.", "helpers.FromContext": "Go context.Context accessor; py/ts thread verified-request state explicitly.", "helpers.NewContext": "Go context.Context accessor; py/ts thread verified-request state explicitly.", diff --git a/sdk/python/ramp_sdk/wire_canon.py b/sdk/python/ramp_sdk/wire_canon.py index 2e02ac5..5a74ee8 100644 --- a/sdk/python/ramp_sdk/wire_canon.py +++ b/sdk/python/ramp_sdk/wire_canon.py @@ -34,10 +34,21 @@ drift-gated ``sdk/go/helpers/testdata/wire-canonical-vectors.json`` corpus for the current snake_case wire, and over a live-captured fixture pair for the retired form. -KNOWN LIMITS (fail-closed, never fail-open): a wire offer carrying fields newer -than the pinned gen models is kept verbatim and will verify FALSE (rejected, -not silently accepted), and a non-optional int64 zero (emitted as ``"0"``) is -not dropped (no such field exists on the Offer tree today). +UNKNOWN FIELDS: a wire key the pinned gen models do not define is kept VERBATIM, +which makes this a PRESERVING canonicalizer in the sense the ``Offer.signature`` +canonical-signing block defines. Two consequences, and only the second is a limit: + +* a field APPENDED after signing lands in the canonical dict, so the bytes differ + from what the signer covered and verification fails — the tamper case is closed; +* a field the SIGNER covered (a peer built against a newer schema) is reproduced + exactly, so verification SUCCEEDS over a message this pin cannot fully interpret. + That is the forward-compatible outcome and is not a rejection. Go, whose + proto-JSON renderer OMITS what it has no schema for, cannot reconstruct such a + message at all and refuses it — an inherent difference between the two renderer + families, not a parity break. + +KNOWN LIMIT (fail-closed, never fail-open): a non-optional int64 zero (emitted as +``"0"``) is not dropped (no such field exists on the Offer tree today). """ from __future__ import annotations diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index c4d7dc6..4f042ed 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -74,32 +74,47 @@ EVERY unpopulated field, so the three SDKs signed different bytes whenever `requ was empty. That input is wire-valid: `Requester.id` carries no `min_len`. Verification failed closed on it (a byte mismatch, never a bypass), but the byte-equivalence the canonical-bytes accessors promise did not hold. Both hand-built faces now drop each empty -string field, and a new `empty_requester_id` vector in -`sdk/go/helpers/testdata/acceptance-vectors.json` pins the agreement across Go, Python and -TS. The corpus change is purely additive — the pre-existing vectors and their signatures -are byte-identical, so no already-issued signature is affected. - -**Canonical signing rejects messages carrying unknown fields (normative; Go SDK -behaviour change, no wire change).** A message carrying fields the renderer's schema -does not define MUST NOT be canonicalized, and a verifier MUST reject it rather than -verify over the reduced bytes. The rule is stated on `Offer.signature`, the single -normative definition of the canonical form, and binds at EVERY depth — a nested message -and each element of a repeated or map field carries its own unknown-field set. proto-JSON -emits only what the schema defines, so bytes reconstructed from such a message silently -omit part of what the signer covered. Left unchecked the omission cuts both ways: a signer -built against a newer schema is rejected for the wrong reason, and an intermediary could -APPEND unknown fields to an already-signed `Offer` **without invalidating its signature**, -smuggling unauthenticated content through a message the recipient treats as verified. -`helpers.VerifyOffer` surfaces the refusal as `ErrOfferSignatureInvalid` — a message that -arrived carrying extra bytes is a tampered offer, not an internal fault — while -`helpers.CanonicalOfferBytes` and `helpers.SignOffer` return it directly. No legitimate -traffic regresses: an offer signed WITH a field this build cannot render already failed to -verify, because the renderer dropped it and the bytes differed; the refusal only makes the -reason explicit. Python (`from_wire_offer`) and TypeScript (`canonicalOfferPayload`) -already rejected such input — both keep unrecognized members, so their bytes differ — so -this brings Go into line rather than moving the cross-language contract. Extensions are -unaffected: they ride in `ext` / `ext_critical`, defined fields that sit inside the signed -bytes, never undeclared field numbers. No new exported symbol; no field, message, or wire +string field, and two new vectors in `sdk/go/helpers/testdata/acceptance-vectors.json` — +`empty_requester_id` and `empty_idempotency_key`, one per omittable field left uncovered — +pin the agreement across Go, Python and TS. Without them the omission can be dropped in any +one language with every gate still green. The corpus change is purely additive — the +pre-existing vectors and their signatures are byte-identical, so no already-issued signature +is affected. + +**Canonical signing refuses messages carrying unknown fields (normative; Go SDK +behavior change, no wire change).** `Offer.signature` — the single normative definition of +the canonical form — now states the rule, which turns on whether a canonicalizer omits or +preserves content it has no schema for. An OMITTING canonicalizer (proto-JSON emits only +schema-defined fields) cannot reproduce the signed bytes of a message carrying unknown +fields, so it MUST refuse the message rather than emit the reduced bytes, at EVERY depth — +a nested message and each element of a repeated or map field carries its own unknown-field +set. A PRESERVING canonicalizer carries unrecognized members through, reproduces the +signed bytes faithfully, and has nothing to refuse. + +Either way an APPENDED field cannot pass, which is the point: the omitting case refuses +the message, and the preserving case renders the appended member into bytes the signer +never covered. Without the refusal the omitting case failed OPEN — an intermediary could +add unknown fields to an already-signed `Offer` **without invalidating its signature**, +smuggling unauthenticated content through a message the recipient treats as verified. That +is what the Go SDK now closes: `helpers.VerifyOffer` surfaces the refusal as +`ErrOfferSignatureInvalid` (a message that arrived carrying extra bytes is a tampered +offer, not an internal fault) wrapping the new `helpers.ErrUnknownFields`, so a caller can +branch on either; `helpers.CanonicalOfferBytes` and `helpers.SignOffer` return +`ErrUnknownFields` directly. + +Python (`from_wire_offer`) and TypeScript (`canonicalOfferPayload`) are preserving +canonicalizers and need no change — they already reject the appended-field case on a byte +mismatch. They are NOT expected to reject a message whose signer covered the unknown +member: they reproduce those bytes exactly and verify, which is the forward-compatible +outcome. Go, being an omitting canonicalizer, cannot reconstruct such a message at all and +refuses it; that asymmetry is inherent to the renderer, not new here — before this change +Go rejected the same message on a byte mismatch instead. + +No legitimate traffic regresses: an offer signed WITH a field this build cannot render +already failed to verify; the refusal only makes the reason explicit. Extensions are +unaffected — they ride in `ext` / `ext_critical`, defined fields that sit inside the signed +bytes, never undeclared field numbers. One new exported Go symbol (`ErrUnknownFields`, +registered as a Go-idiomatic exclusion in the parity map); no field, message, or wire change — proto comments only, with `gen/` and the website mirror regenerated. **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written From a002f2294cd4d5c1c12e8a8c5ef088ed62928447 Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 24 Jul 2026 18:35:06 +0200 Subject: [PATCH 20/21] test(sdk-go): cover the map-value and map-entry edges of the unknown-field walk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two branches of the walk had no test. The map branch is reachable and guards the extension surface: Offer.ext is a google.protobuf.Struct whose `fields` is a map with message values, so an unknown field planted on a Struct Value sits two levels down and behind a map. Nothing shallower catches it. Disabling the map branch fails this test and no other. The entry branch is deliberately absent — the walk recurses into map values, not into the synthetic entry messages carrying them. That is total only because protobuf-go discards unknown bytes planted on an entry at unmarshal, so they never reach the canonicalizer nor the wire again. The second test pins that assumption and says what to do if a protobuf upgrade ever breaks it. Also renames the newer-canonical-form test to say what it actually pins. It asserts the rejection OUTCOME for that direction and passes with the guard removed, since the plain signature mismatch yields the same sentinel; the guard is gated by the injected-unknown tests, where clean and tampered render identically. --- sdk/go/helpers/offer_test.go | 87 +++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/sdk/go/helpers/offer_test.go b/sdk/go/helpers/offer_test.go index 9bc9a7c..c014f17 100644 --- a/sdk/go/helpers/offer_test.go +++ b/sdk/go/helpers/offer_test.go @@ -12,6 +12,7 @@ import ( "github.com/RAMP-Protocol/protocol/sdk/go/helpers" "google.golang.org/protobuf/encoding/protowire" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -310,11 +311,95 @@ func TestVerifyOffer_rejectsInjectedUnknownFields(t *testing.T) { } } -func TestVerifyOffer_rejectsSignatureOverANewerCanonicalForm(t *testing.T) { +func TestCanonicalOfferBytes_refusesUnknownFieldsInsideExt(t *testing.T) { + // ext is a google.protobuf.Struct, whose `fields` is a map with MESSAGE values — + // the one place the walk's map branch is reachable on this schema, and it guards + // the sanctioned extension surface. An unknown field planted on a Struct Value + // sits two levels below the Offer and behind a map, so nothing shallower catches + // it. + st, err := structpb.NewStruct(map[string]any{"vendor.key": "v"}) + if err != nil { + t.Fatal(err) + } + clean := sampleOffer() + clean.Ext = st + if _, err := helpers.CanonicalOfferBytes(clean); err != nil { + t.Fatalf("control: an untampered ext must canonicalize: %v", err) + } + + tamperedVal := new(structpb.Value) + if err := proto.Unmarshal(encodeWithUnknownField(t, st.Fields["vendor.key"]), tamperedVal); err != nil { + t.Fatal(err) + } + if len(tamperedVal.ProtoReflect().GetUnknown()) == 0 { + t.Fatal("fixture is inert: the unknown field did not survive unmarshal") + } + st.Fields["vendor.key"] = tamperedVal + + if _, err := helpers.CanonicalOfferBytes(clean); !errors.Is(err, helpers.ErrUnknownFields) { + t.Errorf("want ErrUnknownFields for an unknown field inside ext, got %v", err) + } +} + +func TestCanonicalOfferBytes_mapEntryUnknownsAreDroppedBeforeTheWalk(t *testing.T) { + // The walk recurses into map VALUES, not into the synthetic entry messages that + // carry them. That is total only because protobuf-go discards unknown bytes + // planted on an entry at unmarshal, so they never reach the canonicalizer — and + // never reach the wire again either. This pins that assumption: if a protobuf + // upgrade started retaining entry-level unknowns, the walk would need a branch + // for them and this test is what would say so. + st, err := structpb.NewStruct(map[string]any{"vendor.key": "v"}) + if err != nil { + t.Fatal(err) + } + valRaw, err := proto.Marshal(st.Fields["vendor.key"]) + if err != nil { + t.Fatal(err) + } + // Struct.fields entry: key(1), value(2), then an undeclared field ON THE ENTRY. + entry := protowire.AppendTag(nil, 1, protowire.BytesType) + entry = protowire.AppendBytes(entry, []byte("vendor.key")) + entry = protowire.AppendTag(entry, 2, protowire.BytesType) + entry = protowire.AppendBytes(entry, valRaw) + entry = protowire.AppendTag(entry, 500, protowire.VarintType) + entry = protowire.AppendVarint(entry, 7) + structRaw := protowire.AppendTag(nil, 1, protowire.BytesType) + structRaw = protowire.AppendBytes(structRaw, entry) + + got := new(structpb.Struct) + if err := proto.Unmarshal(structRaw, got); err != nil { + t.Fatal(err) + } + reEncoded, err := proto.Marshal(got) + if err != nil { + t.Fatal(err) + } + clean, err := proto.Marshal(st) + if err != nil { + t.Fatal(err) + } + if len(reEncoded) != len(clean) { + t.Fatalf("protobuf-go now RETAINS entry-level unknown bytes (%d vs %d); the walk "+ + "must gain a map-entry branch", len(reEncoded), len(clean)) + } + + offer := sampleOffer() + offer.Ext = got + if _, err := helpers.CanonicalOfferBytes(offer); err != nil { + t.Errorf("entry-level unknowns are dropped before the walk, so this must canonicalize: %v", err) + } +} + +func TestVerifyOffer_rejectsANewerCanonicalFormWhicheverCheckFires(t *testing.T) { // The other direction: the peer signed its OWN richer rendering, which includes // the field it knows about and this build does not. The signed bytes cannot be // reconstructed here at all, so the offer is rejected rather than verified over // a reduced form. + // + // This pins the OUTCOME, not the mechanism: the rejection also follows from the + // plain signature mismatch, so the test passes with the unknown-field guard + // removed. The guard is gated by the injected-unknown tests above, where clean + // and tampered render identically and only the guard can reject. pub, priv, _ := ed25519.GenerateKey(nil) base := sampleOffer() baseCanon, err := helpers.CanonicalOfferBytes(base) From 26e63ae7b4420814db0c0b7d4d69c8eab68d4ded Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 24 Jul 2026 18:35:06 +0200 Subject: [PATCH 21/21] docs(sdk): correct the omission prose in the ports and the design history The design history said the acceptance payload is "the one place all three SDKs hand-build the object", two sentences after correctly noting that Go inherits the omission from protojson. Go renders an AgentAcceptancePayload message; only the Python and TS ports hand-build. That distinction is the paragraph's own point. Both ports claimed a future field "cannot arrive without" its omission. The filter tests for the empty string, so a non-string field's zero value would pass it and diverge from Go. The comment now says what holds: every member the message has is a string, the field-set guard pins that list, and a non-string field would need its own zero-value test. --- docs/design-history.md | 4 +++- sdk/python/ramp_sdk/core.py | 6 ++++-- sdk/ts/src/acceptance.ts | 6 ++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/design-history.md b/docs/design-history.md index a1df6e1..8e802d9 100644 --- a/docs/design-history.md +++ b/docs/design-history.md @@ -78,7 +78,9 @@ with an empty value — so the canonical bytes for an empty string field are nev 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 all three SDKs hand-build the object, and it is exactly where the divergence +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 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 diff --git a/sdk/python/ramp_sdk/core.py b/sdk/python/ramp_sdk/core.py index 0513571..7f12179 100644 --- a/sdk/python/ramp_sdk/core.py +++ b/sdk/python/ramp_sdk/core.py @@ -277,8 +277,10 @@ def jcs_acceptance_payload( from ``EmitUnpopulated=false``; this object is hand-built, so the omission is applied once over the whole record rather than per key. A per-key guard is how the rule went missing for ``requester_id`` — wire-valid, since ``Requester.id`` carries - no ``min_len`` — which signed bytes Go never produces; filtering the assembled - record means a field added to ``AgentAcceptancePayload`` cannot arrive without it. + no ``min_len`` — which signed bytes Go never produces. The filter tests for the + empty STRING, which covers every member ``AgentAcceptancePayload`` has (the field-set + guard in the Go suite pins that list), so a string field added to the message cannot + arrive without its omission. A non-string field would need its own zero-value test. Fail-closed on an empty ``offer_sig`` (mirror Go CanonicalAcceptanceBytes): an empty anchor would let the acceptance float free of any concrete offer. """ diff --git a/sdk/ts/src/acceptance.ts b/sdk/ts/src/acceptance.ts index ef9c8fd..da4ee19 100644 --- a/sdk/ts/src/acceptance.ts +++ b/sdk/ts/src/acceptance.ts @@ -68,8 +68,10 @@ export function acceptancePayload(input: AcceptanceInput): Uint8Array = { offer_sig: input.offerSig, requester_id: input.requesterId,