diff --git a/.github/workflows/sdk-types-ci.yml b/.github/workflows/sdk-types-ci.yml index 34fc8473..991ae9ef 100644 --- a/.github/workflows/sdk-types-ci.yml +++ b/.github/workflows/sdk-types-ci.yml @@ -1,7 +1,8 @@ name: sdk-types-ci -# Drift gate for the generated TYPES EXPORT (Pydantic + Zod). Regenerates from the -# proto via scripts/gen-sdk-types.sh and fails if the committed output differs — the +# Drift gate for the generated TYPES EXPORT (Pydantic + Zod) and the published JSON +# Schemas (gen/jsonschema/). Regenerates from the proto via scripts/gen-sdk-types.sh and +# fails if the committed output differs — the # same regenerate-and-diff contract as the gen/ drift gate, for the SDK types. Runs # only when the proto, the descriptor, the generators, or the committed output change. on: @@ -16,6 +17,7 @@ on: - 'conformance/**' - 'gen/python/**' - 'gen/ts/**' + - 'gen/jsonschema/**' - 'sdk/ts/**' - 'sdk/python/**' # The repo-root package.json is the map a CONSUMER resolves through (npm has @@ -60,17 +62,19 @@ jobs: - name: regenerate types export run: ./scripts/gen-sdk-types.sh - # The whole generated wire/ directories, so a NEW generated file (e.g. wire/unique.py) - # is gated the day it is emitted. The untracked sweep is load-bearing: `git diff` cannot - # see a file that was generated but never committed, so a file list alone passes green. - # wire/base.py and wire/base.ts are hand-written seams (never regenerated, so they cannot - # drift); the vocab constants are generated by buf and gated separately. + # The whole generated wire/ directories and the published JSON Schemas, so a NEW + # generated file (e.g. wire/unique.py, or the schema of a new message) is gated the day + # it is emitted. The untracked sweep is load-bearing: `git diff` cannot see a file that + # was generated but never committed, so a file list alone passes green. wire/base.py, + # wire/base.ts, wire/schemas.py and gen/jsonschema/{jsonschema.go,README.md} are + # hand-written (never regenerated, so they cannot drift); the vocab constants are + # generated by buf and gated separately. - name: assert no drift run: | - if ! git diff --exit-code -- gen/python/wire gen/ts/wire \ - || [ -n "$(git ls-files --others --exclude-standard -- gen/python/wire gen/ts/wire)" ]; then - echo "::error::Generated types export is out of sync with the proto. Run scripts/gen-sdk-types.sh and commit gen/python/wire/ + gen/ts/wire/." - git status --short -- gen/python/wire gen/ts/wire + if ! git diff --exit-code -- gen/python/wire gen/ts/wire gen/ts/strict gen/jsonschema \ + || [ -n "$(git ls-files --others --exclude-standard -- gen/python/wire gen/ts/wire gen/ts/strict gen/jsonschema)" ]; then + echo "::error::Generated types export is out of sync with the proto. Run scripts/gen-sdk-types.sh and commit gen/python/wire/ + gen/ts/wire/ + gen/ts/strict/ + gen/jsonschema/." + git status --short -- gen/python/wire gen/ts/wire gen/ts/strict gen/jsonschema exit 1 fi @@ -81,7 +85,7 @@ jobs: # protovalidate by the Go conformance suite (proto-ci) and gated for drift there. - name: Python (Pydantic) parity run: | - python -m pip install -q "pydantic>=2.0" pytest + python -m pip install -q "pydantic>=2.0" "jsonschema>=4.23" pytest PYTHONPATH=gen/python pytest gen/python/tests -q # `npm test` here typechecks wire/ and vocab/ at target ES2020 with the default diff --git a/README.md b/README.md index fbfdc922..dc852d8f 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,23 @@ from wire.models import Offer, Pricing from vocab import pricingunits ``` +### JSON Schema + +The same generator run also emits one self-contained JSON Schema (draft 2020-12) per +message under [`gen/jsonschema/`](gen/jsonschema), named by the fully-qualified message +name, in a default and a strict variant. The strict variant refuses unknown fields at +every depth, so a conformance check can catch an unknown or misspelled field. All three +packages ship them: the Go module as that directory (plus `jsonschema.Load` and +`jsonschema.FS` in package `github.com/FORA-Protocol/protocol/gen/jsonschema`), the +Python package through `wire.schemas.load(name, strict=...)`, and the npm package under +`@fora-protocol/sdk/jsonschema/*`. The [schemas' README](gen/jsonschema/README.md) +records how they treat field naming, `google.protobuf.Struct` and 64-bit integers. + +```python +from wire import schemas +schema = schemas.load("fora.v1.ResourceResponse", strict=True) +``` + ## Protocol SDK Beyond the generated wire types, the repo ships a hand-written **protocol SDK** in all diff --git a/conformance/corpus/cases.json b/conformance/corpus/cases.json index 1751488c..f56ef980 100644 --- a/conformance/corpus/cases.json +++ b/conformance/corpus/cases.json @@ -1552,7 +1552,7 @@ "enum.defined_only" ], "json": { - "reason": 10 + "reason": 12 } }, { @@ -1831,6 +1831,19 @@ "unit_cost": "0" } }, + { + "id": "DiscoveryRequest/requester/missing", + "message": "DiscoveryRequest", + "valid": false, + "rules": [ + "required" + ], + "json": { + "uris": [ + "x" + ] + } + }, { "id": "DiscoveryRequest/uris/too_many", "message": "DiscoveryRequest", @@ -1839,6 +1852,14 @@ "repeated.max_items" ], "json": { + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x", "x", @@ -2105,6 +2126,14 @@ "message": "DiscoveryRequest", "valid": true, "json": { + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -2127,7 +2156,7 @@ "enum.defined_only" ], "json": { - "reason": 6 + "reason": 7 } }, { @@ -2663,7 +2692,7 @@ "enum.defined_only" ], "json": { - "reason": 7 + "reason": 8 } }, { @@ -2890,338 +2919,884 @@ } }, { - "id": "GetAccountStatusRequest/exchange/killer#0", - "message": "GetAccountStatusRequest", - "valid": false, - "rules": [ - "string.pattern" - ], - "json": { - "exchange": "https://exchange.example" - } - }, - { - "id": "GetAccountStatusRequest/exchange/killer#1", - "message": "GetAccountStatusRequest", - "valid": false, - "rules": [ - "string.pattern" - ], - "json": { - "exchange": "exchange.example/register" - } - }, - { - "id": "GetAccountStatusRequest/exchange/killer#10", - "message": "GetAccountStatusRequest", - "valid": false, - "rules": [ - "string.pattern" - ], - "json": { - "exchange": "exchange.example:0" - } - }, - { - "id": "GetAccountStatusRequest/exchange/killer#11", - "message": "GetAccountStatusRequest", - "valid": false, - "rules": [ - "string.pattern" - ], - "json": { - "exchange": "exchange.example:99999" - } - }, - { - "id": "GetAccountStatusRequest/exchange/killer#2", - "message": "GetAccountStatusRequest", - "valid": false, - "rules": [ - "string.pattern" - ], - "json": { - "exchange": "exchange.example?x=1" - } - }, - { - "id": "GetAccountStatusRequest/exchange/killer#3", - "message": "GetAccountStatusRequest", + "id": "ExchangeOutcome/exchange/killer#0", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "exchange": "user@exchange.example" + "exchange": "https://exchange.example", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusRequest/exchange/killer#4", - "message": "GetAccountStatusRequest", + "id": "ExchangeOutcome/exchange/killer#1", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "exchange": "exchange.example:123456" + "exchange": "exchange.example/register", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusRequest/exchange/killer#5", - "message": "GetAccountStatusRequest", + "id": "ExchangeOutcome/exchange/killer#10", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "exchange": "exchange.example:" + "exchange": "exchange.example:0", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusRequest/exchange/killer#6", - "message": "GetAccountStatusRequest", + "id": "ExchangeOutcome/exchange/killer#11", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "exchange": "exchange.example." + "exchange": "exchange.example:99999", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusRequest/exchange/killer#7", - "message": "GetAccountStatusRequest", + "id": "ExchangeOutcome/exchange/killer#2", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "exchange": "exchange..example" + "exchange": "exchange.example?x=1", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusRequest/exchange/killer#8", - "message": "GetAccountStatusRequest", + "id": "ExchangeOutcome/exchange/killer#3", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "exchange": "-exchange.example" + "exchange": "user@exchange.example", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusRequest/exchange/killer#9", - "message": "GetAccountStatusRequest", + "id": "ExchangeOutcome/exchange/killer#4", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "exchange": "[::1]:443" + "exchange": "exchange.example:123456", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusRequest/exchange/missing_empty", - "message": "GetAccountStatusRequest", - "valid": false, - "rules": [ - "string.pattern" - ], - "json": {} - }, - { - "id": "GetAccountStatusRequest/exchange/pattern#0", - "message": "GetAccountStatusRequest", + "id": "ExchangeOutcome/exchange/killer#5", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "exchange": "two words" + "exchange": "exchange.example:", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusRequest/exchange/pattern#2", - "message": "GetAccountStatusRequest", + "id": "ExchangeOutcome/exchange/killer#6", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "exchange": "!!bad!!" + "exchange": "exchange.example.", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusRequest/exchange/pattern#3", - "message": "GetAccountStatusRequest", + "id": "ExchangeOutcome/exchange/killer#7", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "exchange": "\u0000ctl\u0000" + "exchange": "exchange..example", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusRequest/exchange/pattern#4", - "message": "GetAccountStatusRequest", + "id": "ExchangeOutcome/exchange/killer#8", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "exchange": " " + "exchange": "-exchange.example", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusRequest/exchange/pattern#5", - "message": "GetAccountStatusRequest", + "id": "ExchangeOutcome/exchange/killer#9", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "exchange": "-5" - } - }, - { - "id": "GetAccountStatusRequest/exchange/too_long", - "message": "GetAccountStatusRequest", - "valid": false, - "rules": [ - "string.max_len" - ], - "json": { - "exchange": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - } - }, - { - "id": "GetAccountStatusRequest/valid", - "message": "GetAccountStatusRequest", - "valid": true, - "json": { - "exchange": "x" - } - }, - { - "id": "GetAccountStatusResponse/terms_digest/empty_ok", - "message": "GetAccountStatusResponse", - "valid": true, - "json": { - "active": true, - "billing_ref": "acct-seed", - "terms_digest": "", - "ver": "1.0" + "exchange": "[::1]:443", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusResponse/terms_digest/pattern#0", - "message": "GetAccountStatusResponse", + "id": "ExchangeOutcome/exchange/missing_empty", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "active": true, - "billing_ref": "acct-seed", - "terms_digest": "two words", - "ver": "1.0" + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusResponse/terms_digest/pattern#1", - "message": "GetAccountStatusResponse", + "id": "ExchangeOutcome/exchange/pattern#0", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "active": true, - "billing_ref": "acct-seed", - "terms_digest": "1.2.3", - "ver": "1.0" + "exchange": "two words", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusResponse/terms_digest/pattern#2", - "message": "GetAccountStatusResponse", + "id": "ExchangeOutcome/exchange/pattern#2", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "active": true, - "billing_ref": "acct-seed", - "terms_digest": "!!bad!!", - "ver": "1.0" + "exchange": "!!bad!!", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusResponse/terms_digest/pattern#3", - "message": "GetAccountStatusResponse", + "id": "ExchangeOutcome/exchange/pattern#3", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "active": true, - "billing_ref": "acct-seed", - "terms_digest": "\u0000ctl\u0000", - "ver": "1.0" + "exchange": "\u0000ctl\u0000", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusResponse/terms_digest/pattern#4", - "message": "GetAccountStatusResponse", + "id": "ExchangeOutcome/exchange/pattern#4", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "active": true, - "billing_ref": "acct-seed", - "terms_digest": " ", - "ver": "1.0" + "exchange": " ", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusResponse/terms_digest/pattern#5", - "message": "GetAccountStatusResponse", + "id": "ExchangeOutcome/exchange/pattern#5", + "message": "ExchangeOutcome", "valid": false, "rules": [ "string.pattern" ], "json": { - "active": true, - "billing_ref": "acct-seed", - "terms_digest": "-5", - "ver": "1.0" + "exchange": "-5", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusResponse/terms_digest/pattern#6", - "message": "GetAccountStatusResponse", + "id": "ExchangeOutcome/exchange/too_long", + "message": "ExchangeOutcome", "valid": false, "rules": [ - "string.pattern" + "string.max_len" ], "json": { - "active": true, - "billing_ref": "acct-seed", - "terms_digest": "NaN", - "ver": "1.0" + "exchange": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "offer_ids": [ + "x" + ] } }, { - "id": "GetAccountStatusResponse/terms_digest/pattern#7", - "message": "GetAccountStatusResponse", + "id": "ExchangeOutcome/offer_ids/too_many", + "message": "ExchangeOutcome", "valid": false, "rules": [ - "string.pattern" + "repeated.max_items" ], "json": { - "active": true, + "exchange": "x", + "offer_ids": [ + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x" + ] + } + }, + { + "id": "ExchangeOutcome/valid", + "message": "ExchangeOutcome", + "valid": true, + "json": { + "exchange": "x", + "offer_ids": [ + "x" + ] + } + }, + { + "id": "GetAccountStatusRequest/exchange/killer#0", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "https://exchange.example" + } + }, + { + "id": "GetAccountStatusRequest/exchange/killer#1", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "exchange.example/register" + } + }, + { + "id": "GetAccountStatusRequest/exchange/killer#10", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "exchange.example:0" + } + }, + { + "id": "GetAccountStatusRequest/exchange/killer#11", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "exchange.example:99999" + } + }, + { + "id": "GetAccountStatusRequest/exchange/killer#2", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "exchange.example?x=1" + } + }, + { + "id": "GetAccountStatusRequest/exchange/killer#3", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "user@exchange.example" + } + }, + { + "id": "GetAccountStatusRequest/exchange/killer#4", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "exchange.example:123456" + } + }, + { + "id": "GetAccountStatusRequest/exchange/killer#5", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "exchange.example:" + } + }, + { + "id": "GetAccountStatusRequest/exchange/killer#6", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "exchange.example." + } + }, + { + "id": "GetAccountStatusRequest/exchange/killer#7", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "exchange..example" + } + }, + { + "id": "GetAccountStatusRequest/exchange/killer#8", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "-exchange.example" + } + }, + { + "id": "GetAccountStatusRequest/exchange/killer#9", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "[::1]:443" + } + }, + { + "id": "GetAccountStatusRequest/exchange/missing_empty", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": {} + }, + { + "id": "GetAccountStatusRequest/exchange/pattern#0", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "two words" + } + }, + { + "id": "GetAccountStatusRequest/exchange/pattern#2", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "!!bad!!" + } + }, + { + "id": "GetAccountStatusRequest/exchange/pattern#3", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "\u0000ctl\u0000" + } + }, + { + "id": "GetAccountStatusRequest/exchange/pattern#4", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": " " + } + }, + { + "id": "GetAccountStatusRequest/exchange/pattern#5", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "exchange": "-5" + } + }, + { + "id": "GetAccountStatusRequest/exchange/too_long", + "message": "GetAccountStatusRequest", + "valid": false, + "rules": [ + "string.max_len" + ], + "json": { + "exchange": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + { + "id": "GetAccountStatusRequest/valid", + "message": "GetAccountStatusRequest", + "valid": true, + "json": { + "exchange": "x" + } + }, + { + "id": "GetAccountStatusResponse/terms_digest/empty_ok", + "message": "GetAccountStatusResponse", + "valid": true, + "json": { + "active": true, + "billing_ref": "acct-seed", + "terms_digest": "", + "ver": "1.0" + } + }, + { + "id": "GetAccountStatusResponse/terms_digest/pattern#0", + "message": "GetAccountStatusResponse", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "active": true, + "billing_ref": "acct-seed", + "terms_digest": "two words", + "ver": "1.0" + } + }, + { + "id": "GetAccountStatusResponse/terms_digest/pattern#1", + "message": "GetAccountStatusResponse", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "active": true, + "billing_ref": "acct-seed", + "terms_digest": "1.2.3", + "ver": "1.0" + } + }, + { + "id": "GetAccountStatusResponse/terms_digest/pattern#2", + "message": "GetAccountStatusResponse", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "active": true, + "billing_ref": "acct-seed", + "terms_digest": "!!bad!!", + "ver": "1.0" + } + }, + { + "id": "GetAccountStatusResponse/terms_digest/pattern#3", + "message": "GetAccountStatusResponse", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "active": true, + "billing_ref": "acct-seed", + "terms_digest": "\u0000ctl\u0000", + "ver": "1.0" + } + }, + { + "id": "GetAccountStatusResponse/terms_digest/pattern#4", + "message": "GetAccountStatusResponse", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "active": true, + "billing_ref": "acct-seed", + "terms_digest": " ", + "ver": "1.0" + } + }, + { + "id": "GetAccountStatusResponse/terms_digest/pattern#5", + "message": "GetAccountStatusResponse", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "active": true, + "billing_ref": "acct-seed", + "terms_digest": "-5", + "ver": "1.0" + } + }, + { + "id": "GetAccountStatusResponse/terms_digest/pattern#6", + "message": "GetAccountStatusResponse", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "active": true, + "billing_ref": "acct-seed", + "terms_digest": "NaN", + "ver": "1.0" + } + }, + { + "id": "GetAccountStatusResponse/terms_digest/pattern#7", + "message": "GetAccountStatusResponse", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "active": true, "billing_ref": "acct-seed", "terms_digest": "Infinity", "ver": "1.0" @@ -3654,17 +4229,6 @@ "semantics": "TERM_SEMANTICS_ENUMERATED" } }, - { - "id": "LicenseTerm/pricing/missing", - "message": "LicenseTerm", - "valid": false, - "rules": [ - "required" - ], - "json": { - "semantics": "TERM_SEMANTICS_ENUMERATED" - } - }, { "id": "LicenseTerm/quotas/too_many", "message": "LicenseTerm", @@ -4235,10 +4799,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4262,10 +4822,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4289,10 +4845,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4316,10 +4868,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4343,10 +4891,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4370,10 +4914,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4397,10 +4937,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4424,10 +4960,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4451,10 +4983,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4478,10 +5006,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4505,10 +5029,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4532,10 +5052,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4558,10 +5074,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4585,10 +5097,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4612,10 +5120,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4639,10 +5143,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4666,10 +5166,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4693,10 +5189,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4720,10 +5212,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -4734,6 +5222,7 @@ "message": "Offer", "valid": false, "rules": [ + "offer.terms.pricing_unset", "repeated.max_items" ], "json": { @@ -4747,10 +5236,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" }, { @@ -4778,10 +5263,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -13995,7 +14476,7 @@ "error": "matched 2 branches of oneOf, exactly 1 required" } ], - "reason": 8 + "reason": 9 } }, { @@ -14066,6 +14547,12 @@ "exchange": "https://exchange.example", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14080,6 +14567,12 @@ "exchange": "exchange.example/register", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14094,6 +14587,12 @@ "exchange": "exchange.example:0", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14108,6 +14607,12 @@ "exchange": "exchange.example:99999", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14122,6 +14627,12 @@ "exchange": "exchange.example?x=1", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14136,6 +14647,12 @@ "exchange": "user@exchange.example", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14150,6 +14667,12 @@ "exchange": "exchange.example:123456", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14164,6 +14687,12 @@ "exchange": "exchange.example:", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14178,6 +14707,12 @@ "exchange": "exchange.example.", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14192,6 +14727,12 @@ "exchange": "exchange..example", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14206,6 +14747,12 @@ "exchange": "-exchange.example", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14220,6 +14767,12 @@ "exchange": "[::1]:443", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14233,6 +14786,12 @@ "json": { "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14247,6 +14806,12 @@ "exchange": "two words", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14261,6 +14826,12 @@ "exchange": "!!bad!!", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14275,6 +14846,12 @@ "exchange": "\u0000ctl\u0000", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14289,6 +14866,12 @@ "exchange": " ", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14303,6 +14886,12 @@ "exchange": "-5", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14317,6 +14906,12 @@ "exchange": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14332,6 +14927,12 @@ "paths": [ "/x", "x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14347,6 +14948,12 @@ "paths": [ "/x", "/a?b" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14362,6 +14969,12 @@ "paths": [ "/x", "/a#b" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14377,6 +14990,12 @@ "paths": [ "/x", "/a b" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14392,6 +15011,12 @@ "paths": [ "/x", "/a" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14407,6 +15032,12 @@ "paths": [ "/x", "two words" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14423,6 +15054,12 @@ "paths": [ "/x", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14439,6 +15076,12 @@ "paths": [ "/x", "" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14709,6 +15352,1056 @@ "/x", "/x", "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } + ] + } + }, + { + "id": "RemoveResourcesRequest/resources/too_many", + "message": "RemoveResourcesRequest", + "valid": false, + "rules": [ + "repeated.max_items" + ], + "json": { + "exchange": "x", + "paths": [ + "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + }, + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14720,6 +16413,12 @@ "exchange": "x", "paths": [ "/x" + ], + "resources": [ + { + "domain": "x", + "path": "/x" + } ] } }, @@ -14929,6 +16628,34 @@ "window_seconds": 0 } }, + { + "id": "RequestAuthFailure/reason/not_in", + "message": "RequestAuthFailure", + "valid": false, + "rules": [ + "enum.not_in" + ], + "json": {} + }, + { + "id": "RequestAuthFailure/reason/undefined", + "message": "RequestAuthFailure", + "valid": false, + "rules": [ + "enum.defined_only" + ], + "json": { + "reason": 4 + } + }, + { + "id": "RequestAuthFailure/valid", + "message": "RequestAuthFailure", + "valid": true, + "json": { + "reason": "REQUEST_AUTH_FAILURE_REASON_SIGNATURE_MISSING" + } + }, { "id": "RequestConstraints/exchanges/item_killer#0", "message": "RequestConstraints", @@ -15721,6 +17448,7 @@ ], "json": { "domain": "https://exchange.example", + "id": "x", "scopes": [ "x" ], @@ -15736,6 +17464,7 @@ ], "json": { "domain": "exchange.example/register", + "id": "x", "scopes": [ "x" ], @@ -15751,6 +17480,7 @@ ], "json": { "domain": "exchange.example:0", + "id": "x", "scopes": [ "x" ], @@ -15766,6 +17496,7 @@ ], "json": { "domain": "exchange.example:99999", + "id": "x", "scopes": [ "x" ], @@ -15781,6 +17512,7 @@ ], "json": { "domain": "exchange.example?x=1", + "id": "x", "scopes": [ "x" ], @@ -15796,6 +17528,7 @@ ], "json": { "domain": "user@exchange.example", + "id": "x", "scopes": [ "x" ], @@ -15811,6 +17544,7 @@ ], "json": { "domain": "exchange.example:123456", + "id": "x", "scopes": [ "x" ], @@ -15826,6 +17560,7 @@ ], "json": { "domain": "exchange.example:", + "id": "x", "scopes": [ "x" ], @@ -15841,6 +17576,7 @@ ], "json": { "domain": "exchange.example.", + "id": "x", "scopes": [ "x" ], @@ -15856,6 +17592,7 @@ ], "json": { "domain": "exchange..example", + "id": "x", "scopes": [ "x" ], @@ -15871,6 +17608,7 @@ ], "json": { "domain": "-exchange.example", + "id": "x", "scopes": [ "x" ], @@ -15886,6 +17624,7 @@ ], "json": { "domain": "[::1]:443", + "id": "x", "scopes": [ "x" ], @@ -15900,6 +17639,7 @@ "string.pattern" ], "json": { + "id": "x", "scopes": [ "x" ], @@ -15915,6 +17655,7 @@ ], "json": { "domain": "two words", + "id": "x", "scopes": [ "x" ], @@ -15930,6 +17671,7 @@ ], "json": { "domain": "!!bad!!", + "id": "x", "scopes": [ "x" ], @@ -15945,6 +17687,7 @@ ], "json": { "domain": "\u0000ctl\u0000", + "id": "x", "scopes": [ "x" ], @@ -15960,6 +17703,7 @@ ], "json": { "domain": " ", + "id": "x", "scopes": [ "x" ], @@ -15975,6 +17719,7 @@ ], "json": { "domain": "-5", + "id": "x", "scopes": [ "x" ], @@ -15990,6 +17735,38 @@ ], "json": { "domain": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + } + }, + { + "id": "Requester/id/too_long", + "message": "Requester", + "valid": false, + "rules": [ + "string.max_len" + ], + "json": { + "domain": "x", + "id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + } + }, + { + "id": "Requester/id/too_short", + "message": "Requester", + "valid": false, + "rules": [ + "string.min_len" + ], + "json": { + "domain": "x", "scopes": [ "x" ], @@ -16005,6 +17782,7 @@ ], "json": { "domain": "x", + "id": "x", "scopes": [ "x", "x", @@ -16084,6 +17862,7 @@ ], "json": { "domain": "x", + "id": "x", "scopes": [ "x" ] @@ -16095,6 +17874,7 @@ "valid": true, "json": { "domain": "x", + "id": "x", "scopes": [ "x" ], @@ -18217,6 +19997,14 @@ "json": { "deadline": "90s", "exchange": "https://exchange.example", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18232,6 +20020,14 @@ "json": { "deadline": "90s", "exchange": "exchange.example/register", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18247,6 +20043,14 @@ "json": { "deadline": "90s", "exchange": "exchange.example:0", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18262,6 +20066,14 @@ "json": { "deadline": "90s", "exchange": "exchange.example:99999", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18277,6 +20089,14 @@ "json": { "deadline": "90s", "exchange": "exchange.example?x=1", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18292,6 +20112,14 @@ "json": { "deadline": "90s", "exchange": "user@exchange.example", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18307,6 +20135,14 @@ "json": { "deadline": "90s", "exchange": "exchange.example:123456", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18322,6 +20158,14 @@ "json": { "deadline": "90s", "exchange": "exchange.example:", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18337,6 +20181,14 @@ "json": { "deadline": "90s", "exchange": "exchange.example.", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18352,6 +20204,14 @@ "json": { "deadline": "90s", "exchange": "exchange..example", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18367,6 +20227,14 @@ "json": { "deadline": "90s", "exchange": "-exchange.example", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18382,6 +20250,14 @@ "json": { "deadline": "90s", "exchange": "[::1]:443", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18396,6 +20272,14 @@ ], "json": { "deadline": "90s", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18411,6 +20295,14 @@ "json": { "deadline": "90s", "exchange": "two words", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18426,6 +20318,14 @@ "json": { "deadline": "90s", "exchange": "!!bad!!", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18441,6 +20341,14 @@ "json": { "deadline": "90s", "exchange": "\u0000ctl\u0000", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18456,6 +20364,14 @@ "json": { "deadline": "90s", "exchange": " ", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18471,6 +20387,14 @@ "json": { "deadline": "90s", "exchange": "-5", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x" ] @@ -18486,6 +20410,29 @@ "json": { "deadline": "90s", "exchange": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, + "uris": [ + "x" + ] + } + }, + { + "id": "ResourceQuery/requester/missing", + "message": "ResourceQuery", + "valid": false, + "rules": [ + "required" + ], + "json": { + "deadline": "90s", + "exchange": "x", "uris": [ "x" ] @@ -18501,6 +20448,14 @@ "json": { "deadline": "90s", "exchange": "x", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, "uris": [ "x", "x", @@ -18763,15 +20718,464 @@ } }, { - "id": "ResourceQuery/valid", - "message": "ResourceQuery", + "id": "ResourceQuery/valid", + "message": "ResourceQuery", + "valid": true, + "json": { + "deadline": "90s", + "exchange": "x", + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + }, + "uris": [ + "x" + ] + } + }, + { + "id": "ResourceRef/domain/killer#0", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "https://exchange.example", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/killer#1", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "exchange.example/register", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/killer#10", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "exchange.example:0", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/killer#11", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "exchange.example:99999", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/killer#2", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "exchange.example?x=1", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/killer#3", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "user@exchange.example", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/killer#4", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "exchange.example:123456", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/killer#5", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "exchange.example:", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/killer#6", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "exchange.example.", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/killer#7", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "exchange..example", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/killer#8", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "-exchange.example", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/killer#9", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "[::1]:443", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/missing_empty", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/pattern#0", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "two words", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/pattern#2", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "!!bad!!", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/pattern#3", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "\u0000ctl\u0000", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/pattern#4", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": " ", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/pattern#5", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "-5", + "path": "/x" + } + }, + { + "id": "ResourceRef/domain/too_long", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.max_len" + ], + "json": { + "domain": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "path": "/x" + } + }, + { + "id": "ResourceRef/path/killer#0", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "x", + "path": "x" + } + }, + { + "id": "ResourceRef/path/killer#1", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "x", + "path": "/a?b" + } + }, + { + "id": "ResourceRef/path/killer#2", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "x", + "path": "/a#b" + } + }, + { + "id": "ResourceRef/path/killer#3", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "x", + "path": "/a b" + } + }, + { + "id": "ResourceRef/path/killer#4", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "x", + "path": "/a" + } + }, + { + "id": "ResourceRef/path/missing_empty", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.min_len", + "string.pattern" + ], + "json": { + "domain": "x" + } + }, + { + "id": "ResourceRef/path/pattern#0", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "x", + "path": "two words" + } + }, + { + "id": "ResourceRef/path/pattern#1", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "x", + "path": "1.2.3" + } + }, + { + "id": "ResourceRef/path/pattern#2", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "x", + "path": "!!bad!!" + } + }, + { + "id": "ResourceRef/path/pattern#3", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "x", + "path": "\u0000ctl\u0000" + } + }, + { + "id": "ResourceRef/path/pattern#4", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "x", + "path": " " + } + }, + { + "id": "ResourceRef/path/pattern#5", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "x", + "path": "-5" + } + }, + { + "id": "ResourceRef/path/pattern#6", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "x", + "path": "NaN" + } + }, + { + "id": "ResourceRef/path/pattern#7", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "x", + "path": "Infinity" + } + }, + { + "id": "ResourceRef/path/pattern#8", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "domain": "x", + "path": "1E3" + } + }, + { + "id": "ResourceRef/path/too_long", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.max_len", + "string.pattern" + ], + "json": { + "domain": "x", + "path": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + { + "id": "ResourceRef/path/too_short", + "message": "ResourceRef", + "valid": false, + "rules": [ + "string.min_len", + "string.pattern" + ], + "json": { + "domain": "x" + } + }, + { + "id": "ResourceRef/valid", + "message": "ResourceRef", "valid": true, "json": { - "deadline": "90s", - "exchange": "x", - "uris": [ - "x" - ] + "domain": "x", + "path": "/x" } }, { @@ -19694,7 +22098,7 @@ ], "json": { "exchange": "x", - "reason": 19 + "reason": 21 } }, { @@ -19729,10 +22133,6 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -19759,16 +22159,20 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] } } - ] + ], + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + } } }, { @@ -19790,10 +22194,42 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, + "semantics": "TERM_SEMANTICS_ENUMERATED" + } + ] + } + } + ], + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + } + } + }, + { + "id": "TransactionRequest/requester/missing", + "message": "TransactionRequest", + "valid": false, + "rules": [ + "required" + ], + "json": { + "idempotency_key": "idem-tx", + "items": [ + { + "offer": { + "exchange": "exchange.example", + "offer_id": "offer-seed", + "pricing": { + "model": "PRICING_MODEL_FREE", + "rate": "0" + }, + "terms": [ + { "semantics": "TERM_SEMANTICS_ENUMERATED" } ] @@ -19819,16 +22255,279 @@ }, "terms": [ { - "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0" - }, "semantics": "TERM_SEMANTICS_ENUMERATED" } ] } } - ] + ], + "requester": { + "domain": "x", + "id": "x", + "scopes": [ + "x" + ], + "type": "REQUESTER_TYPE_AGENT" + } + } + }, + { + "id": "UpstreamRefusal/code/too_long", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.max_len" + ], + "json": { + "code": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "party": "x" + } + }, + { + "id": "UpstreamRefusal/code/too_short", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.min_len" + ], + "json": { + "party": "x" + } + }, + { + "id": "UpstreamRefusal/party/killer#0", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "https://exchange.example" + } + }, + { + "id": "UpstreamRefusal/party/killer#1", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "exchange.example/register" + } + }, + { + "id": "UpstreamRefusal/party/killer#10", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "exchange.example:0" + } + }, + { + "id": "UpstreamRefusal/party/killer#11", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "exchange.example:99999" + } + }, + { + "id": "UpstreamRefusal/party/killer#2", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "exchange.example?x=1" + } + }, + { + "id": "UpstreamRefusal/party/killer#3", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "user@exchange.example" + } + }, + { + "id": "UpstreamRefusal/party/killer#4", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "exchange.example:123456" + } + }, + { + "id": "UpstreamRefusal/party/killer#5", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "exchange.example:" + } + }, + { + "id": "UpstreamRefusal/party/killer#6", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "exchange.example." + } + }, + { + "id": "UpstreamRefusal/party/killer#7", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "exchange..example" + } + }, + { + "id": "UpstreamRefusal/party/killer#8", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "-exchange.example" + } + }, + { + "id": "UpstreamRefusal/party/killer#9", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "[::1]:443" + } + }, + { + "id": "UpstreamRefusal/party/missing_empty", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x" + } + }, + { + "id": "UpstreamRefusal/party/pattern#0", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "two words" + } + }, + { + "id": "UpstreamRefusal/party/pattern#2", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "!!bad!!" + } + }, + { + "id": "UpstreamRefusal/party/pattern#3", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "\u0000ctl\u0000" + } + }, + { + "id": "UpstreamRefusal/party/pattern#4", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": " " + } + }, + { + "id": "UpstreamRefusal/party/pattern#5", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.pattern" + ], + "json": { + "code": "x", + "party": "-5" + } + }, + { + "id": "UpstreamRefusal/party/too_long", + "message": "UpstreamRefusal", + "valid": false, + "rules": [ + "string.max_len" + ], + "json": { + "code": "x", + "party": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + { + "id": "UpstreamRefusal/valid", + "message": "UpstreamRefusal", + "valid": true, + "json": { + "code": "x", + "party": "x" } }, { @@ -20244,7 +22943,7 @@ "enum.defined_only" ], "json": { - "reason": 6 + "reason": 7 } }, { diff --git a/conformance/corpus/crossfield.json b/conformance/corpus/crossfield.json index cbdf8288..65a58c08 100644 --- a/conformance/corpus/crossfield.json +++ b/conformance/corpus/crossfield.json @@ -169,8 +169,7 @@ "message": "LicenseTerm", "valid": false, "rules": [ - "license_term.reference_only.requires_uri", - "required" + "license_term.reference_only.requires_uri" ], "json": { "semantics": "TERM_SEMANTICS_REFERENCE_ONLY" @@ -188,6 +187,113 @@ "trigger": "OBLIGATION_TRIGGER_ON_USE" } }, + { + "id": "Offer/cel/metered_estimate_positive/negative", + "message": "Offer", + "valid": false, + "rules": [ + "offer.metered.estimate_positive" + ], + "json": { + "exchange": "exchange.example", + "offer_id": "offer-metered", + "pricing": { + "currency": "USD", + "estimated_quantity": -1, + "model": "PRICING_MODEL_PER_UNIT", + "rate": "0.00002", + "unit": "tokens" + }, + "terms": [ + { + "semantics": "TERM_SEMANTICS_ENUMERATED" + } + ] + } + }, + { + "id": "Offer/cel/metered_estimate_positive/zero", + "message": "Offer", + "valid": false, + "rules": [ + "offer.metered.estimate_positive" + ], + "json": { + "exchange": "exchange.example", + "offer_id": "offer-metered", + "pricing": { + "currency": "USD", + "estimated_quantity": 0, + "model": "PRICING_MODEL_PER_UNIT", + "rate": "0.00002", + "unit": "tokens" + }, + "terms": [ + { + "semantics": "TERM_SEMANTICS_ENUMERATED" + } + ] + } + }, + { + "id": "Offer/cel/terms_pricing_unset/per_unit_term", + "message": "Offer", + "valid": false, + "rules": [ + "offer.terms.pricing_unset" + ], + "json": { + "exchange": "exchange.example", + "offer_id": "offer-metered", + "pricing": { + "currency": "USD", + "model": "PRICING_MODEL_FLAT", + "rate": "1.00" + }, + "terms": [ + { + "pricing": { + "currency": "USD", + "model": "PRICING_MODEL_PER_UNIT", + "rate": "0.00002", + "unit": "tokens" + }, + "semantics": "TERM_SEMANTICS_ENUMERATED" + } + ] + } + }, + { + "id": "Offer/cel/terms_pricing_unset/same_price", + "message": "Offer", + "valid": false, + "rules": [ + "offer.terms.pricing_unset" + ], + "json": { + "exchange": "exchange.example", + "offer_id": "offer-metered", + "pricing": { + "currency": "USD", + "estimated_quantity": 2500, + "model": "PRICING_MODEL_PER_UNIT", + "rate": "0.00002", + "unit": "tokens" + }, + "terms": [ + { + "pricing": { + "currency": "USD", + "estimated_quantity": 2500, + "model": "PRICING_MODEL_PER_UNIT", + "rate": "0.00002", + "unit": "tokens" + }, + "semantics": "TERM_SEMANTICS_ENUMERATED" + } + ] + } + }, { "id": "Pricing/cel/free_zero_rate", "message": "Pricing", @@ -230,6 +336,23 @@ "reason": "REGISTRATION_FAILURE_REASON_TERMS_DIGEST_STALE" } }, + { + "id": "ResourceEntry/cel/terms_pricing_required", + "message": "ResourceEntry", + "valid": false, + "rules": [ + "resource_entry.terms.pricing_required" + ], + "json": { + "domain": "publisher.example", + "path": "/article", + "terms": [ + { + "semantics": "TERM_SEMANTICS_ENUMERATED" + } + ] + } + }, { "id": "Restriction/cel/permitted_prohibited_disjoint", "message": "Restriction", diff --git a/conformance/corpusgen/main.go b/conformance/corpusgen/main.go index 32e17c51..334445fb 100644 --- a/conformance/corpusgen/main.go +++ b/conformance/corpusgen/main.go @@ -68,7 +68,8 @@ func seeds() map[string]proto.Message { // and the audience statement of a TransactionRequest), so a seed without it // is not a valid baseline — seeds bypass auto-fill entirely. terms is bounded // to exactly one: an offer IS one licensing arrangement, so the baseline - // carries the single term it sells rather than an empty list. + // carries the single term it sells rather than an empty list. The term + // carries no pricing: the offer states its price once, in Offer.pricing. offer := func() *forav1.Offer { return &forav1.Offer{ OfferId: "offer-seed", @@ -76,10 +77,21 @@ func seeds() map[string]proto.Message { Pricing: pricing(), Terms: []*forav1.LicenseTerm{{ Semantics: forav1.TermSemantics_TERM_SEMANTICS_ENUMERATED, - Pricing: pricing(), }}, } } + // requester is the required sub-message of ResourceQuery, DiscoveryRequest and + // TransactionRequest, so auto-fill needs its seed. It is exactly the Requester the + // auto-fill built before the field was required, so the Requester/* cases are + // unchanged. + requester := func() *forav1.Requester { + return &forav1.Requester{ + Id: "x", + Domain: "x", + Type: forav1.RequesterType_REQUESTER_TYPE_AGENT, + Scopes: []string{"x"}, + } + } return map[string]proto.Message{ "Pricing": pricing(), "License": &forav1.License{Id: proto.String("CC-BY-4.0")}, @@ -97,7 +109,8 @@ func seeds() map[string]proto.Message { // and TransactionRequest needs a valid 1-item items[] baseline because its // items field is now repeated.min_items=1 (single-offer mode removed). "Offer": offer(), - "TransactionRequest": &forav1.TransactionRequest{IdempotencyKey: "idem-tx", Items: []*forav1.TransactionItem{{Offer: offer()}}}, + "Requester": requester(), + "TransactionRequest": &forav1.TransactionRequest{IdempotencyKey: "idem-tx", Requester: requester(), Items: []*forav1.TransactionItem{{Offer: offer()}}}, "AgentRequestAcceptancePayload": &forav1.AgentRequestAcceptancePayload{ Items: []*forav1.AgentRequestAcceptanceItem{{ OfferSig: "offer-signature", @@ -212,8 +225,8 @@ var patternKillers = map[string][]string{ }, } -// resourcePathPattern is the absolute-path shape ResourceEntry.path and -// RemoveResourcesRequest.paths carry, quoted from the proto for the same +// resourcePathPattern is the absolute-path shape ResourceEntry.path, +// ResourceRef.path and the deprecated RemoveResourcesRequest.paths carry, quoted from the proto for the same // reason bareDomainPattern is — and, like it, a drift between this copy and the // fields is caught by conformance's own descriptor guard, not here. That matters // more for this one than for a plain constant: the killer table above is keyed by @@ -321,6 +334,56 @@ func writeCrossField(v protovalidate.Validator) { &forav1.Pricing{Model: forav1.PricingModel_PRICING_MODEL_FREE, Rate: "5"}, "pricing.free.zero_rate", }, + { + // A metered offer may state no estimate (the purchase then charges + // one unit), but an estimate it does state is positive: zero would + // price the purchase at nothing. + "Offer/cel/metered_estimate_positive/zero", + meteredOffer(proto.Int32(0)), + "offer.metered.estimate_positive", + }, + { + "Offer/cel/metered_estimate_positive/negative", + meteredOffer(proto.Int32(-1)), + "offer.metered.estimate_positive", + }, + { + // An offer states its price once, in Offer.pricing: a term that + // carries pricing of its own is refused, even when the two prices + // agree. + "Offer/cel/terms_pricing_unset/same_price", + func() *forav1.Offer { + o := meteredOffer(proto.Int32(2500)) + o.Terms[0].Pricing = proto.Clone(o.Pricing).(*forav1.Pricing) + return o + }(), + "offer.terms.pricing_unset", + }, + { + // A PER_UNIT term under FLAT offer pricing no longer makes the offer + // metered — the metered rule reads Offer.pricing only — but the term's + // pricing is refused for being there at all. + "Offer/cel/terms_pricing_unset/per_unit_term", + func() *forav1.Offer { + o := meteredOffer(nil) + o.Pricing = &forav1.Pricing{Model: forav1.PricingModel_PRICING_MODEL_FLAT, Rate: "1.00", Currency: "USD"} + o.Terms[0].Pricing = &forav1.Pricing{ + Model: forav1.PricingModel_PRICING_MODEL_PER_UNIT, Rate: "0.00002", Currency: "USD", Unit: proto.String("tokens"), + } + return o + }(), + "offer.terms.pricing_unset", + }, + { + // A catalog term carries its price: absent Pricing is not free. + "ResourceEntry/cel/terms_pricing_required", + &forav1.ResourceEntry{ + Domain: "publisher.example", + Path: "/article", + Terms: []*forav1.LicenseTerm{{Semantics: forav1.TermSemantics_TERM_SEMANTICS_ENUMERATED}}, + }, + "resource_entry.terms.pricing_required", + }, { "License/cel/digest_required_with_uri", &forav1.License{Id: proto.String("CC-BY-4.0"), Uri: proto.String("https://example.com/license")}, @@ -466,6 +529,23 @@ func writeCrossField(v protovalidate.Validator) { fmt.Printf("wrote %d cross-field cases -> conformance/corpus/crossfield.json\n", len(cases)) } +// meteredOffer is a PER_UNIT offer, otherwise valid, whose pricing carries the +// given estimate (nil leaves it unset). The term carries no pricing: the offer +// states its price once, in Offer.pricing. +func meteredOffer(estimate *int32) *forav1.Offer { + return &forav1.Offer{ + OfferId: "offer-metered", + Exchange: "exchange.example", + Pricing: &forav1.Pricing{ + Model: forav1.PricingModel_PRICING_MODEL_PER_UNIT, Rate: "0.00002", Currency: "USD", Unit: proto.String("tokens"), + EstimatedQuantity: estimate, + }, + Terms: []*forav1.LicenseTerm{{ + Semantics: forav1.TermSemantics_TERM_SEMANTICS_ENUMERATED, + }}, + } +} + // ── baseline construction ──────────────────────────────────────────────────── func baseline(md protoreflect.MessageDescriptor, sd map[string]proto.Message) (protoreflect.Message, error) { diff --git a/conformance/domain_constraint_test.go b/conformance/domain_constraint_test.go index 5db0cf85..a829a7c3 100644 --- a/conformance/domain_constraint_test.go +++ b/conformance/domain_constraint_test.go @@ -43,7 +43,7 @@ var malformedDomains = []struct{ name, value string }{ // wantDomainFields is the number of fields the shared domain constraint is meant // to be on. It is a ratchet, not a description: see the exact-count check below. -const wantDomainFields = 18 +const wantDomainFields = 21 // digestPattern is the "method:hexdigest" shape. Several fields carry it, and which // ones is read from the descriptor by the membership check below rather than listed @@ -54,8 +54,8 @@ const digestPattern = `^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f] const wantDigestFields = 4 -// resourcePathPattern is the absolute-path shape, carried by ResourceEntry.path -// and RemoveResourcesRequest.paths. It is the third shared pattern in the +// resourcePathPattern is the absolute-path shape, carried by ResourceEntry.path, +// ResourceRef.path and the deprecated RemoveResourcesRequest.paths. It is the third shared pattern in the // contract, and it gets the same descriptor-derived membership check the other two // have for a reason the corpus generator depends on: corpusgen keys its // pattern-specific killer table BY THE PATTERN STRING, so if the proto's copy moves @@ -64,7 +64,7 @@ const wantDigestFields = 4 // makes that drift loud. const resourcePathPattern = `^/[^?#\x00-\x20\x7f]*$` -const wantResourcePathFields = 2 +const wantResourcePathFields = 3 func fieldNames(fs []domainField) string { names := make([]string, 0, len(fs)) diff --git a/conformance/jsonschema_test.go b/conformance/jsonschema_test.go new file mode 100644 index 00000000..e88189d5 --- /dev/null +++ b/conformance/jsonschema_test.go @@ -0,0 +1,149 @@ +package conformance + +// The published JSON Schemas (gen/jsonschema/) against the proto. +// +// The drift gate proves the committed schemas are what the generator emits today; this +// proves what the generator emits is right about the contract, read from the compiled +// descriptors, independently of the generator's own guards: +// +// - exactly one default and one strict file per contract message, and no other file; +// - each schema's properties are the message's proto field names (the snake_case wire +// naming), in both variants; +// - the strict variant closes every message object it defines, the default variant +// closes none, and neither closes a google.protobuf.Struct field; +// - a 64-bit integer field accepts the decimal-string form canonical proto-JSON emits. +// +// The verdicts themselves (a value accepted or refused) are asserted against the +// conformance corpus by gen/python/tests/test_jsonschema.py and +// sdk/ts/tests/jsonschema.test.ts; Go has no draft 2020-12 validator in this module. + +import ( + "encoding/json" + "io/fs" + "sort" + "strings" + "testing" + + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/FORA-Protocol/protocol/gen/jsonschema" +) + +type schemaDoc struct { + ID string `json:"$id"` + Properties map[string]json.RawMessage `json:"properties"` + Defs map[string]json.RawMessage `json:"$defs"` + Additional *bool `json:"additionalProperties"` +} + +func loadSchema(t *testing.T, name string, strict bool) schemaDoc { + t.Helper() + b, err := jsonschema.Load(name, strict) + if err != nil { + t.Fatal(err) + } + var d schemaDoc + if err := json.Unmarshal(b, &d); err != nil { + t.Fatalf("%s: %v", name, err) + } + return d +} + +func TestPublishedSchemasCoverExactlyTheContract(t *testing.T) { + var want []string + EachMessage(func(md protoreflect.MessageDescriptor) { + n := string(md.FullName()) + want = append(want, n+".schema.json", n+".schema.strict.json") + }) + got, err := fs.Glob(jsonschema.FS, "*.json") + if err != nil { + t.Fatal(err) + } + sort.Strings(want) + sort.Strings(got) + if strings.Join(got, "\n") != strings.Join(want, "\n") { + t.Fatalf("gen/jsonschema/ does not match the contract messages:\n got %d files\nwant %d files", len(got), len(want)) + } + if _, err := jsonschema.Load("fora.v1.NoSuchMessage", true); err == nil { + t.Fatal("Load accepted an unknown message name") + } + if _, err := jsonschema.Load("../jsonschema/fora.v1.ResourceResponse", false); err == nil { + t.Fatal("Load accepted a path outside the schema directory") + } +} + +func TestPublishedSchemasMatchTheDescriptor(t *testing.T) { + EachMessage(func(md protoreflect.MessageDescriptor) { + name := string(md.FullName()) + for _, strict := range []bool{false, true} { + d := loadSchema(t, name, strict) + checkFields(t, d, md) + checkClosed(t, d, strict) + } + }) +} + +// checkFields asserts the property set is the proto field names, that a Struct field +// is an open object and that a 64-bit integer admits its decimal-string form. +func checkFields(t *testing.T, d schemaDoc, md protoreflect.MessageDescriptor) { + t.Helper() + fields := md.Fields() + if len(d.Properties) != fields.Len() { + t.Errorf("%s: %d properties, the message has %d fields", d.ID, len(d.Properties), fields.Len()) + } + for i := 0; i < fields.Len(); i++ { + fd := fields.Get(i) + raw, ok := d.Properties[string(fd.Name())] + if !ok { + t.Errorf("%s: no property for field %s", d.ID, fd.Name()) + continue + } + var prop map[string]any + if err := json.Unmarshal(raw, &prop); err != nil { + t.Fatal(err) + } + if fd.Message() != nil && fd.Message().FullName() == "google.protobuf.Struct" { + if prop["type"] != "object" || prop["additionalProperties"] != nil { + t.Errorf("%s: Struct field %s is not an open object: %s", d.ID, fd.Name(), raw) + } + } + switch fd.Kind() { + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind, + protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + if !hasStringArm(prop) { + t.Errorf("%s: int64 field %s has no decimal-string form: %s", d.ID, fd.Name(), raw) + } + } + } +} + +func hasStringArm(prop map[string]any) bool { + arms, _ := prop["anyOf"].([]any) + for _, a := range arms { + if arm, ok := a.(map[string]any); ok && arm["type"] == "string" { + return true + } + } + return false +} + +// checkClosed asserts the unknown-field policy of every message object in the file. +func checkClosed(t *testing.T, d schemaDoc, strict bool) { + t.Helper() + closed := func(where string, additional *bool) { + switch { + case strict && (additional == nil || *additional): + t.Errorf("%s: %s is not closed in the strict variant", d.ID, where) + case !strict && additional != nil: + t.Errorf("%s: %s sets additionalProperties in the default variant", d.ID, where) + } + } + closed("the root message", d.Additional) + for name, raw := range d.Defs { + var def schemaDoc + if err := json.Unmarshal(raw, &def); err != nil { + t.Fatal(err) + } + closed(name, def.Additional) + } +} diff --git a/conformance/reachability_test.go b/conformance/reachability_test.go index 747a4619..9ca15a8b 100644 --- a/conformance/reachability_test.go +++ b/conformance/reachability_test.go @@ -1,9 +1,13 @@ // Package conformance — reachability_test.go holds INV-4, the orphan-type guard. // // The contract reaches the wire through exactly three delivery channels: -// 1. RPC bodies — every service method's input and output message -// 2. served documents — the manifest a participant publishes at .well-known -// 3. the error envelope — ErrorDetail and the typed failure reasons it carries +// 1. RPC bodies — every service method's input and output message +// 2. served documents — the manifest a participant publishes at .well-known +// 3. the error envelope — ErrorDetail and the typed failure reasons it carries +// +// The error envelope needs no root of its own: ErrorDetail is also reachable from +// an RPC body, through UpstreamRefusal.detail on BrokerService.ExecuteTransaction, +// so the walk from the RPC roots already covers it and its reasons. // // A message or enum that NO channel can carry is an orphan: schema that is // defined but undeliverable — dead weight, or (worse) a wiring mistake where a @@ -23,15 +27,14 @@ import ( ) // outOfBandRoots are the delivery roots that are NOT an RPC input/output: served -// documents and the transport-error envelope. Each states why it is a root. The +// documents and the never-sent signing payload. Each states why it is a root. The // test proves every entry is BOTH a real top-level message AND genuinely not // already reachable from an RPC body — so an entry that becomes RPC-reachable is // flagged as stale instead of silently masking an orphan beneath it. var outOfBandRoots = map[string]string{ - "WellKnownManifest": "served at /.well-known/fora.json by every participant (capabilities, role; identity keys moved to WBAFile)", - "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", + "WellKnownManifest": "served at /.well-known/fora.json by every participant (capabilities, role; identity keys moved to WBAFile)", + "WBAFile": "served at /.well-known/http-message-signatures-directory — the pure WBA JWK Set (identity keys + revocation_url)", + "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 — it fixes the field set the signer and verifier canonicalize (RFC 8785 JCS over canonical proto-JSON) to derive byte-identical signed bytes", } diff --git a/conformance/validate_test.go b/conformance/validate_test.go index 6a884c4d..0393df77 100644 --- a/conformance/validate_test.go +++ b/conformance/validate_test.go @@ -115,6 +115,24 @@ func licensingCases() []validationCase { {"pricing free zero rate ok", &forav1.Pricing{Model: forav1.PricingModel_PRICING_MODEL_FREE, Rate: "0"}, true, ""}, {"pricing free nonzero rate rejected", &forav1.Pricing{Model: forav1.PricingModel_PRICING_MODEL_FREE, Rate: "1.0"}, false, "pricing.free.zero_rate"}, + // Offer message-level CEL: a metered offer may state an estimate on its + // own pricing, positive when stated, and the offer's term carries no + // pricing at all. + {"offer metered with estimate ok", meteredOffer(meteredPricing(proto.Int32(2500))), true, ""}, + {"offer metered without estimate ok", meteredOffer(meteredPricing(nil)), true, ""}, + {"offer metered zero estimate rejected", meteredOffer(meteredPricing(proto.Int32(0))), false, "offer.metered.estimate_positive"}, + {"offer metered negative estimate rejected", meteredOffer(meteredPricing(proto.Int32(-5))), false, "offer.metered.estimate_positive"}, + {"offer flat without estimate ok", &forav1.Offer{OfferId: "of_flat", Exchange: exampleExchange, Pricing: &forav1.Pricing{Model: forav1.PricingModel_PRICING_MODEL_FLAT, Rate: "1", Currency: "USD"}, Terms: freeTerms()}, true, ""}, + // Offer.terms carries no pricing: the price is stated once, in + // Offer.pricing. The metered rule reads Offer.pricing only, so a PER_UNIT + // term under FLAT offer pricing is refused for the term's pricing alone. + {"offer term with pricing rejected", pricedTermOffer(meteredPricing(proto.Int32(2500)), meteredPricing(proto.Int32(2500))), false, "offer.terms.pricing_unset"}, + {"offer term with free pricing rejected", pricedTermOffer(freePricing(), freePricing()), false, "offer.terms.pricing_unset"}, + {"offer per_unit term under flat pricing rejected", pricedTermOffer(&forav1.Pricing{Model: forav1.PricingModel_PRICING_MODEL_FLAT, Rate: "1", Currency: "USD"}, meteredPricing(nil)), false, "offer.terms.pricing_unset"}, + {"transaction with priced offer term rejected", &forav1.TransactionRequest{IdempotencyKey: "idem-tx-p", Requester: exampleRequester(), Items: []*forav1.TransactionItem{{Offer: pricedTermOffer(freePricing(), freePricing())}}}, false, "offer.terms.pricing_unset"}, + {"transaction with unestimated metered offer ok", &forav1.TransactionRequest{IdempotencyKey: "idem-tx-m", Requester: exampleRequester(), Items: []*forav1.TransactionItem{{Offer: meteredOffer(meteredPricing(nil))}}}, true, ""}, + {"transaction with zero-estimate metered offer rejected", &forav1.TransactionRequest{IdempotencyKey: "idem-tx-z", Requester: exampleRequester(), Items: []*forav1.TransactionItem{{Offer: meteredOffer(meteredPricing(proto.Int32(0)))}}}, false, "offer.metered.estimate_positive"}, + // Pricing.unit format: empty / bare-dashed / vendor:namespaced. {"pricing unit bare ok", &forav1.Pricing{Model: forav1.PricingModel_PRICING_MODEL_PER_UNIT, Unit: proto.String("sq-km"), Rate: "1"}, true, ""}, {"pricing unit vendor ok", &forav1.Pricing{Model: forav1.PricingModel_PRICING_MODEL_PER_UNIT, Unit: proto.String("acme:widgets"), Rate: "1"}, true, ""}, @@ -149,14 +167,19 @@ func licensingCases() []validationCase { {"resource entry title over cap rejected", &forav1.ResourceEntry{Domain: "publisher.example", Path: "/x", Title: proto.String(strings.Repeat("t", 513))}, false, "string.max_len"}, {"resource entry negative word count rejected", &forav1.ResourceEntry{Domain: "publisher.example", Path: "/x", WordCount: proto.Int32(-1)}, false, "int32.gte"}, {"resource entry 32 terms ok", &forav1.ResourceEntry{Domain: "publisher.example", Path: "/x", Terms: enumeratedTerms(32)}, true, ""}, + // A catalog term carries its price; the rule is the entry's, because an + // offer's term carries none. + {"resource entry term missing pricing rejected", &forav1.ResourceEntry{Domain: "publisher.example", Path: "/x", Terms: []*forav1.LicenseTerm{{Semantics: forav1.TermSemantics_TERM_SEMANTICS_ENUMERATED}}}, false, "resource_entry.terms.pricing_required"}, {"resource entry 33 terms rejected", &forav1.ResourceEntry{Domain: "publisher.example", Path: "/x", Terms: enumeratedTerms(33)}, false, "repeated.max_items"}, // Catalog request lists — an empty push or removal asks for nothing. {"push request with one entry ok", &forav1.PushResourcesRequest{Exchange: "exchange.example", Entries: []*forav1.ResourceEntry{{Domain: "publisher.example", Path: "/x"}}}, true, ""}, {"push request without entries rejected", &forav1.PushResourcesRequest{Exchange: "exchange.example"}, false, "repeated.min_items"}, - {"remove request with one path ok", &forav1.RemoveResourcesRequest{Exchange: "exchange.example", Paths: []string{"/x"}}, true, ""}, - {"remove request without paths rejected", &forav1.RemoveResourcesRequest{Exchange: "exchange.example"}, false, "repeated.min_items"}, - {"remove request relative path rejected", &forav1.RemoveResourcesRequest{Exchange: "exchange.example", Paths: []string{"x"}}, false, "string.pattern"}, + {"remove request with one resource ok", &forav1.RemoveResourcesRequest{Exchange: "exchange.example", Resources: []*forav1.ResourceRef{{Domain: "publisher.example", Path: "/x"}}}, true, ""}, + {"remove request without resources rejected", &forav1.RemoveResourcesRequest{Exchange: "exchange.example"}, false, "repeated.min_items"}, + {"remove request with only deprecated paths rejected", &forav1.RemoveResourcesRequest{Exchange: "exchange.example", Paths: []string{"/x"}}, false, "repeated.min_items"}, + {"remove request relative path rejected", &forav1.RemoveResourcesRequest{Exchange: "exchange.example", Resources: []*forav1.ResourceRef{{Domain: "publisher.example", Path: "x"}}}, false, "string.pattern"}, + {"remove request schemed domain rejected", &forav1.RemoveResourcesRequest{Exchange: "exchange.example", Resources: []*forav1.ResourceRef{{Domain: "https://publisher.example", Path: "/x"}}}, false, "string.pattern"}, // Quota.metric format — bare-dashed or vendor:namespaced; empty rejected. // window set so the only variable under test is metric. @@ -170,9 +193,12 @@ func licensingCases() []validationCase { {"license uri with digest ok", &forav1.License{Uri: proto.String("https://x.example/lic"), UriDigest: proto.String("sha256:" + hex64)}, true, ""}, {"license uri without digest rejected", &forav1.License{Uri: proto.String("https://x.example/lic")}, false, "license.digest_required_with_uri"}, - // LicenseTerm presence invariants (pricing required; REFERENCE_ONLY needs license.uri). + // LicenseTerm presence invariants (REFERENCE_ONLY needs license.uri). + // Whether pricing is required depends on the message holding the term + // — required on ResourceEntry.terms, unset on Offer.terms — so a term on + // its own is valid either way. {"term enumerated with pricing ok", &forav1.LicenseTerm{Semantics: forav1.TermSemantics_TERM_SEMANTICS_ENUMERATED, Pricing: freePricing()}, true, ""}, - {"term missing pricing rejected", &forav1.LicenseTerm{Semantics: forav1.TermSemantics_TERM_SEMANTICS_ENUMERATED}, false, "required"}, + {"term without pricing ok", &forav1.LicenseTerm{Semantics: forav1.TermSemantics_TERM_SEMANTICS_ENUMERATED}, true, ""}, {"term reference_only with license uri ok", &forav1.LicenseTerm{Semantics: forav1.TermSemantics_TERM_SEMANTICS_REFERENCE_ONLY, Pricing: freePricing(), License: &forav1.License{Uri: proto.String("https://x.example/lic"), UriDigest: proto.String("sha256:" + hex64)}}, true, ""}, {"term reference_only without license uri rejected", &forav1.LicenseTerm{Semantics: forav1.TermSemantics_TERM_SEMANTICS_REFERENCE_ONLY, Pricing: freePricing()}, false, "license_term.reference_only.requires_uri"}, @@ -204,8 +230,15 @@ func licensingCases() []validationCase { // not just the licensing subtree. {"authorized_exchange relationship set ok", &forav1.AuthorizedExchange{Domain: exampleExchange, Relationship: forav1.ProviderRelationship_PROVIDER_RELATIONSHIP_DIRECT}, true, ""}, {"authorized_exchange relationship unspecified rejected", &forav1.AuthorizedExchange{Domain: exampleExchange}, false, "enum.not_in"}, - {"requester type set ok", &forav1.Requester{Domain: "agent.example", Type: forav1.RequesterType_REQUESTER_TYPE_AGENT}, true, ""}, - {"requester type unspecified rejected", &forav1.Requester{Domain: "agent.example"}, false, "enum.not_in"}, + {"requester type set ok", &forav1.Requester{Id: "agent-1", Domain: "agent.example", Type: forav1.RequesterType_REQUESTER_TYPE_AGENT}, true, ""}, + {"requester type unspecified rejected", &forav1.Requester{Id: "agent-1", Domain: "agent.example"}, false, "enum.not_in"}, + // The acceptance signatures name the requester, so both halves of it are + // required: an empty id or domain would let the signed bytes name nobody. + {"requester id empty rejected", &forav1.Requester{Domain: "agent.example", Type: forav1.RequesterType_REQUESTER_TYPE_AGENT}, false, "string.min_len"}, + {"requester id over 255 rejected", &forav1.Requester{Id: strings.Repeat("a", 256), Domain: "agent.example", Type: forav1.RequesterType_REQUESTER_TYPE_AGENT}, false, "string.max_len"}, + {"requester id at 255 ok", &forav1.Requester{Id: strings.Repeat("a", 255), Domain: "agent.example", Type: forav1.RequesterType_REQUESTER_TYPE_AGENT}, true, ""}, + {"requester domain empty rejected", &forav1.Requester{Id: "agent-1", Type: forav1.RequesterType_REQUESTER_TYPE_AGENT}, false, "string.pattern"}, + {"requester domain with scheme rejected", &forav1.Requester{Id: "agent-1", Domain: "https://agent.example", Type: forav1.RequesterType_REQUESTER_TYPE_AGENT}, false, "string.pattern"}, {"resource_identity mutability set ok", &forav1.ResourceIdentity{ResourceMutability: forav1.ResourceMutability_RESOURCE_MUTABILITY_STATIC}, true, ""}, {"resource_identity mutability unspecified rejected", &forav1.ResourceIdentity{}, false, "enum.not_in"}, {"well_known_manifest role set ok", &forav1.WellKnownManifest{Role: forav1.Role_ROLE_AGENT}, true, ""}, @@ -319,9 +352,14 @@ func TestIdempotencyKeyRequired(t *testing.T) { func idempotencyCases() []validationCase { return []validationCase{ - {"transaction empty key rejected", &forav1.TransactionRequest{IdempotencyKey: "", Items: []*forav1.TransactionItem{{Offer: &forav1.Offer{OfferId: "of_1", Exchange: exampleExchange, Pricing: freePricing(), Terms: freeTerms()}}}}, false, "string.min_len"}, - {"transaction key ok", &forav1.TransactionRequest{IdempotencyKey: "idem-tx-1", Items: []*forav1.TransactionItem{{Offer: &forav1.Offer{OfferId: "of_1", Exchange: exampleExchange, Pricing: freePricing(), Terms: freeTerms()}}}}, true, ""}, - {"transaction empty items rejected", &forav1.TransactionRequest{IdempotencyKey: "idem-tx-empty"}, false, "repeated.min_items"}, + {"transaction empty key rejected", &forav1.TransactionRequest{IdempotencyKey: "", Requester: exampleRequester(), Items: []*forav1.TransactionItem{{Offer: &forav1.Offer{OfferId: "of_1", Exchange: exampleExchange, Pricing: freePricing(), Terms: freeTerms()}}}}, false, "string.min_len"}, + {"transaction key ok", &forav1.TransactionRequest{IdempotencyKey: "idem-tx-1", Requester: exampleRequester(), Items: []*forav1.TransactionItem{{Offer: &forav1.Offer{OfferId: "of_1", Exchange: exampleExchange, Pricing: freePricing(), Terms: freeTerms()}}}}, true, ""}, + {"transaction empty items rejected", &forav1.TransactionRequest{IdempotencyKey: "idem-tx-empty", Requester: exampleRequester()}, false, "repeated.min_items"}, + // A query and a purchase name their requester: the message is required, so + // an absent one cannot skip the Requester.id and Requester.domain rules. + {"transaction without requester rejected", &forav1.TransactionRequest{IdempotencyKey: "idem-tx-nr", Items: []*forav1.TransactionItem{{Offer: &forav1.Offer{OfferId: "of_1", Exchange: exampleExchange, Pricing: freePricing(), Terms: freeTerms()}}}}, false, "required"}, + {"resource query without requester rejected", &forav1.ResourceQuery{}, false, "required"}, + {"discovery request without requester rejected", &forav1.DiscoveryRequest{}, false, "required"}, {"usage report empty key rejected", &forav1.UsageReport{IdempotencyKey: "", Exchange: exampleExchange}, false, "string.min_len"}, {"usage report key ok", &forav1.UsageReport{IdempotencyKey: "idem-ur-1", Exchange: exampleExchange}, true, ""}, {"dispute empty key rejected", &forav1.DisputeRequest{IdempotencyKey: "", Exchange: exampleExchange, Reason: forav1.DisputeReason_DISPUTE_REASON_CONTENT_MISMATCH}, false, "string.min_len"}, @@ -335,17 +373,51 @@ func idempotencyCases() []validationCase { // to exercise. const exampleExchange = "exchange.example" +// meteredPricing is a PER_UNIT price per token with the given estimate (nil +// leaves it unset). +func meteredPricing(estimate *int32) *forav1.Pricing { + return &forav1.Pricing{ + Model: forav1.PricingModel_PRICING_MODEL_PER_UNIT, Unit: proto.String("tokens"), Currency: "USD", Rate: "0.00002", + EstimatedQuantity: estimate, + } +} + +// meteredOffer is an offer under the given offer pricing whose term carries no +// pricing, as every offer's term does: the offer's price is Offer.pricing. +func meteredOffer(pricing *forav1.Pricing) *forav1.Offer { + return &forav1.Offer{ + OfferId: "of_metered", Exchange: exampleExchange, Pricing: pricing, + Terms: []*forav1.LicenseTerm{{Semantics: forav1.TermSemantics_TERM_SEMANTICS_ENUMERATED}}, + } +} + +// pricedTermOffer is an offer under offerPricing whose term carries termPricing +// — a second copy of the price, which the offer.terms.pricing_unset rule +// refuses. +func pricedTermOffer(offerPricing, termPricing *forav1.Pricing) *forav1.Offer { + return &forav1.Offer{ + OfferId: "of_priced_term", Exchange: exampleExchange, Pricing: offerPricing, + Terms: []*forav1.LicenseTerm{{Semantics: forav1.TermSemantics_TERM_SEMANTICS_ENUMERATED, Pricing: termPricing}}, + } +} + +// exampleRequester is a valid Requester: query and purchase cases carry one, since the +// message is required on both. +func exampleRequester() *forav1.Requester { + return &forav1.Requester{Id: "agent-1", Domain: "agent.example", Type: forav1.RequesterType_REQUESTER_TYPE_AGENT} +} + func freePricing() *forav1.Pricing { return &forav1.Pricing{Model: forav1.PricingModel_PRICING_MODEL_FREE, Rate: "0"} } // freeTerms is the single term a valid Offer sells. Offer.terms is bounded to // exactly one — an offer IS one licensing arrangement — so a fixture offer -// carries the term it sells rather than an empty list. +// carries the term it sells rather than an empty list. The term carries no +// pricing: the offer's price is Offer.pricing. func freeTerms() []*forav1.LicenseTerm { return []*forav1.LicenseTerm{{ Semantics: forav1.TermSemantics_TERM_SEMANTICS_ENUMERATED, - Pricing: freePricing(), }} } @@ -403,6 +475,8 @@ func errorDetailCases() []validationCase { {"retrieval_auth_failure unspecified rejected", &forav1.RetrievalAuthFailure{Reason: forav1.RetrievalAuthFailureReason_RETRIEVAL_AUTH_FAILURE_REASON_UNSPECIFIED}, false, "enum.not_in"}, {"usage_report_rejection valid", &forav1.UsageReportRejection{Reason: forav1.UsageReportRejectionReason_USAGE_REPORT_REJECTION_REASON_DUPLICATE}, true, ""}, {"usage_report_rejection unspecified rejected", &forav1.UsageReportRejection{Reason: forav1.UsageReportRejectionReason_USAGE_REPORT_REJECTION_REASON_UNSPECIFIED}, false, "enum.not_in"}, + {"request_auth_failure valid", &forav1.RequestAuthFailure{Reason: forav1.RequestAuthFailureReason_REQUEST_AUTH_FAILURE_REASON_SIGNATURE_STALE}, true, ""}, + {"request_auth_failure unspecified rejected", &forav1.RequestAuthFailure{Reason: forav1.RequestAuthFailureReason_REQUEST_AUTH_FAILURE_REASON_UNSPECIFIED}, false, "enum.not_in"}, // ErrorDetail wrapper: carries a generic class (no typed reason) or a valid // typed detail; a nested invalid reason fails through the wrapper. @@ -424,6 +498,7 @@ var standardRuleIDs = map[string]bool{ "repeated.min_items": true, "int64.gte": true, "int32.gte": true, + "int32.gte_lte": true, "string.max_len": true, "string.min_len": true, "enum.not_in": true, diff --git a/docs/design-history.md b/docs/design-history.md index 2e804731..52f00543 100644 --- a/docs/design-history.md +++ b/docs/design-history.md @@ -9,6 +9,18 @@ git history. ## Request and hop authentication: RFC 9421, hop-by-hop +> **Superseded in part — FORA's Broker no longer forwards.** Each party now signs the +> request it authors: a Broker originates its discovery queries and re-packages a +> purchase, signing each alone, so a FORA request carries one signature. A key is +> resolved in the WBA directory its signer's covered `Signature-Agent` member names, +> not from `/.well-known/fora.json`. `WellKnownManifest.max_intermediary_hops` counts +> every signature and is checked before any is verified; `RequestConstraints.max_hops` +> reaches no Exchange and no party enforces it. Each request is answered to the party +> that sent it. The move from in-message signatures to RFC 9421 described below still +> stands. Why the Broker signs alone is recorded in "A Broker signs alone, because it +> changes what the agent signed"; see also "Signing under the Web Bot Auth profile" +> and "The hop cap counts signatures; max_hops waits for multi-hop Broker discovery". + Early drafts carried authentication inside the protobuf messages: per-message signature fields on requests, on each intermediary hop, and on catalog pushes. We moved all request authentication to RFC 9421 HTTP Message Signatures @@ -40,6 +52,12 @@ agent's identity (`agent_identity_hash`) and needs no return-path relay. ## Canonical signing: JCS over proto-JSON, not deterministic protobuf +> **Superseded in part.** An acceptance can no longer name an empty requester: +> `Requester.id` now requires at least one character, and the acceptance signers and +> verifiers refuse an empty requester id or domain, so the divergence below can no +> longer arise. The omission rule it taught still binds every empty field, and the +> acceptance vectors pin it with an empty `idempotency_key`. + Three signed FORA payloads cover a protobuf message: `Offer.signature`, the agent's `AgentAcceptance.signature`, and the agent's `AgentRequestAcceptance.signature` — the request-set proof, added later and born @@ -402,6 +420,10 @@ renderer a language reaches for. ## Recipient addressing: a body field, not the signed request URL +> **Superseded in part.** FORA's Broker no longer forwards a request verbatim, so the +> verbatim-forwarded path mentioned below no longer exists in FORA: every request is +> signed by the party that authored it. The rest of the reasoning stands. + Every addressed request carries `exchange`, the bare host of its intended recipient, and a recipient rejects a request naming someone else. The obvious objection is that the RFC 9421 request signature already covers `@target-uri`, so @@ -512,6 +534,13 @@ through the same dispute chain as the rest of the protocol. ## Universal Licensing Core +> **Superseded in part.** An offer now carries its price once, in `Offer.pricing`: +> the term inside `Offer.terms` carries no pricing, while every catalog term in +> `ResourceEntry.terms` still does. The two ends therefore no longer share the +> term's exact shape: the price a publisher pushes on a term reaches the agent as +> `Offer.pricing`. The closed licensing core and agent-selected restrictions below +> stand. + Licensing collapsed from several parallel, partly-overlapping shapes into one `LicenseTerm` that carries `License`, `Restriction`, `Quota`, `Obligation`, and `Pricing`, and appears with the *same shape* at both ends of the protocol — @@ -569,6 +598,17 @@ records only the wire-shaping reasoning. > still stands — it is the same separation (identity = signature, > entitlement = scopes/delegation) taken one step further: billing needs no > request field at all. +> +> **Superseded in part — the account on a purchase a Broker relays.** There the +> Broker signs the request, so the signature that names the account is the agent's +> acceptance, verified against the directory `Requester.domain` names, not the +> request signature. Identity is still a signature and never a field. See "A +> purchase a Broker relays is the agent's". +> +> **Superseded in part — what scopes are.** The scopes a requester states are its own +> signed statement, which the Exchange does not verify: they decide which terms are +> presented. A delegation the Exchange verifies grants scopes, where an Exchange +> requires one. See "A requester's scopes are its own statement". `Requester` once carried a `license_id`, and the name implied that the value was what entitled the requester to access — that holding the right `license_id` @@ -628,6 +668,18 @@ contiguous, with no gaps or reused numbers left by the removals. ## Delegation-claims profile: opaque token, bound holder +> **Superseded in part.** The request signer is no longer always the holder. On a +> direct request the key that signs the RFC 9421 request must still equal the holder +> key the token names. On a purchase a Broker relays, the Broker signs the request, +> so the Exchange checks the holder binding against the key each item's +> `AgentAcceptance` verifies under, and an item without one fails it. A Broker is +> never delegated to. On a query a Broker originated, nothing can pass the holder +> binding, so the Exchange does not honour the delegation and answers as if none were +> present. How an agent proves a holder-of-key entitlement at discovery time through a +> Broker is an open protocol question. See "A Broker signs alone, because it changes +> what the agent signed" and "A delegation on a query a Broker originated is not +> honoured". + Delegation tokens stay opaque on the wire — FORA does not parse or re-encode the token, and `token_format` only names which verifier to run. But leaving the *meaning* of a token entirely to each issuer made delegations non-portable: an @@ -669,6 +721,12 @@ default.) ## One normative scope-matching algorithm +> **Superseded in part — what a match decides.** The algorithm stands. The requester +> side it compares is the requester's own statement, which the Exchange does not +> verify, so a match decides which terms are presented, not what an actor may reach, +> unless a verified delegation grants the scopes. See "A requester's scopes are its own +> statement". + Scopes appear in two places — on the requester/delegation side (what an actor may reach) and on `LicenseTerm.scopes` (what a term covers) — and earlier drafts left the matching semantics implicit, which invited each implementation to choose its @@ -687,6 +745,13 @@ normative rule. ## Revenue share stays off-protocol +> **Superseded in part — how the deal's party reaches the term.** It states the +> matching scope in its signed request and need not hold a delegation; a delegation +> counts only where an Exchange requires one before it presents a scoped term. An agent +> that states the scope without being party to the deal is shown the term, and +> reconciling purchases against the deal's parties exposes it. See "A requester's +> scopes are its own statement". + CoMP's `License` carries a `revshare` rate next to `price`, and FORA's `PricingModel` was reduced to three charging structures (`FREE`, `PER_UNIT`, `FLAT`), so the conspicuous question is whether FORA should add a @@ -721,6 +786,13 @@ minimal and its signed offers stay price-comparable. > sole delegation `token_format`, and the entitlement *mechanism* (a > holder-bound capability token on a covered header) is unchanged — only the > Biscuit format is gone. Read the "still optional" language below as historical. +> +> **Superseded in part — the key that proves possession.** On a direct request the +> agent still proves possession with its RFC 9421 request signature. On a purchase a +> Broker relays, the Broker signs the request, so the holder binding is checked +> against the key each item's `AgentAcceptance` verifies under. The `cnf` chain and +> its offline walk below stand. See "Delegation-claims profile: opaque token, bound +> holder". The delegation token started as a Biscuit (`token_format` defaulted to `"biscuit-v3"`), chosen for offline attenuation. Modelling the holder-binding @@ -784,6 +856,11 @@ to look for. ## A failed action is not a success body +> **Superseded in part.** A purchase no longer splits by item count. Every per-item +> denial, a one-item purchase's included, is answered in the body on +> `TransactionResultItem.denial_reason`, and `ErrorDetail.transaction_denial` is +> sent only when the Exchange refuses the whole request and decides no item. + The corollary decision is where a failure lives. The rule is the gRPC / AIP-193 one: a method that could not perform the requested *action* returns a non-OK code plus an `ErrorDetail`; a *query* that ran successfully returns its answer in the @@ -898,6 +975,16 @@ autolink pass could not see into. All of it is deleted. `protoc-gen-foravocab` a ## A covered header the peer never receives is not bound +> **Superseded in part.** The fifth covered component is now the signer's own +> `"signature-agent";key="